본문 바로가기
그룹 스터디 공부(IT 서적)/모던 자바 인 액션

chapter 9 리펙터링, 테스팅, 디버깅

by hanyugyeong 2023. 7. 30.
반응형
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"); //람다 표현식을 사용한 최신 코드

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

  1. 익명 클래스에서 사용한 this, super는 람다 표현식에서 서로 다른 의미를 가지게 된다.
  2. this는 익명 클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 의미하게 된다.
  3. 익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있다. 하지만 다음 코드에서 보여주는것 처럼 람다 표현식으로는 가릴수 없다.
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