Study/Modern-Java-In-Action

Modern Java Ch09. 리팩터링_테스팅_디버깅

hongeeii 2023. 1. 24.
728x90
반응형

람다, 메서드 참조, 스트림을 활용해서 코드 가독성을 개선할 수 있는 간단한 세 가지 리팩터링 예제를 소개한다.

1 - 코드 가독성 개선

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

ex) Runnable 객체를 만드는 익명 클래스와 이에 대응하는 람다 표현식

Runnable r1 = new Runnable() { // 익명클래스
    public void run() {
        System.out.println("Hello");
    }
};

// 람다표현식의 코드
Runnable r2 = () -> System.out.println("Hello");

하지만 모든 익명클래스를 변환할 수 있는것은 아니다.
익명 클래스의 this, super는 람다 표현식에서 다음 의미.

  • 익명클래스에서 this 는 익명클래스 자신을 가리키지만, 람다에서 this 는 람다를 감싸는 클래스를 가리킴.
  • 익명클래스는 감싸고있는 클래스의 변수를 가릴수 있음(섀도 변수), 람다에서는 변수를 가릴수 없음.
int a = 10;

Runnable r1 = new Runnable() {
    public void run() {
        int a = 2; // 잘 동작.
        System.out.println(a);
    }
};

Runnable r2 = () -> {
    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 a){ r.execute(); }

// Task를 구현하는 익명클래스 전달가능.
doSomething(new Task() {
    public void execute() {
        System.out.println("Danger danger!!");
    }
});

//하지만 익명클래스를 람다로 바꾸면 메서드 호출시 Runnable 과 Task 모두 대상형식이 되므로 모호함 발생.

doSomething(() -> System.out.println("Danger danger!!"));

//명시적 형변환으로 모호한 제거 가능.
doSomething((Task)() -> System.out.println("Danger danger!!"));

3- 람다 표현식을 메서드 참조로 리팩터링
메서드 참조의 메서드 명으로 코드의 의도를 명확하게 알릴수 있음.

ex) 6장의 칼로리 수준으로 요리 그룹화하는 코드

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;
            }));
            
// 람다표현식을 별도의 메서드로 추출한 후 groupingBy에 인수로 전달.
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
    menu.stream().collect(groupingBy(Dish::getCaloricLevel));
    
public class Dish {
    ...
    public CaloricLevel getCaloricLevel() {
        if(dish.getCalories() <= 400) return CaloricLevel.DIET;
        else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    }
}

또한 comparing 과 maxBy 같은 정적 헬퍼 메서드를 활용하는 것도 좋다.

// 비교구현에 신경써야함.
inventory.sort(
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

// 코드가 문제 자체를 설명.
inventory.sort(comparing(Apple::getWeight));
  • 함수형 인터페이스 적용 - 람다 표현식을 이용하려면 함수형 인터페이스가 필요
  • 조건부 연기 실행

실제 작업을 처리하는 코드 내부에 제어 흐름문이 복잡하게 얽힌 코드를 종종 보임.

if(logger.isLoggable(Log.FINER)) {
    logger.finer("Problem: " + generateDiagnostic());
}
  • logger 상태가 isLoggable이라는 메서드에 의해 클라이언트 코드로 노출
  • 메시지를 로깅할 때마나 logger객체 상태를 매번 확인해야 하므로 코드를 어지럽힘.

메시지를 로깅하기전에 logger객체가 적절한 수준으로 설정되었는지 내부적으로 확인하는 log메서드를 사용

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

하지만 logger가 활성화 되지 않더라도 항상 로깅 메시지를 평가하게 되므로 람다를 이용해 문제를 해결.

특정조건 에서만 메시지가 생성될 수 있도록 메시지 생성과정을 연기 할수 있게함.

자바 8에서는 Supplier를 인수로 갖는 오버로드된 log 메서드 제공

// 새로 추가된 log 메서드의 시그니처
public void log(Level level, Supplier<String> msgSupplier)

// 다음과 같이 호출


// log 메서드는 logger의 수준이 적절하게 설정되어 있을때만 인수로 넘겨진 람다를 내부적으로 실행
public void log(Level level, Supplier<String> msgSupplier) {
    if(logger.isLoggable(level)) {
        log(level, msgSupplier.get());
    }
}

만일 클라이언트 코드에서 객체 상태를 자주 확인(logger 상태)하거나

객체 일부 메서드를 호출하는 상황(메시지 로깅)이라면

내부적으로 객체의 상태를 확인한 다음 메서드를 호출(람다나 메서드 참조를 인수로 사용)하도록

새로운 메서드를 구현하는것이 좋다.

코드의 가독성이 좋아질 뿐 아니라 캡슐화도 강화 (객체 상태가 클라이언트 코드로 노출되지 않음.)


- 실행 어라운드 : 3장 참고




람다로 객체지향 디자인 패턴 리팩터링하기

람다식을 이용해 이전의 디자인 패턴으로 해결하던 문제를 더 쉽게 해결할수도 있고, 람다식으로 기존의 객체지향 디자인 패턴을 제거하거나 간결하게 재구현할수도 있음.

  • 전략 : 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법.

다양하나 기준을 갖는 입력값을 검증하거나, 다양한 파싱방법을 사용하거나, 입력 형식을 설정하는 등, 다양한 시나리오에 전략패턴 사용.

  • 세부분으로 구성됨.
  • 알고리즘을 나타내는 인터페이스(Strategy 인터페이스)
  • 다양한 알고리즘을 나타내는 한개 이상의 인터페이스 구현(ConcreatStrategyA, ConcreatStrategyB 구체적 구현클래스)
  • 전략 객체를 사용하는 한개 이상의 클라이언트

ex) 오직 소문자 또는 숫자로 이루어져야 하는 등 텍스트 입력이 다양한 조건에 맞게 포맷되어 있는지 검증한다고 가정하자. String 문자열을 검증하는 인터페이스

public interface ValidationStrategy {
    boolean execute(String s);
}

//구현 클래스 하나이상.
public class IsAllowerCase implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}
public class IsNumeric implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

//검증 전략

public class Validator {
    private final ValidationStrategy strategy;
    public Validator(ValidationStrategy v) {
        this.strategy = v;
    }
    public boolean validate(String s) {
        return strategy.execute(s);
    }
}


Validator numericValidator = new Validator(new IsNumeric);
boolean b1 = numericValicator.validate("aaaa"); // false
Validaror lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb"); // true

alidationStrategy는 함수형 인터페이스이고 Predicate 과 같은 함수 디스크립터를 갖고있다.
따라서 새로운 클래스 구현없이, 직접 람다 표현식을 전달 가능.


Validator numericValidator = 
    new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = nemericValidator.validate("aaaa");
Validator lowerCaseValidator = 
    new Validator((String s) -> s.matches("\\d+"));
boolean b1 = lowerCaseValidator.validate("aaaa");    
    

람다 표현식을 사용하면 전략 디자인 패턴에서 발생하는 자잘한 코드르 제거할 수 있다.
람다 표현식은 코드 조각(또는 전략)을 캡슐화한다. 람다 표현식으로 전략 디자인 패턴을 대신할 수 있다.



  • 템플릿메서드

템플릿메서드는 '이 알고리즘을 사용하고 싶은데 그대로는 안되고 조금 고쳐야 하는' 상황에 적합.

ex) 간단한 온라인 뱅킹 애플리케이션을 구현하는 예제

  • 사용자가 고객 ID를 애플리케이션에 입력하면 은행 데이터베이스에서 고객 정보를 가져오고 고객이 원하는 서비스를 제공
  • 고객 계좌에 보너스를 입금한다고 가정
// 온라인 뱅킹 애플리케이션 동작을 정의하는 추상 클래스
abstract class OnlineBanking {
    public void processCustomer(int id) {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }
    abstract void makeCustomerHappy(Customer c);
}

각각의 지점은 OnlineBanking 클래스를 상속받아 makeCustomerhappy메서드가 원하는 동작을 수행하도록 구현

  • 람다사용 : 알고리즘의 개요를 만든 다음에 구현자가 원하는 기능을 추가할 수 있게 만들어 보기 람다나 메서드 참조로 아록리즘에 추가할 다양한 컴포넌트를 구현할 수 있다.


    이전에 정의한 makeCustomerhappy의 메서드 시그니처와 일치하도록
    Consumer 형식을 갖는 두번째 인수를 processCustomer에 추가.
    public void processCustomer(int id, Cunsumer<Customer> makeCustomerHappy) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(c);
}

//onlineBanking 클래스를 상속받지 않고 직접 람다를 전달해서 다양한 동작 추가 가능.
new OnlineBankingLamda().processCustomer(1337, (Customer c) -> 
    System.out.println("Hello "+ c.getName()));



  • 옵저버 어떤 이벤트가 발생했을때 한 객체(subject)가 다른 객체 리스트(observer)에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용. GUI 애플리케이션에서 옵저버 패턴이 자주등장. && 주식의 가격 변동에 반응하는 다수의 거래자 예제....

ex) 트위터같은 커스터마이즈된 알림 시스템을 설계하고 구현.

1. 다양한 옵저버를 그룹화할 Observer 인터페이스가 필요하다. 
interface Observer {
    void notify (String tweet);
}

// 트윗에 포함된 다양한 키워드에 다른 동작을 수행할 수 있는 여러 옵저버 정의.
class NYTimes implements Observer {
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("money")){
            System.out.println("Breaking news in NY! " +tweet);
        }
    }
}
class Gurdian implements Obserber {
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("queen")) {
            System.out.println("Yet more news from London... " +tweet);
        }
    }
}
class LeMonde implements Obserber {
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine and news! " +tweet);
        }
    }
}

// 주제 구현.
interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}

// 주제는 registerObserver 메서드로 새로운 옵저버를 등록한 다음 
// notifyObserver 메서드로 트윗의 옵저버에 이를 알림
class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    public void registerObserver(Observer o) {
        this.observers.add(o);
    }
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}
 
    
    
Feed f = new Feed();
f.registerObserer(new NYTimes());
f.registerObserer(new Guardian());
f.registerObserer(new LeMonde());
f.registerObserer("The queen said her favourite book is 유니감자 ");
  • 람다 사용 : 세개의 옵저버를 명시적으로 인스턴스화 하지않고 람다표현식을 직접 전달해서 실행할 동작 지정.
    f.registerObserver((String tweet) -> {
    if(tweet !=null && tweet.contains("money")) {
        System.out.println("Breaking news in NY! "+tweet);
    }
}
  • 의무 체인

작업처리 객체의 체인(동작 체인 등)을 만들때는 의무 체인 패턴을 사용.
- 일반적으로 다음으로 처리할 객체 정보를 유지하는 필드를 포함하는 작업처리 추상클래스로 의무체인 패턴을 구성.

  • 작업처리 객체가 자신의 작업을 끝냈으면 다음 작업처리 객체로 결과를 전달.
    
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 = handleWork(input);
        if(successor != null) {
            return successor.handle(r);
        }
        return r;
    }
    abstract protected T handleWork(T input);
}
    
  • 템플릿 메서드 사용 : ProcessingObject 클래스를 상속받아 handleWork 메서드를 구현하여 다양한 종류의 작업처리 객체를 만들수 있음. ex) 텍스트를 처리하는 예제




public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return "From Raoul, Mario and Alan: "+text;
    }
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda"); //일부러 m 빠트림
    }
}

// 두 작업처리 객체를 연결해서 작업 체인
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result);
  • 람다 사용
    이 패턴은 함수체인과 비슷하다. //3장 다시 한번 점검 3장에서 람다 표현식 조합방법과 같이 작업처리 객체를 Function<String, String>

UnaryOperator 형식의 인스턴스로 표현 가능.
andThen 메서드로 이들 함수를 조합해서 체인 만들기.

UnaryOperator<String> headerProcessing = 
    (String text) -> "From Raoul, Mario and Alan: "+text;
UnaryOperator<String> spellCheckerProcessing = 
    (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = 
    headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labda really sexy?!!");
  • 팩토리 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들때 팩토리 디자인 패턴을 사용.
    ex) 은행에서 취급하는 대출, 채권, 주식 등 다양한 상품을 만드는 예제
public class ProductFactory {
    public static Product createProduct(String name) {
        switch(name) { // 모두 Product의 서브 형식.
            case "loan": return new Loan();
            case "stock": return new Stock();
            case "bond": return new Bond();
            default: throw new RuntimeException("No such product "+ name);
        }
    }
}
// 생성자와 설정을 외부로 노출하지 않음.
// 클라이언트가 단순하게 상품을 생산.
Product p = ProductFactory.createProduct("loan");
  • 람다 사용 : 생성자도 메서드 참조처럼 접근할 수 있다.
Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get();

// 상품명을 생성자로 연결하는 Map을 만들어 재구현
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);
}

// Map을 이용해 팩토리디자인패턴 에서 한것처럼 다양한 상품 인스턴스화
public static Product createProduct(String name) {
    Supplier<Product> p = map.get(name);
    if(p != null) return p.get();
    throw new IllegalArgumentException("No such product "+ name);
}    
    
    
  • 람다 테스팅

일반적으로 프로그램이 의도대로 동작하는지 확인하는 단위 테스팅을 진행함.

// 그래픽 애플리케이션 일부 Point 클래스
public class Point {
    private final int x;
    private final int y;
    
    private Point(int x, int y) {
        this.x = x;
        thix.y = y;
    }
    public int getX(){return x;}
    public int getY(){return y;}
    public Point moveRightBy(int a) {
        return new Point(this.x + a, this.y);
    }
}

// 단위테스트
@Test
public void testMoveRightBy() throws Exception {
    Point p1 = new Point(5,5);
    Point p2 = p1.moveRightBy(10);
    assertEqual(15, p2.getX());
    assertEqual(5, p2.getY());
}    

테스트 케이스 내부에서 Point클래스 코드를 테스트할 수 있으나 람다는 익명함수이므로 테스트 코드 이름을 호출할 수 없음.

그래서 필요하다면 람다를 <b>필드에 저장</b> 해서 재사용할 수 있으며 람다의 로직을 테스트 할 수 있다.  

Point 클래스에 compareByXAndThenY라는 정적 필드를 추가.

람다 표현식은 함수형 인터페이스의 인스턴스를 생성함.

따라서 메소드를 호출해 인스턴스의 동작으로 람다 표현식을 테스트할 수있음.

public class Point {
    public final static Comparator<Point> compareByXAndThenY = 
        comparing(Point::getX).thenComparing(Point::getY);
    ...    
}

// Comparator 객체 compareByXAndThenY에 다양한 인수로 compare 메서드 호출
@Test
public void testComparingTwoPoints() throws Exception {
    Point p1 = new Point(10, 15);
    Point p2 = new Point(10, 20);
    int result = Point.compareByXAndThenY.compare(p1, p2);
    assertTrue(result < 0);
}    
  • 람다를 사용하는 메서드의 동작에 집중하라

람다의 목표는 정해진 동작을 다른메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화하는 것이다.
그러려면 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 한다. 람다 표현식을 사용하는 메서드의 동작을 테스트 함으로써 람다를 공개하지 않으면서도 람다 표현식을 검증할 수 있음.


public static List<Point> moveAllPointsRightBy(List<Point> points, int a) {
    return points.stream()
                 .map(p -> new Point(p.getX() + a, p.getY()))
                 .collect(toList());
}

// moveAllPointsRightBy 메서드 동작 확인
@Test
public void testMoveAllPointsRightBy() throws Exception {
    List<Point> points = 
        Arrays.asList(new Point(5, 5), new Point(10,5));
    List<Point> expectedPoints = 
        Arrays.asList(new Point(15, 5), new Point(20,5));
    List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
    assertEquals(expectedPoints, newPoints);
} 
    
  • 복잡한 람다를 개별 메서드로 분할하기

테스트 코드에서 람다 표현식을 참조할 수 없는데 복잡한 람다 표현식을 어떻게 테스트 할것인가?
--> 람다 표현식을 메서드 참조로 바꾸는 것이다.
그러면 일반 메서드를 테스트하듯 람다표현식을 테스트 할 수있음.

========================================

  • 고차원 함수 테스팅

함수를 인수로 받거나 다른함수를 반환하는 메서드는 사용하기 어려움. 메서드가 람다를 인수로 받는다면 다른 람다로 메서드 동작 테스트 가능

ex) 2장의 프레디케이트로 만든 filter 메서드 테스트

@Test
public void testFilter() throws Exception {
    List<Integer> numbers = Arrays.asList(1,2,3,4);
    List<Integer> even = filter(number, i -> i % 2 == 0);
    List<Integer> smallerThanThree = filter(number, i-> i<3);
    assertEquals(Arrays.asList(2,4), even);
    assertEquals(Arrays.asList(1,2), smallerThanThree);
}

테스트할 메서드가 다른 함수를 반환한다면? - 함수형 인터페이스의 인스턴스로 간주하고 함수의 동작을 테스트.

  • 디버깅

람다 표현식과 스트림은 기존의 디버깅 기법을 무력화 한다.

  1. 스택 트레이스 확인
  • 람다 표현식은 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어냄.
  • 메서드 참조를 사용해도 스택 트레이스에는 메서드 명이 나타나지 않음.
  • 메서드 참조를 사용하는 클래스와 같은곳에 선언되어 있는 메서드를 참조할때는 스택트레이스에 메서드 참조 이름이 나타남.
  1. 정보 로깅

스트림 파이프라인 연산을 어떻게 디버깅 할 수 있을까? --> forEach 로 스트림결과를 출력하거나 로깅할수있다.

하지만 forEach를 호출하는 순간 전체 스트림이 소비됨.

스트림 파이프라인에 적용된 각각의 연산(map, filter, limit)이 어떤 결과를 도출하는지 확인하고 싶다. -> peek 스트림 연산을 활용

peek은 실제로 스트림의 요소를 소비하지 않고 자신이 확인한 요소를 파이프라인의 다음연산으로 그대로 전달함

  • 람다 표현식으로 가독성 좋고 유연한 코드
  • 익명클래스는 람다 표현식으로 바꾸는 것이 좋다. 이때 this, 변수섀도 등 주의.
  • 메서드 참조로 람다 표현식보다 더 가독성 좋은 코드 구현
  • 반복적으로 컬렉션을 처리하는 루틴은 스트림 API 로 대체가능한지 고려
  • 람다 표현식으로 전략, 템플릿메서드, 옵저버, 의무 체인, 팩토리 등 객체지향 디자인 패턴에서 발생하는 불필요한 코드 제거
  • 람다 표현식으로 단위테스트 수행. 람다 표현식 자체를 테스트하는것보다 사용되는 메서드의 동작을 테스트.
  • 복잡한 람다 표현식은 일반 메서드로 재구현
  • 람다 표현식을 사용하면 스택 트레이스를 이해하기 어려움
  • 스트림 파이프라인에서 요소를 처리할때 peek메서드로 중간값 확인 가능.
728x90
반응형

추천 글