[Ch9] 리팩터링, 테스팅, 디버깅

이번 챕터에서는 기존 코드를 람다 표현식을 이용해 가독성과 유연성을 높이는 방법을 알아본다. 또한, 람다로 전략 패턴, 템플릿 메서드 패턴 등, 자바 개발자가 익히 아는 패턴을 어떻게 쉽게 구현할 수 있는 지 알아본다. 람다를 테스팅 하는 방법은 조금 복잡하다. 그 방법을 알아보고 디버깅 방법까지 알아본다.

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

대부분의 익명클래스를 람다로 변경해서 코드를 좀 더 간결하게 만들 수 있다. (익명클래스에 중점을 두자 :white_check_mark:)

코드 가독성이 좋다는 것은 ‘어떤 코드를 다른 사람도 쉽게 이해할 수 있음’을 의미한다. 코드 가독성을 높이려면 코드의 문서화를 잘하고, 표준 코딩 규칙을 준수하는 등의 노력을 기울여야 한다.

자바8의 새 기능을 이용해 코드 가독성을 높일 수 있다.

  • 익명 클래스를 람다 표현식으로 리팩터링
  • 람다 표현식을 메서드 참조로 리팩터링
  • 명령형 데이터 처리를 스트림으로 리팩터링

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

익명클래스를 쓰면 코드가 장황해지는데, 람다를 쓰면 깔끔하게 코드를 작성할 수 있다.

하지만 주의해야할 점이 있다.

  • 익명 클래스에서 thissuper 는 라다 표현식에서 다른 의미를 갖는다.
    • 익명 클래스에서 this 는 익명클래스 자신
    • 람다에서 this 는 람다를 감싸는 클래스를 가리킴
  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다. (shadow variable) :left_right_arrow: 람다는 shadow variable 을 가질 수 없다.
    int a = 10;
    Runnable r1 = () -> {
      int a = 2; // 컴파일 에러
    }
    
    Runnable r2 = new Runnable() {
      public void run() {
        int a = 2; // 정상
      }
    }
    
  • 오버로딩에 따른 ambiguity
    • 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면, 람다의 형식 context 에 따라 달라진다.
    • 예를 들어, 같은 시그니처를 갖는 함수형 인터페이스가 1개 이상일 경우, 형식을 지정해주지 않는 한 람다 표현식으로는 어떤 함수를 호출할지 모르므로 문제가 생긴다.
      • doSomething(() -> System.out.println("Danger danger!!"));
        

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

메서드 참조는 메서드명으로 코드의 의도를 명확하게 알릴 수 있다.

복잡한 람다 표현식일 경우, 메서드로 추출한 뒤, 해당 메서드를 참도하도록 바꾸는 방식이다.

groupingBy, comparing, maxBy 와 같은 정적 헬퍼 메서드를 활용하는 것도 좋다. (자신이 어떤 동작을 수행하는지 메서드 이름으로 설명함)

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

다음은 필터링과 추출을 하는 명령형 코드이다. 이 코드가 필터링과 추출을 하는 코드임을 이해하려면 개발자는 코드 전체를 읽고 이해해야 한다.

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

우리가 지금껏 배웠던 스트림 API로 변형하면 코드가 간결해질 뿐만 아니라 병렬화할 수도 있다.

menu.parallelStream()
    .filter(d -> d.getCalories() > 300)
    .map(Dish::getName)
    .collect(Collectors.toList());

코드 유연성 개선

지금껏 코드의 가독성에 대해 알아봤다. 이번에는 유연성을 개선해보자. 람다 표현식을 이용하면 동작 파라미터화를 쉽게 구현해서, 여러 다양한 동작을 하나의 코드로 표현할 수 있다. 따라서 변화하는 요구사항에 대응할 수 있는 코드를 구현할 수 있다.

함수형 인터페이스 적용

@FunctionalInterface 를 선언해서 함수형 인터페이스를 만들고, conditional deferred execution 과 execute around 패턴으로 람다 표현식을 구현하면 코드를 유연하게 만들 수 있다.

conditional deferred execution (조건부 연기 실행)

if(logger.isLoggable(Log.FINER)) {
  logger.finer("Problem: " + getProblem());
}

위 코드는 다음과 같은 문제가 있다

  • logger의 상태가 isLoggable 이라는 메서드에 의해 클라이언트 코드로 노출된다.
  • 메시지를 로깅할 때마다 if 문으로 매번 체크해야 한다.

따라서 아래와 같이 리팩터링 한다.

  • if 문을 제거하고, log 라는 정적 팩터리 메서드로 상태를 확인한다. (클라이언트 코드로 노출을 감춘다.)
  • 람다를 사용해서 특정 조건에서만 메시지가 생성될 수 있도록 메시지 생성 과정을 defer 한다.
// 인수를 받지 않고 값을 리턴하는 Supplier 함수형 인터페이스를 인수로 받는다.
public void log(Level level, Supplier<String> msgSupplier) {
  if(logger.isLoggable(level)) { // 특정 조건을 만족하면
    log(level, msgSupplier.get()); // Supplier 객체를 실행한다.
  }
}

이렇게 해서 상태를 체크하는 코드를 감춰서 캡슐화를 강화했으며, 클라이언트에서는 Level 상태와, 로깅할 메시지만 넘기면 되서 코드 가독성도 좋아졌다.

execute around (실행 어라운드)

매번 동일한 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면, 이를 람다로 변환할 수 있다. 3장에서 살펴본 예제 중 파일을 read 하는 코드를 보자.

BufferedReader 객체의 동작을 결정할 수 있도록 함수형 인터페이스 BufferedReaderProcessor 를 선언하고 이를 람다로 동작을 파라미터화 하는 것이다.

@FunctionalInterface
public interface BufferedReaderProcessor {
  String process(BufferedReader b) throws IOException;
}

public static String processFile(BufferedReaderProcessor p) throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    return p.process(br); // 동작 파라미터 화
  }
}

String test2 = processFile((BufferedReader br) -> br.readLine() + br.readLine());

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

디자인 패턴에 람다 표현식이 더해지면 색다를 기능을 발휘할 수 있다. 즉, 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있다.

  • strategy (전략)
  • template method (템플릿 메서드)
  • observer (옵저버)
  • chain of responsibility (의무 체인)
  • factory (팩토리)

실습 코드는 GIT 링크 에서 확인 가능하다.


각 디자인 패턴에서 람다를 어떻게 활용할 수 있는지에 대해 설명한다. 나는 예제는 직접 실습하면서 패턴을 익혔다. 그 중 포인트만 정리한다.

  1. 함수형 인터페이스를 선언하자
  2. 해당 인터페이스를 구현한 구현체들을 람다로 파라미터화 해서 넘긴다.
  3. abstract 클래스를 선언하지 않고 표준 함수형 인터페이스를 써서 람다로 구현한다.
    • Consumer<T> (인수를 받고 반환값은 없는 함수)
    • Function<T> (인수화 반환타입이 다른 함수)
    • 등등..
  4. 실행하는 동작이 간단하다면, 굳이 구현체를 만들지 않고 람다 표현식을 직접 전달한
  5. Function<T> 인터페이스의 andThen 메서드로 체이닝을 구현할 수 있다.
  6. 생성자 메서드 참조를 이용해서 팩터리를 만들 수 있다.

람다 테스팅

람다를 테스트 하는 방법은, 단위 테스트를 직접 작성하는 것이다. 람다를 필드에 저장해서 재사용하면서 람다의 로직을 테스트 할 수 있다.

public class Point {
  public final static Comparator<Point> compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY);

  // compareByXAndThenY 을 이용해서 단위 테스트를 수행한다.
}

디버깅

문제가 발생한 코드를 디버깅할 때 다음 두 가지를 먼저 확인해야 한다.

  • 스택 트레이스
  • 로깅

하지만 람다에서 에러가 발생했을 경우 스택 트레이스에 알 수 없는 로그들이 찍힌다. :arrow_forward: 방법이 없다. 이해하기 어려울 수 있다는 점만 기억하자.

스트림의 파이프라인 연산을 디버깅할때, peek 메서드를 이용해서 각 단계에서 어떤 값이 추출되는 지 확인할 수 있다.

List<Integer> result = numbers.stream()
                              .peek(x -> System.out.println("from stream: " + x))
                              .map(x -> x + 17)
                              .peek(x -> System.out.println("after map: " + x))
                              .collect(toList());

댓글남기기