이번 장에서는 기존 코드를 이용해서 람다와 스트림 API를 활용하는 새로운 프로젝트를 시작하는 상황을 가정한다. 즉, 람다 표현식을 이용해 가독성과 유연성을 높이려면 기존 코드를 어떻게 리팩터링 해야하는지 설명한다.

람다표현식으로 전략, 탬플릿 메서드, 옵저버, 의무체인, 팩토리 등의 객체지향 디자인 패턴을 어떻게 간소화 할 수 있는지 살펴본다.

객체 지향 디자인 패턴

  1. 싱글톤 패턴
    어떤 클래스의 객체가 프로세스 내에서 단 한개만 만들어져야 하는 경우
    전역 변수를 사용하지 않고 객체를 하나만 생성 하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
    정적변수에 인스턴스를 만들어 초기화 하는 방식 이용

  2. 전략 패턴
    프로그램 실행 중 선택된 모드에 따라 실행되는 방식

  3. State 패턴
    Strategy 와 비슷하지만 Strategy 패턴이 지정된 특정 메소드가 모듈화된 모드에 따라 다르게 실행되도록 하는 거라면 State 패턴은 메소드가 실행될 때 그 모드도 전환되는 것이다.

  4. Command 패턴
    Strategy 패턴은 같은 일을 하고 알고리즘이나 방식이 갈아끼워지는 것이라면 커맨드 패턴은 그 하는 일 자체가 다르다.
    작성하는 방식이 다양

  5. Adapter 패턴
    한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환한다.
    어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

  6. Proxy 패턴
    대상 원본 객체를 대리하여 대신 처리하게 함으로써 로직의 흐름을 제어하는 행동 패턴

  7. Facade 패턴
    파사드 패턴은 일련의 저수준 인터페이스들을 하나의 고수준 인터페이스로 묶어주는 패턴이다.
    한 작업에서 여러 클래스를 생성하여 복잡한 작업을 해야할 때, 이 과정들을 외벽 뒤로 숨겨 추상화 할 때 사용된다.

  8. 탬플릿 메서드 패턴
    전체적으로 동일하면서 부분적으로는 다른 구문으로 구성된 메서드의 코드 중복을 최소화할때 유용
    전체 알고리즘은 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있게 하는데 사용됨

  9. 데코레이터 패턴
    객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴

  10. 팩토리 메서드 패턴
    클래스의 인스턴스를 만드는 일을 서브 클래스에게 맡김
    팩토리 메서드를 사용하면 객체 생성부분이 변경되었을 경우 팩토리 클래스만 보면된다. 조건에 따라 객체를 생성해 가져오는 작업을 팩토리 클래스에게 위임해 개발자가 세부 구현 사항에 대해 알 필요가 없다.

  11. Abstract-factory 추상팩토리 패턴
    다양한 구성 요소 별로 '객체의 집합'을 생성해야 할 때 유용하다.
    서로 관련있는 여러 객체를 만들어주는 인터페이스를 제공하는 패턴

  12. Mediate 패턴
    특정 이벤트에 반응해서 관련된 다른 클래스들에 알려주는 일을 mediator 역할을 하는 클래스에게 전담시키는 것
    여러 클래스들의 관계가 특정 이벤트들을 중심으로 복잡하게 얽힌 설계에서 유용하게 사용

  13. Composite 패턴
    객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴

  14. 옵저버 패턴
    한 객체의 상태 변화에 따라 다른 객체의 상태도 연동되도록 일대다 객체 의존 관계를 구성 하는 패턴

  15. 의무체인 패턴
    작업 처리 객체의 체인(동작 체인)을 만들 때는 의무 체인 패턴을 사용한다.
    한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야할 작업을 처리한 다음에 또 다른 객체로 전달하는 식

9.1 가독성과 유연성을 개선하는 리팩터링

9.1.1 코드 가독성 개선

가독성을 개선한다는 것은 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미한다.
코드 가독성을 높이려면 코드의 문서화를 잘 하고, 표준 코딩 규칙을 준수하는 등의 노력을 기울여야 한다.

9.1.2 익명 클래스를 람다표현식으로 리팩터링하기

익명클래스를 람다 표현식으로 변환하면 간결하고 가독성 좋은 코드를 구현할 수 있다.

익명클래스를 람다표현식으로 변환할 때 주의점

  1. 익명 클래스에서 사용한 this 와 super 는 람다표현식에서 다른 의미를 갖는다. 즉, 익명 클래스에서 this 는 익명 클래스 자신을 가리키지만 람다에서 this 는 람다를 감싸는 클래스를 가리킨다.

  2. 익명클래스는 감싸고있는 클래스의 변수를 가릴 수 있지만, 람다 표현식으로는 가릴 수 없다.

  3. 익명클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래될 수 있다. 익명클래스는 인스턴스화할때 명시적으로 형식이 정해지는 반면 람다의 형식은 컨텍스트에 따라 달라지기 때문이다.

interface Task {
  public void execute();
}
public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task a) { r.run(); }

//익명클래스로 전달 가능
doSomethig(new Task() {
  public void execute() {
    System.out.println("Danger danger!!");
  }
});

//Runnable과 Task 모두 대상 형식이 가능하므로 모호함 발생
doSomething(() -> System.out.println("Danger danger!!"));

//명시적 형변환을 사용해서 모호함을 제거할 수 있음
doSomething((Task)() -> System.out.println("Danger danger!!"));

9.1.3 람다 표현식을 메서드 참조로 리팩터링하기

메서드 참조를 사용하면 메서드명으로 코드의 의도를 명확히 알릴 수 있다.
또한 comparing과 maxBy같은 정적 헬퍼 메서드를 활용하는 것도 좋다.


inventory.sort(
  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); //비교구현에 신경써야함
inventory.sort(comparing(Apple::getWeight)); //코드가 문제 자체를 설명
  • 해결 2가지 방법(2번 방법 권장)
//람다 + 저수준 리듀싱 사용
int totalaCalories = menu.stream().map(Dish::getCalories)
  .reduce(0, (c1, c2) -> c1 + c2);
  
//내장 컬렉터 사용
int totalaCalories = menu.stream().collect(summingInt(Dish::getCalories));

9.1.4 명령형 데이터 처리를 스트림으로 리팩터링하기

스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여준다. 스트림은 쇼트서킷과 게으름이라는 강력한 최적화뿐 아니라 멀티코어 아키텍처를 활용할 수 있는 지름길을 제공한다.

List<String> dishNames = new ArrayList<>();
for(Dish dish : menu) {
  if(dish.getCalories() > 300 ) {
    dishNames.add(dish.getName());
  }
}

//스트림 API로 변환하면 더 직접적으로 기술 & 쉽게 병렬화 가능
menu.parallelStream()
  .filter(d -> d.getCaloires() > 300)
  .map(Dish::getName)
  .collect(toList());

9.1.5 코드 유연성 개선

람다표현식을 이용하려면 함수형 인터페이스가 필요하다

조건부 연기 실행

if(logger.isLoggale(Log.FINER)) {
  logger.finner("Problem : " + generateDiagnostic());
}

위 코드는 logger의 상태가 isLoggable 메서드에 의해 클라이언트 코드로 노출된다. 또한 메시지를 로깅할때마다 logger의 객체를 매번 확인해야 한다.

logger.log(Lelvel.FINER, "Problem : " + generateDiagnostic());

이번엔 불필요한 if문을 제거하고 logger의 상태를 노출할 필요도 없어졌지만, logger가 활성화되어 있지 않더라도 항상 로깅 메시지를 평가하게 된다.

logger.log(Level.FINER, () ->  "Problem : " + generateDiagnostic());

supplier를 인수로 갖는 오버로드된 log 메서드를 통해 위와같이 해결할 수 있다.
다음은 log 메서드의 내부구현 코드이다.

public void log(Level level, Supplier<String> msgSupplier) {
  if(logger.isLoggable(level)) {
    log(level, msgSupplier.get()); //람다 실행
  }
}

실행어라운드

매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 람다로 변환할 수 있다.

9.2 람다로 객체지향 디자인 패턴 리팩터링하기

9.2.1 전략패턴

전략패턴은 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법이다.

//String 문자열을 검증하는 인터페이스
public interface ValidationStrategy {
  boolean execute(String s);
}


//인터페이스를 구현하는 클래스
public class IsAllLowerCase implements ValidationStrategy {
  public boolean execute(String s) {
    return s.matches("[a-z]+");
  }
}

public class IsNumeric implements ValidationStrategy {
  public boolean execute(String s) {
    return s.matches("\\d+");
  }
}

//전략객체를 사용하는 클래스
public class Validator {
  private final ValidationStrategy strategy;
  public Validator(ValidationStrategy v) {
    this.strategy = v;
  }
  public boolean validate(String s) {
    return strategy.execute(s);
  }
}

Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("AAA"); //false
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbb"); //true

람다 표현식 사용

다양한 전략을 구현하는 새로운 클래스를 구현할 필요 없이 람다 표현식을 직접 전달하면 코드가 간결해진다.

Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+")); //람다를 직접 전달
boolean b1 = numericValidator.validate("AAA");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+");
boolean b2 = lowerCaseValidator.validate("bbb");

9.2.2 탬플릿메서드

알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용한다.
다시말해, 이 알고리즘을 사용하고 싶은데 그대로는 안되고 조금 고쳐야하는 상황에 적합하다.

abstract class OnlineBanking {
  public void processCustomer(int id) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy(c);
  }
  abstract void makeCustomerHappy(Customer c);
}

processCustomer 메서드는 온라인 뱅킹 알고리즘이 해야 할 일을 보여준다.
각각의 지점은 OnlineBanking 클래스를 상속받아 makeCustomerHappy 메서드가 원하는 동작을 수행하도록 구현할 수 있다.

람다 표현식 사용

//Consumer<Customer> 형식의 두번째 인수를 추가
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
  Customer c = Database.getCustomerWithId(id);
  makeCustomerHappy.accept(c);
}

//람다표현식을 직접 전달 가능
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello" + c.getName());

9.2.3 옵저버

어떤 이벤트가 발생했을 때 한 객체(주제)가 다른 객체 리스트(옵저버)에 자동으로 알림을 보내야하는 상황에서 옵저버 디자인 패턴을 사용한다.

//1. 옵저버
//주제가 호출할 수 있도록 notify 메서드 제공
interface Observer {
  void notify(String tweet);
}

Class NYTimes implements Observer {
  public void notify(String tweet) {
    if(tweet != null && tweet.contains("money")) {
      System.out.println("Breaking news in NY!" + tweet);
    }
  }
}

Class Guardian implements Observer {
  public void notify(String tweet) {
    if(tweet != null && tweet.contains("queen")) {
      System.out.println("Yet more news from London..." + tweet);
    }
  }
}

//2. 주제
interface Subject {
  void registerObserver(Observer o);
  void notifyObservers(String tweet);
}

Class Feed implements Subject {
  private final List<Observer> observers = new ArrayList<>();
  public void registerObserver(Observer o) {
    this.observers.add(0);
  }
  public void notifyServeres(String tweet) {
    observers.forEach(o -> o.notify(tweet));
  }
}

Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.notifyServeres("The queen said her favorite book is Modern Java in Action!");

람다 표현식 사용

옵저버를 명시적으로 인스턴스화하지 않고 람다표현식을 직접 전달해서 실행할 동작을 지정할 수 있다.

f.registerObserver((String tweet) -> {
  if(tweet != null && tweet.contains("money")) {
    System.out.println("Breaking news in NY!" + tweet);
  }
});

f.registerObserver((String tweet) -> {
  if(tweet != null && tweet.contains("queen")) {
    System.out.println("Yet more news from London..." + tweet);
  }
});

9.2.4 의무체인

작업 처리 객체의 체인(동작 체인 등)을 만들 떄는 의무 체인 패턴을 사용한다.

public abstract class processingObject<T> {
  protected ProcessingObject<T> successor;
  public void setSuccessor(ProcessingObject<T> successor) {
    this.successor = successor;
  }
  public T handle(T input) {
    T r = handleWork(input);
    if(successor != null) {
      return successor.handle(r);
    }
    return r;
  }
  abstract protected T handleWork(T input);
}

handle 메서드는 일부 작업을 어떻게 처리해야할지 전체적으로 기술한다.

ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들 수 있다.

public class HeaderTextProcessing extends ProcessingObject<String> {
  public String handleWork(String text) {
    return "From Raoul, Mario and Alan : " + text;
  }
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
  public String handleWork(String text) {
    return text.replaceAll("labda", "lambda");
  }
}

ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2); //두 작업처리 객체를 연결
String result = p1.handle("Aren't labdas really sexy?!");
System.out.println(result);

람다 표현식 사용

작업처리 객체를 UnaryOperator<String> 형식의 인스턴스를 표현할 수 있으며, andThen 메서드로 이들 함수를 조합해서 체인을 만들 수 있다.

UnaryOperator<String> headerProcessing = 
  (String text) -> "From Raoul, Mario and Alan : " + text;
UnaryOperator<String> spellCheckerProcessing = 
  (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!");

9.2.5 팩토리

인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴을 사용한다.

public class ProdectFactor {
  public static Product createProduct(String name) {
    switch(name) {
      case "loan" : return new Loan();
      case "stock" : return new Stock();
      case "bond" : return new Bond();
      default : throw new RuntimeException("No Such product" + name);
    }
  }
}

product p = ProductFactory.createProduct("loan");

여기서 Loan, Stock, Bond는 모두 Product의 서브 형식이다. 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순하게 상품을 생산할 수 있다.

람다 표현식 사용

//상품명과 생성자를 연결하는 Map 코드로 재구현
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
  map.put("loan", Loan::new);
  map.put("stock", Stock::new);
  map.put("bond", Bond::new);
}

//Map을 이용해 다양한 상품을 인스턴스화
public static Product createProduct(String name) {
  Supplier<product> p = map.get(name);
  if(p != null) return p.get();
  throw new RuntimeException("No Such product" + name);
}

9.3 람다 테스팅

9.3.1 보이는 람다 표현식의 동작 테스팅

람다는 익명이므로 테스트 코드 이름을 호출 할 수 없다.
따라서 필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직을 테스트 할 수 있다.
아래와 같이 메서드를 호출하는 것처럼 람다를 사용하면 된다.

public class Point {
  public final static Comparator<Point> compareByXAndThenY = 
    comparing(Point::getX).thenComparing(Point::getY);;
 }
 
 //TEST 코드
 @Test
 public void testComparingTwoPoints() throws Exception {
   Point p1 = new Point(10, 15);
   Point p2 = new Point(10, 20);
   int result = Point.compareByXThenY.compare(p1, p2);
   assertTrue(result < 0);
 }

람다 표현식은 함수형 인터페이스의 인스턴스를 생성한다는 사실을 기억하자. 따라서 생성된 인터페이스의 동작으로 람다 표현식을 테스트할 수 있다.

위의 코드에서 compareByXAndThenY는 Comparator 인터페이스의 인스턴스로 초기화된다. comparing 및 thenComparing 메서드는 Comparator를 생성하는 메서드로, 람다 표현식을 전달하여 어떻게 비교할지를 정의한다.Point::getX 및 Point::getY는 두 개의 Point 객체를 비교하는 방법을 정의하는 람다 표현식이다.

9.3.2 람다를 사용하는 메서드의 동작에 집중해라

람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다. 그러려면 세부 구현을 포함하는 람다표현식을 공개하지 말아야한다.
람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공개하지 않으면서도 람다 표현식을 검증할 수 있다.

public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
  return points.stream().map(p -> new Point(p.getX() + x, p.getY())).collect(toList());
}

//TEST 코드
@Test
public void testComparingAllPointsRigtTwoPoints() throws Exception {
  List<Point> points = Arrays.asList(new Point(5, 5), new Point(10, 5));
  List<Point> excpectedPoints = Arrays.asList(new Point(15, 5), new Point(20, 5));
  
  List<Point> newPoinsts = Point.moveAllPointsRightBy(points, 10);
  assertEquals(expectedPoint, newPoints);
 }

9.3.3 복잡한 람다를 개별 메서드로 분할하기

람다표현식을 메서드 참조로 바꾸는 것, 그러면 일반 메서드를 테스트 하듯이 람다표현식도 테스트가 가능하다.

9.3.4 고차원 함수 테스팅

함수를 인수로 받거나 다른 함수를 반환하는 메서드를 고차원 함수라고 한다.(* 19장에서 다룰 예정)
메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트할 수 있다.

9.4 디버깅

9.4.1 스택 트레이스 확인

람다와 스택 트레이스

람다 표현식은 이름이 없기 때문에 복잡한 스택 트레이스가 생성된다.
메서드 참조를 사용해도 스택 트레이스에는 메서드 명이 나타나지 않는다.
메서드 참조를 사용하는 클래스와 같은 곳에 선언되어있는 메서드를 참조할 때는 메서드 참조 이름이 스택 트레이스에 나타난다.

즉, 람다표현식과 관련한 스택 트레이스는 이해하기 어려울 수 있다는 점을 염두해 두자.

9.4.2 정보 로깅

numbers.stream()
  .map(x -> x + 17)
  .filter(x -> x % 2 == 0)
  .limit(3)
  .forEach(System.out::println);

위 연산에서 forEach를 호출하는 순간 전체 스트림이 소비된다.

전체 스트림이 소비된다? 스트림 파이프라인의 다른 연산들도 한꺼번에 실행되어 중간 연산들과 최종 연산들이 함께 동작한다는 것을 의미, 이것은 스트림의 지연 평가(lazy evaluation)와 관련이 있다.

파이프라인에 적용된 각각의 연산(map, filter, limt)이 어떤 결과를 도출하는지 확인하려면 peek이라는 스트림 연산을 활용할 수 있다.
peek은 스트림의 각 요소를 소비한 것처럼 동작하지만, forEach처럼 실제로 소비하지는 않는다.

numbers.stream()
  .peek(x -> System.out.println(x))
  .map(x -> x + 17)
  .peek(x -> System.out.println(x))
  .filter(x -> x % 2 == 0)
  .peek(x -> System.out.println(x))
  .limit(3)
  .peek(x -> System.out.println(x))
  .collect(toList());
profile
기록용

0개의 댓글

Powered by GraphCDN, the GraphQL CDN