그룹 스터디 공부(IT 서적)/모던 자바 인 액션
chapter 9 리펙터링, 테스팅, 디버깅
hanyugyeong
2023. 7. 30. 14:16
반응형
SMALL
- 람다 표현식으로 코드 리팩터링하기
- 람다 표현식이 객체지향 설계 패턴에 미치는 영향
- 람다 표현식 테스팅
- 람다 표현식과 스트림 API 사용 코드 디버깅
9.1 가독성과 유연성을 개선하는 리팩터링
- 람다, 메서드 참조, 스트림 등의 기능을 이용해 가독성을 높이고 유연한 코드로 리팩토링하는것을 설명.
9.1.1 코드 가독성 개선
- 익명 클래스를 람다 표현식으로 리팩토링
- 람다 표현식을 메서드 참조로 리팩토링
- 명령형 데이터 처리를 스트림으로 리팩토링
9.1.2 익명 클래스를 람다 표현식으로 리팩터링하기
- 하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩토링 할 수 있다.
Runnable r1 = new Runnable() { // 익명 클래스를 사용한 이전 코드
@Override
public void run() {
System.out.println("Hello");
}
};
Runnable r2 = () -> System.out.println("Hello"); //람다 표현식을 사용한 최신 코드
모든 익명 클래스를 람다 표현식으로 변환할 수 있는것은 아니다.
- 익명 클래스에서 사용한 this, super는 람다 표현식에서 서로 다른 의미를 가지게 된다.
- this는 익명 클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 의미하게 된다.
- 익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있다. 하지만 다음 코드에서 보여주는것 처럼 람다 표현식으로는 가릴수 없다.
int a = 10;
int a = 10;
Runnable r1 = () -> {
int a = 2; // 컴파일 에러
System.out.println(a);
};
int a = 10;
Runnable r2 = new Runnable() {
@Override
public void run() {
int a = 2; // 모든 것이 잘 작동한다.
System.out.println(a);
}
};
- 익명 클래스를 람다 표현식으로 변경하면 콘텍스트 오버로딩에 따른 모호함이 초래된다.
- 익명 클래스는 인스턴스화 할때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달라지기 때문이다.
interface Task {
public void execute();
}
public static void doSomeThing(Runnable r) {
r.run();
}
public static void doSomeThing(Task t) {
t.execute();
}
doSomeThing(new Task() {
public void execute() {
System.out.println("do");
}
});
doSomeThing(() -> System.out.println("do")); // 어떤 것이 실행되어야 하는지 알수 없다.
doSomeThing((Task)() -> System.out.println("do"));
- 익명 클래스를 람다 표현식으로 변경하는 경우 Runnable 과 Task 중 어떤 클래스를 사용해야 하는지 알수 없으므로 모호함이 발생한다.
- 명시적 형변환을 이용해서 처리할 수 있지만 일반적으로 IDE에서 이러한 모호함을 처리해준다.
9.1.3 람다 표현식을 메서드 참조로 리펙토링 하기
- 람다 표현식을 메소드 참조로 변환하면 가독성을 높아지고 코드의 의도를 명확하게 표현할 수 있다.
enum CaloricLevel { DIET, NORMAL, FAT };
public static void main(String[] args) {
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;
}));
System.out.println(dishByCaloricLevel);
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));
- comparingBy와 mayBy 같은 정적 헬퍼 메서드를 활용하는것도 좋다.
inventory.sort((Apple a1, Apple a2) -> a1. getWeight().compareTo(a2.getWeight())); //비교 구현에 신경 써야 한다
inventory.sort(comparing(Apple::getWeight)) // 코드 문제 자체를 설명한다.
9.1.4 명령형 데이터 처리를 스트림으로 리팩토링하기
List<String> dishNames = new ArrayList<>();
for (Dish dish : menu) {
if (dish.getCalories() > 300) {
dishNames.add(dish.getName());
}
}
스트림 API를 이용하면 문제를 더 직접적으로 기술할 수 있을 뿐 아니라 쉽게 병렬화할 수 있다.
menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
9.1.5 코드 유연성 개선
- 함수형 인터페이스 적용
- 조건부 연기 실행 패턴
- 실행 어라운드 패턴
- 조건부 연기 실행
- 실제 작업을 처리하는 코드 내부에 제어 흐름문이 복잡하게 얽힌 코드를 흔히 볼 수 있다.
if (logger.isLoggable(Log.FINER)) {
logger.finer("Problem: " + generateDiagnostic());
}
- 위 코드는 다음과 같은 사항에 문제가 있다.
- logger의 상태가 isLoggable이라는 메서드에 의해 클라이언트로 노출된다.
- 메시지를 로깅할 때마다 logger 객체의 상태를 매번 확인해야 할까 ?
- 다음처럼 메시지를 로깅하기 전에 logger 객체가 적절한 수준으로 설정되었는지 내부적으로 확인하는 log 메서드를 사용하는것이 바람직하다.
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
- 불필요한 if 문은 사라졌지만 여전히 위 코드는 문제가 있다.
- 조건에 맞지 않더라도 항상 로깅 메시지를 평가하게 된다.
- 람다를 이용하여 이러한 문제를 해결하자. 특정 조건에서만 메시지가 생성될 수 있도록 메시지 생성 과정을 연기해야 한다.
- Java 8에서는 이와 같은 문제를 해결할 수 있도록 Supplier를 제공하고 있다.
다음은 새로 추가된 log 매서드의 시그니처다.
public void log(Level level, Supplier<String> msgSupplier);
다음처럼 log 매서드를 호출할 수 있다.
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
log 매서드의 내부 구현 코드
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.2 람다로 객체지향 디자인 패턴 리팩터링하기
- 디자인 패턴에 람다 표현식이 더해지면서 색다른 기능을 발휘 하게 되었다.
- 즉, 람다를 이용하여 이전에 디자인 패턴으로 해결하던 문제를 더 쉽게 간단하게 해결할 수 있다.
- 람다 표현식으로 기존의 많은 객체지향 디자인 패턴을 제거하거나 간결하게 재 구현할 수 있다.
- 이절에선 다섯 가지 패턴을 살펴본다.
- 전략(Strategy)
- 템플릿 메서드(Template Method)
- 옵저버(Observer)
- 의무 체인(Chain of Responsibility)
- 팩토리(Factory)
9.2.1 전략
- 전략 패턴은 한 유형의 알고리즘을 보유한 상태에서 런타임시 적절한 알고리즘을 선택하는 기법이다.
- 전략 패턴은 크게 클라이언트 -> 전략 -> A, B (런타임시 선택) 으로 구성된다.
- 알고리즘을 나타내는 인터페이스 (Strategy 인터페이스)
- 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스 구현 (StrategyA, StrategyB)
- 전략 객체를 사용하는 한 개 이상의 클라이언트
- 예시로 오직 소문자 또는 숫자로 이루어져야하는 등 텍스트 입력이 다양한 조건에 맞도록 포맷되어있는지 검증하는 로직이 존재한다고 가정하자.
@FunctionalInterface
//String 문자열을 검증하는 인터페이스
public interface ValidationStrategy {
boolean execute(String s);
}
//인터페이스를 구현하는 클래스를 하나 이상 정의
public class IsAllLowerCase implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class isNumeric implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("\d+");
}
}
// 지금까지 구현한 클래스를 다양한 검증 전략으로 활용가능
public class Validator {
private final ValidationStrategy validationStrategy;
public Validator(ValidationStrategy validationStrategy) {
this.validationStrategy = validationStrategy;
}
public boolean validate(String s) {
return validationStrategy.execute(s);
}
}
Validator numericValidator = new Validator(new isNumeric());
numericValidator.validate("aaa"); //false 반환
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
lowerCaseValidator.validate("bbbb"); //true 반환
람다 표현식 사용
Validator lowerCaseValidator2 = new Validator((String s) -> s.matches("[a-z]+"));
lowerCaseValidator2.validate("bbbb");
Validator numericValidator2 = new Validator((String s) -> s.matches("\d+"));
numericValidator2.validate("1234");
- ValidationStreategy는 함수형 인터페이스이며 Predicate과 같은 함수 디스크럼터를 갖고 있음을 파악했을것이다.
- 따라서 전략을 구현하는 새로운 클래스를 만드는것보다 람다 표현식을 전달하여 간결하게 나타낼 수 있다.
- 위 코드에서 확인할 수 있듯이 람다 표현식을 전달하면 전략 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.
- 추가적으로 람다 표현식은 전략을 캡슐화 하는 효과도 존재한다.
9.2.2 템플릿 메서드
사용자가 고객 ID를 애플리케이션에 입력하면 은행 데이터베이스에서 고객 정보를 가져오고 고객이 원하는 서비스를 제공할 수 있다. 예를 들어 고객 계좌에 보너스를 입금한다고 가정하자. 은행마다 다양한 온라인 뱅킹 애플리케이션을 사용하며 동작 방법도 다르다.
abstract class OnlineBanking {
public void processCustomer(int id) {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappay(Customer c);
}
람다 표현식 사용
public void processCustomer(int id, Cusumer<Customer> makeCustomerHappy) {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> print("hello" + c.getName()));
9.2.3 옵저버
- 어떤 이벤트가 발생했을 때 한 객체(Subject)가 다른 객체 리스트(Observer)에 자동으로 알림을 보내야 하는 패턴에서 옵저버 디자인 패턴을 사용한다.
- notifyObserver() -> notify() <- ObserverA, ObserverB
//다양한 옵저버를 그룹화할 Observer 인터페이스가 필요하다.
//Observer 인터페이스는 새로운 트윗이 있을 때 주제가 호출할 수 있도록 notify라고 하는 하나의 매서드를 제공한다.
interface Observer {
void notify(String tweet);
}
//다양한 키워드에 다른 동작을 수행할 수 있는 여러 옵저버를 정의할 수 있다.
public class NyTimes implements NotiObserver {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("monety")) {
System.out.println("Breaking news in NY ! " + tweet);
}
}
}
public class Guardian implements NotiObserver {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more new from London .. " + tweet);
}
}
}
public class LeMonde implements NotiObserver {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
// 주제도 구현해야 한다. 다음은 Subject 인터페이스의 정의다
public interface Subject {
void registerObserver(NotiObserver o);
void notifyObservers(String tweet);
}
// 주제는 registerObserver 메서드로 새로운 옵저버를 등록한 다음에 notifyObservers 메서드로 트윗의 옵저버에 이를 알린다.
public class Feed implements Subject {
private final List<NotiObserver> observers = new ArrayList<>();
@Override
public void registerObserver(NotiObserver o) {
observers.add(o);
}
@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
//Feed는 트윗을 받았을 때 알림을 보낼 옵저버 리스트를 유지한다. 이제 주제와 옵저버를 연결하는 데모 애플리케이션을 만들 수 있다.
Feed f = new Feed();
f.registerObserver(new NyTimes());
f.registerObserver(new LeMonde());
f.registerObserver(new Guardian());
f.notifyObservers("The Queen ...")
람다 표현식 사용
람다는 불필요한 감싸는 코드 제거 전문가다.
Feed feed = new Feed();
feed.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY ! " + 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 = hadleWork(input);
if (successor != null) {
return successor.handle(r);
}
return r;
}
abstract protected T hadleWork(T input);
}
- 코드를 자세히 살펴보면 템플릿 메서드 패턴이 사용되었음을 알 수 있다.
- handle 메서드는 일부 작업을 어떻게 처리할지 전체적으로 서술한다.
- ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업 처리 객체를 만들수 있다.
public class HandleTextProcessing extends ProcessingObject<String> {
@Override
protected String hadleWork(String input) {
return "From Raoul, Mario and Alan : " + input;
}
}
public class SpellCheckProcessing extends ProcessingObject<String> {
@Override
protected String hadleWork(String input) {
return input.replaceAll("labda", "lambda");
}
}
ProcessingObject<String> p1 = new HandleTextProcessing();
ProcessingObject<String> p2 = new SpellCheckProcessing();
p1.setSuccessor(p2);
p1.handle("Aren't ladbas really sexy?");
From Raoul, Mario and Alan: Aren't lambdas really sexy?!!
- 람다 표현식 사용
- 이러한 패턴은 함수 체인과 비슷하다. 람다 표현식을 조합하는 방식으로는 기본적으로 compose, andThen이 있다.
- andThen메서드로 이들 함수를 조합 해 체인을 만들어 보자.
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan : " + text;
UnaryOperator<String> spellCheckProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckProcessing);
pipeline.apply("Aren't ladbas really sexy?");
From Raoul, Mario and Alan: Aren't lambdas really sexy?!!
9.2.5 팩토리
- 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들기 위해 팩토리 디자인 패턴을 사용한다.
public class ProductFactory {
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("");
}
}
}
Product p = ProductFactory.createProduct("loan");
- 람다 표현식 사용
- 생성자도 메서드 참조 처럼 접근이 가능하다.
Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();
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);
}
public static Product createProduct(String name) {
Supplier<Product> p = map.get(name);
if (p != null) {
return p.get();
}
...
}
- 팩토리 패턴이 수행하던 기능을 Java8의 새로운 기능으로 깔끔히 정리 했다.
- 하지만 파라미터가 많아질 경우 코드의 복잡도가 올라가니 적절한 상황에서 사용하도록 하자.
9.3 람다 테스팅
- 일반적으로 프로그램이 의도대로 동작하는지 확인할 수 있는 방법은 단위 테스트를 작성하는것이다.
9.3.1 보이는 람다 표현식의 동작 테스팅
- 람다의 동작을 테스트 하기 위해 메서드를 정의하는 것 처럼 람다를 정의하여 테스트 가능하다.
public static final Comparator<Point> compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY);
Point.compareByXAndThenY.compare(p1, p2);
9.3.2 람다를 사용하는 메서드의 동작에 집중
- 람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화 하는것이다.
- 람다 표현식을 사용하는 메서드의 동작을 테스트 함으로서 람다 표현식을 검증 할 수 있다.
9.3.3 복잡한 람다를 개별 메서드로 분할
- 복잡한 로직이 포함된 람다를 구현하게 된다면 로직을 분리 하거나 메서드 레퍼런스를 활용하도록 하자.
9.3.4 고차원 함수 테스팅
- 함수를 인수로 받거나 다른 함수를 반환하는 메서드는 사용하기도, 테스트하기도 어렵다.
- 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작을 테스트 할 수 있다.
- 테스트해야하는 함수가 다른 함수를 반환한다면 앞서 Comparator와 비슷하게 함수형 인터페이스의 인스턴스로 간주하고 테스트 할 수 있다.
9.4 디버깅
- 문제가 발생하느 코드를 디버깅할때는 다음 두가지를 가장 먼저 확인해야한다.
- 스택 트레이스
- 로깅
- 로그를 찍어야 하는 상황이라면 스트림을 소비하지 않는 peek을 사용하도록 하자.
List<Integer> result = Stream.of(2, 3, 4, 5)
.peek(x -> System.out.println("taking from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
System.out.println(result);
[20, 22]
반응형
LIST