1. Question

람다, 메서드 참조, 스트림을 활용해서 더 가독성이 좋은 코드로 리팩터링 하는 방법은?

2. Answer

일반적으로 코드 가독성이 좋다는 것은 ‘어떤 코드를 다른 사람도 쉽게 이해할 수 있음’을 의미한다. 즉, 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미한다.

‘람다’, ‘메서드 참조’, ‘스트림’을 활용해서 코드 가독성을 개선할 수 있는 방법은 대표적으로 세 가지가 있다.

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

3. Detail

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

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

// 익명 클래스를 사용한 이전 코드
Runnable r1 = new Runnable() {
  public void run() {
    System.out.println("Hello");
  }
}

// 람다 표현식을 사용한 최신 코드
Runnable r2 = () -> System.out.println("Hello");

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

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

  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다(섀도 변수 shadow variable). 하지만 람다 표현식으로는 변수를 가릴 수 없다.

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

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

람다 표현식은 쉽게 전달할 수 있는 짧은 코드다. 하지만 람다 표현식 대신 메서드 참조를 이용하면 가독성을 높일 수 있다. 메서드 참조의 메서드명으로 코드의 의도를 명확하게 알릴 수 있기 때문이다.

List<String> list = Arrays.asList("a", "b", "c");
// 람다 표현식 사용
list.forEach(s -> System.out.println(s));
// 메서드 참조 사용
list.forEach(System.out::println);

comparingmaxBy 같은 정적 헬퍼 메서드를 활용하는 것도 좋다. 이들은 메서드 참조와 조화를 이루도록 설계되었다. 람다 표현식보다는 메서드 참조가 코드의 의도를 더 명확하게 보여준다.

// 람다 표현식 사용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 메서드 참조 사용
inventory.sort(comparing(Apple::getWeight));

sum, maximum 등 자주 사용하는 리듀싱 연산은 메서드 참조와 함께 사용할 수 있는 내장 헬퍼 메서드를 제공한다. 예를 들어 최댓값이나 합계를 계산할 때 람다 표현식과 저수준 리듀싱 연산을 조합하는 것보다 Collectors API를 사용하면 코드의 의도가 더 명확해진다.

// 저수준 리듀싱 연산 사용
int totalCalories = 
  menu.stream()
    .map(Dish::getCalories)
    .reduce(0, (c1, c2) -> c1 + c2);
// 내장 컬렉터 사용
int totalCalories2 = 
  menu.stream().collect(summingInt(Dish::getCalories));

내장 컬렉터를 이용하면 코드 자체로 문제를 더 명확하게 설명할 수 있다. 자신이 어떤 동작을 수행하는지 메서드 이름으로 설명하기 때문이다.

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

Stream API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여주기 때문에, 이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 Stream API로 바꿔야 한다. 스트림은 쇼트서킷게으름이라는 강력한 최적화뿐 아니라, 멀티코어 아키텍처를 활용할 수 있는 지름길을 제공한다.

예를 들어 다음 명령형 코드는 두 가지 패턴(필터링과 추출)으로 엉킨 코드다. 이 코드를 접한 프로그래머는 전체 구현을 자세히 살펴본 이후에야 전체 코드의 의도를 이해할 수 있다.

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

Stream API를 이용하면 문제를 더 직접적으로 기술할 수 있을 뿐 아니라 쉽게 병렬화할 수 있다.

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

4. Reference

  • “모던 자바 인 액션” (저자: 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트)

태그:

카테고리:

업데이트:

댓글남기기