9장

1. 리팩터링

코드 가독성이 좋다는 건 처음 접하는 사람도 이해하기 쉽게 작성된 코드라는 말이다. 리팩터링은 코드를 간결하고 이해하기 쉽게 만드는 것이다. 람다, 메서드 참조, 스트림을 활용하면 코드 가독성을 높일 수 있다.

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

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

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

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

익명 클래스를 람다 표현식으로 변환할 때는 몇 가지 주의점이 있다. 첫째로 this와 super는 익명 클래스와 람다 클래스에서 의미가 다르다. 익명 클래스에서 this는 자신을 가리키지만, 람다 표현식에서 this는 람다를 감싸는 클래스를 가리킨다. 둘째로 익명 클래스는 감싸고 있는 변수를 가릴 수 있지만, 람다 표현식은 변수를 가릴 수 없다.

// 익명 클래스 활용
Runnable r1 = new Runnable() {
    public void run() {
        int a = 2;
        System.out.println(a);
    }
}

// 람다 표현식 활용
int a = 10;
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(); }

// 익명 클래스 활용
doSomething(new Task() {
    public void execute() {
        System.out.println("Danger danger!!");
    }
});

// 람다 표현식 활용(모호함)
doSomething(() -> System.out.println("Danger danger!!"));

// 람다 표현식 활용(명시적 형변환 사용)
doSomething((Task)() -> System.out.println("Danger danger!!"));

위 예시처럼 람다 표현식은 doSomething(Runnable)과 doSomething(Task) 중 어느 것을 가리키는지 말 수 없다. 이를 해결하기 위해 명시적 형변환을 사용할 수 있다.

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

람다 표현식을 메서드 참조로 리팩터링할 수 있다.

// 람다 표현식 활용
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;
        }));

// 메서드 참조 활용
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
    menu.stream()
        .collect(groupingBy(Dish::getCaloricLevel)); // 메서드로 추출

// 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 같은 정적 헬퍼 메서드를 활용할 수도 있다. sum, maximum 등 자주 사용하는 리듀싱 연산은 메서드 참조와 함께 사용할 수 있는 내장 헬퍼 메서드를 제공한다.

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

명령형 데이터 처리를 스트림으로 리팩터링할 수 있다.

// 명령형 데이터 처리
List<String> dishNames = new ArrayList<>();
for(Dish dish : menu) {
    if (dish.getCalories() > 300) {
        dishNames.add(dish.getName());
    }
}

// 스트림 API 활용
menu.parallelStream()
    .filter(d -> d.getCalories() > 300)
    .map(Dish::getName)
    .collect(toList());

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

디자인 패턴(design pattern)은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는 검증된 청사진을 말한다. 예를 들어 구조체와 동작하는 알고리즘을 분리하고 싶을 때 방문자 디자인 패턴(visitor design pattern)을 사용할 수 있다. 또한 클래스 인스턴스를 하나의 객체로 제한하기 위해 싱글톤 패턴(singleton pattern)을 사용할 수 있다.

2.1. 전략

전략 패턴(strategy pattern)은 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법이다. 전략 패턴은 세 부분으로 구성된다.

  • 전략 객체를 사용하는 한 개 이상의 클라이언트

  • 알고리즘을 나타내는 전략 인터페이스

  • 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스 구현

png
// 인터페이스
public interface ValidationStrategy {
    boolean execute(String s);
}

// 인터페이스 구현
public class IsAllLowerCase implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

public class IsNumeric implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matcheds("\\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 = numericValidator.validate("aaaa");

람다 표현식을 활용하면 전략 디자인 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.

Validator numericValidator = 
    new Validator((String s) -> s.matches("[a-z]="));

2.2. 템플릿 메서드

템플릿 메서드 패턴(template method pattern)은 알고리즘의 개요를 만든 다음 알고리즘의 일부를 수정하는 기법이다.

abstract class OnlineBanking {
    public void processCustomer(int id) {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }
    abstract void makeCustomerHappy(Customer c);
}

람다 표현식을 활용하면 템플릿 메서드 디자인 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(c);
}

// 람다 표현식 활용
new OnlineBankingLambda()
    .processCustomer(1337, (Customer c) ->
        System.out.println("Hello " + c.getName()));

2.3. 옵저버

옵저버 패턴(observer pattern)은 어떤 이벤트가 발생했을 때 한 객체(subject)가 다른 객체 리스트(observer)에 자동으로 알림을 보내는 기법이다. 옵저버 패턴으로 커스터마이즈 된 알림 시스템을 설계하고 구현할 수 있다.

png
// 인터페이스 정의
interface Observer {
    void notify(String tweet);
}
interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}

// 옵저버 구현
class NYTimes impelments Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("NYTimes! " + tweet);
        }
    }
}
class Guardian implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Guardian! " + tweet);
        }
    }
}

// 주제 구현
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.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.notifyObservers("Time is money.");

람다 표현식을 활용하면 옵저버 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.

f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("NYTimes! " + tweet);
    }
});
f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("queen")) {
        System.out.println("Guardian! " + tweet);
    }
});

2.4. 책임 연쇄

책임 연쇄 패턴(chain of responsibility pattern)은 작업 처리 객체의 체인을 만드는 기법이다. 한 객체가 어떤 작업을 처리한 다음 다른 객체로 결과를 전달하고, 다른 객체로 작업을 처리한 다음 또 다른 객체로 전달한다.

png
// 작업 처리 객체 추상 클래스
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 sucessor.handle(r);
        }
        return r;
    }
    abstract protected T handleWork(T input);
}

// 추상 클래스 상속
public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return "Title: " + text;
    }
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

// 작업 체인
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Labdas are great.");
System.out.println(result); // "Title: Lambdas are great." 출력

람다 표현식을 활용하면 책임 연쇄 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.

UnaryOperator<String> headerProcessing =
    (String text) -> "Title: " + text;
UnaryOperator<String> spellCheckerProcessing =
    (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline =
    headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Labdas are great.");

2.5. 팩토리

팩토리 패턴(factory pattern)은 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만드는 기법이다.

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("No such product " + name);
        }
    }
}
Product p = ProductFactory.createProduct("loan");

생성자 참조를 활용하면 팩토리 패턴에서 발생하는 자잘한 코드를 제거할 수 있다.

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();
    throw new IllegalArgumentException("No such product " + name);
}

3. 람다 테스팅

그래픽 애플리케이션의 일부인 Point 클래스가 있다고 생각해보자.

public class Point {
    private final int x;
    private final int y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }

    public int getY() { return y; }

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

람다는 익명 함수이므로 테스트 코드 이름을 호출할 수 없다. 그러나 람다 표현식은 함수형 인터페이스의 인스턴스를 생성하므로 생성된 인스턴스의 동작으로 람다 표현식을 테스트할 수 있다.

@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);
}

4. 디버깅

4.1. 스택 트레이스

public class Debugging {
    public static void main(String[] args) {
        List<Point> points = Arrays.asList(new Point(12, 2), null);
        points.stream().map(p -> p.getX()).forEach(System.out::println);
    }
}

코드를 실행하면 다음과 같은 스택 트레이스가 출력된다.

Exception in thread "main" java.lang.NullPointerException
    at Debugging.lambda$main$0(Debugging.java:6)
    at Debugging$$Lambda$5/284720968.apply(Unknown Source)

람다 표현식은 이름이 없기 때문에 컴파일러가 람다를 참조하는 이름을 만들어낸다. 메서드 참조를 사용해도 스택 트레이스에는 메서드 명이 나타나지 않는다. 참조하는 메서드가 동일한 위치에 선언되어 있어야만 메서드 참조 이름이 스택 트레이스에 출력된다.

4.2. 로깅

forEach를 호출하면 전체 스트림이 소비된다.

numbers.stream()
        .map(x -> x + 17)
        .limit(3)
        .forEach(System.out::println);

peek 스트림 연산을 활용해서 소비 없이 스트림 파이프라인 연산을 디버깅할 수 있다. peek은 각 원소를 소비한 것처럼 동작하지만 실제로 스트림의 원소를 소비하지는 않는다. peek은 자신이 확인한 원소를 다음 파이프라인으로 전달한다.

numbers.stream()
        .peek(x -> System.out.println("from stream: " + x))
        .map(x -> x + 17)
        .peek(x -> System.out.println("after map: " + x))
        .limit(3)
        .peek(x -> System.out.println("after limit: " + x))
        .collect(toList());

참고 자료

  • 모던 자바 인 액션 - 한빛미디어

Last updated