자바/모던 자바 인 액션

[모던 자바 인 액션] 9장. 리팩터링, 테스팅, 디버깅

Rudtjs 2022. 11. 18. 13:02

기존 코드를 가지고 새로운 프로젝트를 시작하는 상황을 가졌을 때 코드를 깔끔하게 만들고 다른 사람이 쉽게 이해할 수 있도록 만들려면 어떻게 해야 될까? 즉, 람다 표현식이나 스트림을 이용해 기존 코드들을 리팩터링해야 한다.

 

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

코드 가독성이란 어떤 코드든 다른 사람이 볼때 쉽게 이해할 수 있음을 의미한다.

코드의 가독성을 높이려면 코드의 문서화를 잘하고, 표준 규칙을 지키려는 노력을 기울여야 한다.

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

 

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

하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩터링 할 수 있다.

Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello");
        }
    };
    
Runnable r2 = () -> System.out.println("Hello");

하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.

  • 익명 클래스에서 사용한 this, super는 람다 표현식에서 다른 의미를 갖는다.
  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다. 하지만 다음 코드에서 보여주는 것처럼 람다 표현식으로 변수를 가릴 수 없다.
int a = 10;

Runnable r1 = new Runnable() {
    int a = 20;    
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

Runnable r2 = () -> {
    int a = 20; // 컴파일 에러
    System.out.println("Hello");
};

 

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

람다 표현식 대신 메서드 참조를 이용하면 메서드 참조의 메서드명으로 코드의 의도를 명확하게 알릴 수 있다.

Map<CaloricLevel, List<Dish>> dishByCaloricLevel = menu.stream()
                .collect(groupingBy(dish -> {
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT;
                }));


public CaloricLevel getCaloricLevel() {
    if (this.calories <= 400) return CaloricLevel.DIET;
    else if (this.calories <= 700) return CaloricLevel.NORMAL;
    else return CaloricLevel.FAT;
}


Map<CaloricLevel, List<Dish>> dishByCaloricLevel = menu.stream()
                .collect(groupingBy(Dish::getCaloricLevel));

 

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

스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여준다.

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

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

 

9.1.4 코드 유연성 개선

함수형 인터페이스를 적용하여 코드를 개선해 보자.

 

함수형 인터페이스 적용

  • 조건부 연기 실행
  • 실행 어라운드 패턴

조건부 연기 실행

  if (logger.isLoggable(Log.FINER)) {
    logger.finer("Problem: " + generateDiagnostic());
  }
 
  • 위 코드는 다음과 같은 사항에 문제가 있다.
    • logger의 상태가 isLoggable이라는 메서드에 의해 클라이언트로 노출된다.
    • 메시지를 로깅할 때마다 logger 객체의 상태를 매번 확인해야 한다.
  • 다음처럼 메시지를 로깅하기 전에 logger 객체가 적절한 수준으로 설정되었는지 내부적으로 확인하는 log 메서드를 사용하는 것이 바람직하다.
  logger.log(Level.FINER, "Problem: " + generateDiagnostic());

불필요한 if문을 제거하였지만 여전히 항상 로깅 메시지를 보게 된다.

Java8 에서부터 제공해 주는 Supplier를 이용하여 제거해 보자.

public void log(Level level, Supplier<String> msgSupplier);

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

public vodid log(Level level, Supplier<String> msgSupplier) {
  if (logger.isLoggable(level)) {
    log(level, msgSupplier.get()); //람다 실행
  }
}
  • log 메서드는 조건에 맞는 경우에만 실행되고 인수로 넘겨진 람다를 내부적으로 실행할 수 있게 변경되었다.
  • 이러한 방법으로 객체 상태를 자주 확인하는 상황이나 객체의 일부 메서드를 확인한 다음에 메서드를 호출하도록 새로운 메서드를 구현하는 것이 좋다.
  • 코드의 가독성이 좋아질 뿐만 아니라 캡슐화(객체 상태가 클라이언트로 노출되지 않는다)가 강화된다.

실행 어라운드

String oneLine = processFile((BufferedReader b) -> b.readLine()); // 람다 전달
String twoLine = processFile((BufferedReader b) -> b.readLine() + b.readLine()); // 다른 람다 전달

public static String processLine(BufferedReaderProcessor p) throws IOException {
  try (BufferdReader br = new BufferedReader(new FileReader("file.txt"))) {
    return p.process(br); // 인수로 전달된 BufferedReaderProcessor 실행
  }
}

public interface BufferedReaderProcessor {
  String process(BufferedReader b) throws IOException;
}
  • 람다로 BufferedReader 객체의 동작을 결정할 수 있는 것은 함수형 인터페이스 BufferedReaderProcessor 덕분이다.

 

9.3 람다 테스팅

 

일반적으로 프로그램이 의도대로 동작하는지 확인할 수 있는 방법은 단위 테스트를 작성하는 것이다.

 

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

public static final Comparator<Point> compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY);
Point.compareByXAndThenY.compare(p1, p2);

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

  • 람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다.
  • 람다 표현식을 사용하는 메서드의 동작을 테스트 함으로써 람다 표현식을 검증할 수 있다.

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

  • 복잡한 로직이 포함된 람다를 구현하게 된다면 로직을 분리하거나 메서드 레퍼런스를 활용하도록 하자.

고차원 함수 테스팅

  • 함수를 인수로 받거나 다른 함수를 반환하는 메서드는 사용하기도, 테스트하기도 어렵다.
  • 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트 할 수 있다.
  • 테스트해야 하는 함수가 다른 함수를 반환한다면 앞서 Comparator와 비슷하게 함수형 인터페이스의 인스턴스로 간주하고 테스트할 수 있다.

9.4 디버깅

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

  • 스택 트레이스
  • 로깅

프로그램이 멈췄다면 프로그램이 어디서 멈췄는지 메서드 호출 리스트를 통해 문제를 파악하자.