9.1 가독성과 유연성을 개선하는 리팩토링
람다 표현식은 익명 클래스보다 코드를 더 간결하게 만들어준다. ⇒ 3장에서 확인해봤었다. 게다가, 재활용이 가능하므로 더 큰 유언성을 갖츨 수 있다.
9.1.1 코드 가독성 개선
- 익명 클래스를 람다 표현식으로 리팩토링하기
- 람다 표현식을 메서드 참조로 리팩토링하기
- 명령형 데이터 처리를 스트림으로 리팩토링하기
9.1.2 익명 클래스를 람다 표현식으로 리팩토링하기
주의할 점이 있는데, 모든 익명 클래스를 람다 표현식으로 바꿀 수 없다.
- 익명 클래스의 this와 super는 람다 표현식에서는 다른 의미를 갖는다. 익명 클래스에서 this는 익명 클래스 자신을 가르키지만 람다 표현식에서 this는 람다 표현식을 포함하는 캘르스를 가르킨다.
- 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 없다(shadow 변수). 하지만 람다 표현식으로는 변수를 가릴 수 없다.
int a = 10; Runnable r1 = () -> { int a = 2; // 컴파일 에러 System.out.println(a); }; Runnable r2 = new Runnable() { public void run() { int a = 2; // 동작 완료 System.out.println(a); } };
- 익명 클래스를 람다 표현식으로 바꾸면 컨텍스트 오버로딩에 따른 모호함이 발생할 수 있다. 람다 표현식의 형식은 컨텍스트에 따라 달라지기 때문이다. 다만, 이는 명시적으로 형식을 타입캐스팅하면 해결가능하다.
9.1.3 람다 표현식을 메서드 참조로 리팩토링하기
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 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 class Dish{
...
public CaloricLevel getCaloricLevel() {
if (this.getCalories() <= 400) return CaloricLevel.DIET;
else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
}
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::igetCaloricLevel));
Collectors API
의 정적 헬퍼 메서드와 메서드 참조를 통해서 더 가독성 좋게 개선한 모습이다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));
int totalCalories = menu.stream().map(Dish::getCalories)
.reduce(0, (c1, c2) -> c1 + c2);
totalCalories = menu. stream().collect(summingInt(Dish::getCalories));
위 코드 역시 Collectors API
의 정적 헬퍼 메서드를 통해서 개선한 모습이다.
9.1.4 명령형 데이터 처리를 스트림으로 리팩토링하기
일반적인 반복문을 통한 컬렉터 처리는 모두 스트림으로 대체하는게 이론적으로 옳다. 하지만, 그 작업이 쉬운것은 아니다. 반복문이나 분기문을 다 분석하여 로직을 완전히 파악해야 리팩토링이 가능하다.
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu) {
if(dish.getCalories() > 300) {
dishNames.add(dish.getName());
}
}
menu.parallelStream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.Collect(toList());
이렇게 쉽게 병렬화하고, 가독성이 좋게 바꿀 수 있다.
9.1.5 코드 유연성 개성
람다 표현식을 사용하면 동작 파라미터화가 쉽게 구현가느하므로 변화하는 요구사항에 대응하기 쉽다.
함수형 인터페이스 적용
람다 표현식을 사용하려면 함수형 인터페이스가 필요하다.
조건부 연기 실행
특정 조건에서만 코드가 evaluate 되도록 할 수 있다. 특히, 클라이언트에서 객체상태를 자주 확인하고, 이후 객체 일부 메서드를 호출하느 상황에서 이런 메서드를 구현하는 것이 좋다.
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
public void log(Level lvel, Supplier<String> msgSupplier) {
if (logger.isLoggable(level)) {
log(level, msgSupplier.get());
}
}
log 메서드를 사용하면 logger의 상대가 loggable 하지 않을 때에는 메시지에 해당하는 String이 생성되지 않는다.
실행 어라운드 패턴
매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 준비, 종료 과정을 처리하는 로직을 재사용 함으로써 이를 람다로 변환할 수 있다. 3장에서 다룬 내용이니 코드는 생략한다.
9.2 람다로 객체지향 디자인 패턴 리팩토링하기
아래 다섯 가지 패턴을 리팩토링해보자.
- Strategy Pattern
- Template Method Pattern
- Observer Pattern
- Chain Of Responsibiliity Pattern
- Factory Pattern
9.2.1 전략 패턴
![](https://blog.kakaocdn.net/dn/HAuuG/btslQma9Lq8/kxVmOTmVeIgxzyjoeJneOk/img.png)
- 알고리즘을 나타내는 인터페이스(Strategy 인터페이스)
- 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스(ConcreteStrategyA, ConcreteStategyB)
- 전략 객체를 사용하는 한개 이상의 인터페이스
코드 추가 요망
보기와 같이 전략패턴을 사용하기 위한 인터페이스, 클래스 선언 때문에 많은 코드를 작성해야한다. 이를 람다로 해결할 수 있다.
람다 표현식 사용
람다 표현식을 이용하면 전략 패턴에서 발생하는 자잘한 코드들을 제거할 수 있다. 즉, 람다 표현식으로 전략패턴을 대신할 수 있다.
코드 추가 요망
9.2.2 템플릿 메서드 패턴
알고리즘의 개요를 제시한 다음, 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야할 때 템플릿 메서드 패턴을 사용한다. ‘이 알고리즘을 사용하고 싶은데, 그대로는 안 되겠고 조금 고쳐야하는’ 상황에 적합하다.
코드 추가 요망
람다 표현식 사용
OnlineBanking 클래스를 상속받지 않고 직접 람다 표현식을 전달해서 다양한 동작을 추가할 수 있다. 그리고 람다 표현식을 사용해서 자잘한 코드 역시 제거할 수 있다.
코드 추가 요망
9.2.3 옵저버
![](https://blog.kakaocdn.net/dn/Q7bZj/btslSpZguDt/G1s2fxa07BJr5g19NXPU6k/img.png)
어떤 이벤트가 발생했을 때 객체가 다른 객체 리스트에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용한다.
코드 추가 요망
람다 표현식 사용하기
위 코드에서 세개의 옵저버를 명시적으로 인스턴스화하지 않고 람다 표현식을 직접 전달해서 실행할 동작을 지정할 수 있다.
코드 추가 요망
9.2.4 의무 체인
![](https://blog.kakaocdn.net/dn/U6LGv/btslOCL8y5i/6hKvNTdYCAgkHkLbvARIkK/img.png)
작업 처리 객체의 체인을 만들 때는 의무 체인 패턴을 사용한다. 일반적으로 다음으로 처리할 객체 정보를 유지하는 필드를 포함하는 작업 처리 추상 클래스로 의무 체인 패턴을 구성한다.
코드 추가 요망
위 코드를 보면 여기서 템플릿 메서드 패턴도 적용한 것을 알 수 있다.
코드 추가 요망
이렇게 작업 체인을 계속 호출할 수 있다.
람다 표현식 사용
이 패턴을 보면 함수 체인과 비슷하다는 것을 알 수 있다. 아래와 같이 동작하던 것을 기억해보자.
코드 추가 요망
9.2.5 팩토리
인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴을 사용한다.
코드 추가 요망
위 코드의 장점은 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순하게 상품을 생성할 수 있다는 것이다.
람다 표현식 사용
코드 추가 요망
이렇게 상품명을 생성자로 연결하는 Map
을 만들어서 코드를 재구현할 수 있다.
9.3 람다 테스팅
9.3.1 보이는 람다 표현식의 동작 테스팅
람다는 익명이므로 테스트 코드 이름을 호출할 수 없다. 따라서 필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직을 테스트할 수 있다.
코드 추가 요망
이렇게도 할 수 있다는 것이지, 이렇게 테스트하는 방법은 별로 좋지 않다.
9.3.2 람다를 사용하는 메서드의 동작을 집중하라
람다의 목표는 동해진 동작을 다른 메서드에 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다. 따라서 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다. 람다 표현식을 포함하는 메서드 자체를 테스트함으로써 람다를 공개하지 않으면서 람다 표현식을 검증할 수 있다.
코드 추가 요망
9.3.3 복잡한 람다를 개별 메서드로 분할하기
여기서 개별 메서드는 메서드 참조로 바꾸어라는 말이다.
9.3.4 고차원 함수 테스팅
함수를 인수로 받거나 다른 함수를 반환하는 메서드를 고차원 함수라 하는데, 여기서 테스팅이 더 어렵다.
고는 하는데, 그냥 9.3.1처럼 람다를 함수형 인터페이스의 인스턴스로 간주하면 된다.
코드 추가 요망
9.4 디버깅
일반적으로 코드를 디버깅할 때 개발자는 다음 두 가지를 가장 먼저 확인한다. 하지만, 람다 표현식과 스트림은 기존 디버깅 방법을 무력화한다. 왜 그런지 알아보자.
- 스택 트레이스
- 로깅
9.4.1 스택 트레이스 확인
안타깝지만 람다 표현식은 이름이 없기 때문에 조금 복잡한 스택 트레이스가 생성된다…
코드 추가 요망
람다 표현식은 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어낸 것이다. lambda$main$0
는 다소 생소한 이름이다. 특히 하나의 클래스에 여러 람다 표현식이 있을 때는 더 골치가 아프다.
메서드 참조를 사용한다해도 스택 트레이스에는 메서드명이 나타나지 않는다. 다만, 참조하는 메서드가 같은 클래스에 선언되어 있다면 메서드 참조 이름이 스택 트레이스에 나타나난다.
코드 추가 요망
코드 추가 요망
이런 문제는 자바 컴파일러가 개선해야할 문제다.. 우리가 할 수 있는 건 로깅을 열심히 하는 것 뿐이다.
9.4.2 정보 로깅
코드 추가 요망
안타깝게도 로깅을 하기 위해서 forEach
를 사용하는 순간 전체 스트림이 소비되어버린다. 왜냐하면 최종 연산이기 때문이다.
이럴 때에는 peek
이라는 스트림 연산을 활용할 수 있다. peek
은 스트림의 각 요소를 소비한 것 처럼 동작을 실행한다. 하지만, forEach
처럼 실제 스트림 요소를 소비하지는 않는다.
코드 추가 요망
![](https://blog.kakaocdn.net/dn/WD68b/btslQnujN1K/iZzAffezczZQ5u5Y9KtZt1/img.png)
9.5 마치며
- 람다 표현식으로 가독성이 좋고 더 유연한 코드를 만들 수 있다.
- 익명 클래스는 람다 표현식으로 바꾸는 것이 좋다. 하지만 이때 this, 변수 shadow 등 미묘하게 의미상 다른 내용이 있음에 주의하자.
- 메서드 참조로 람다 표현식보다 더 가독성이 좋은 코드를 구현할 수 있다.
- 반복적으로 컬렉션을 처리하는 루틴은 스트림 API로 대체할 수 있을지 고려하는 것이 좋다.
- 람다 표현식을 전략, 템플릿 메서드, 옵저버, 의무 체인, 팩토리 등의 객체지향 디자인 패턴에서 발생하는 불필요한 코드를 제거할 수 있다.
- 람다 표현식도 단위 테스트를 수행할 수 있다. 하지만 람다 표현식 자체를 테스트하는 것 보다는 람다 표현식이 사용되는 메서드의 동작을 테스트하는 것이 바람직하다.
- 복잡한 람다 표현식은 일반 메서드로 재구현할 수 있다.
- 람다 표현식을 사용하면 스택 트레이스를 이해하기 어려워진다.
- 스트림 파이프라인에서 요소를 처리할 때
peek
메서드로 중간값을 확인할 수 있다.
Uploaded by N2T
(23.05.31 23:41)에 작성된 글 입니다.