2장

1. 변화하는 요구사항에 대응하기

사용자 요구사항은 시시각각 변한다. 동작 파라미터화를 사용하면 변덕스러운 요구사항에 적절히 대처할 수 있다. **동작 파라미터화(behavior parameterization)**란 아직 어떻게 실행할 것인지 결정하지 않은 코드 블록을 말한다. 이 코드 블록은 나중에 호출된다. 메서드가 나중에 실행된다고 가정하자. 메서드의 인수로 코드 블록을 전달하면 코드 블록에 따라 메서드의 동작 또한 파라미터화된다.

1.1. 첫 번째 시도 : 색상 필터링

1장과 마찬가지로 사과를 색깔로 필터링하는 코드를 작성한다고 생각해보자. 먼저 사과의 색상을 나타내는 열거형 클래스 Color가 아래와 같이 정의되어 있다.

enum Color { RED, GREEN };

다음은 첫 번째 필터링 코드이다.

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (GREEN.equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

여기서 필터링을 하는 조건은 if문의 GREEN.equals(apple.getColor())이다. 여기서 빨간 사과를 필터링하고 싶은 경우 어떻게 해야 할까? 가장 쉬운 방법은 코드를 복사해서 필터링 조건만 바꾸는 것이다. 만일 노란색 사과를 필터링하고 싶다면 새롭게 코드를 복사해서 조건을 바꿔야 한다. 사과를 필터링하는 조건이 바뀐다면 상황에 유연하게 대응하기 어렵다. 이 상황을 해결하기 위한 좋은 규칙은 다음과 같다.

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.

1.2. 두 번째 시도 : 색의 파라미터화

중복되지 않는 건 필터링 조건 뿐이다. 추상화는 구체화의 반대말로 상위 카테고리이자 보편적 개념이다. '녹색'과 '적색'의 공통점은 '색'이다. 필터링 조건인 '색'을 비교하면 중복을 피할 수 있다. 다시 말해서 메서드의 인수로 색이라는 파라미터를 추가하면 중복을 피하고 보다 유연한 코드를 작성할 수 있다. 다음은 색을 파라미터화한 두 번째 필터링 코드이다.

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}

두 번째 필터링 코드에서 메서드의 이름과 매개변수, 필터링 조건이 바뀌었다. 이 메서드는 다음과 같이 호출할 수 있다.

List<Apple> greenApples = filterApplesByColor(inventory, GREEN);

List<Apple> redApples = filterApplesByColor(inventory, RED);

그런데 갑자기 요구사항이 추가되어, 사과의 무게를 구분해야 한다면 어떻게 할까? 색상 필터링 코드에서 매개변수와 필터링 조건만 바꾸어 작성할 수 있다.

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getWeight() > weight) {
            result.add(apple);
        }
    }
}

또 다시 중복되는 코드가 늘었다. 보다 구체적으로 소프트웨어 공학의 DRY(don't repeat yourself, 같은 것을 반복하지 말 것) 원칙을 어기고 있다. 중복을 피하기 위한 가장 간단한 방법은 필터링 조건을 변경하는 것이다. 색이나 무게 중 어떤 것을 기준으로 필터링할 지 가리키는 플래그를 추가할 수 있다. 이 방법은 실전에서 절대 사용하지 말아야 한다는 것을 명심하자.

1.3. 세 번째 시도 : 플래그 추가

다음은 만류에도 불구하고 모든 속성을 메서드 파라미터로 추가한 세 번째 필터링 코드이다.

public static List<Apple> filterApples(List<Apple> inventory, Color color,
    int weight, boolean flag) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if ((flag && apple.getColor().equals(color)) ||
        (!flag && apple.getWeight() > weight)) {
            result.add(apple);
        }
    }
    return result;
}

이 메서드는 다음과 같이 호출할 수 있다.

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);

List<Apple> heavyApples = filterApples(inventory, null, 150, false);

어째서 이 코드가 형편없을까? 세 번째 필터링 코드는 문제가 잘 정의되어 있는 상황에서만 효과적으로 동작한다. 변화하는 요구사항에 맞게 코드를 변경하다보면 하나의 거대한 필터 메서드가 될 것이다.

2. 동작 파라미터화

변화하는 요구사항에 유연하게 대응하려면 어떤 방법을 사용해야 할까? 사과에는 색깔, 무게 등 다양한 속성이 있다. 이 속성에 기초해서 boolean값을 반환하는 방법이 있다. boolean 값을 반환하는 함수를 프레디케이트(predicate)라고 한다. 선택 조건을 결정하는 프레디케이트 인터페이스를 작성한다고 생각해보자.

public interface ApplePredicate {
    boolean test(Apple apple);
}

그리고 ApplePredicate 인터페이스를 구현한 필터링 클래스를 작성해보자.

public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150; // 핵심 코드
    }
}

public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor()); // 핵심 코드
    }
}

이제 조건에 따라 filter 메서드가 다르게 동작한다. 이를 전략 디자인 패턴이라고 한다. 전략 디자인 패턴(strategy design pattern)은 각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 알고리즘을 선택하는 기법이다. 예제에서는 ApplePredicate가 알고리즘 패밀리고 AppleWeightPredicate와 AppleGreenColorPredicate가 전략이다. 이제 filterApples에서 ApplePredicate 객체를 받아 조건을 검사하도록 메서드를 고쳐보자. 동작 파라미터화는 메서드가 다양한 동작(전략)을 받아서 내부적으로 다양한 동작을 수행하는 걸 의미한다.

2.1. 네 번째 시도 : 추상적 조건으로 필터링

다음은 프레디케이트 객체로 필터링 조건을 캡슐화한 네 번째 필터링 메서드이다.

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

이제 Apple 속성과 관련한 모든 변화에 대응하는 유연한 코드를 작성할 수 있다. 이 필터링 메서드는 다음과 같이 실행한다.

List<Apple> redApples = filterApples(inventory, new AppleHeavyWeightPredicate());

사과 예제에서 가장 중요한 구현은 test 메서드이다. 안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다. 객체를 이용해서 코드를 전달하지 않으려면 람다 표현식을 써야 한다.

3. 복잡한 과정 간소화

filterApples 메서드로 새로운 동작을 전달하려면 어떻게 해야 할까? 앞서 작성한 예제는 ApplePredicate 인터페이스를 구현하는 클래스를 정의하고 인스턴스화해야 한다. 이 작업은 번거로울 뿐더러 쓸모없는 코드를 증가시킨다. 이를 개선할 수 있도록 자바는 익명 클래스 기법을 제공한다. 익명 클래스(anonymous class)는 클래스의 선언과 인스턴스화를 동시에 수행한다. 물론 익명 클래스가 모든 걸 해결하지 않지만, 상당 부분 개선할 수 있다.

3.1. 다섯 번째 시도 : 익명 클래스 사용

다음은 익명 클래스를 활용한 다섯 번째 필터링 코드이다.

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor);
    }
});

익명 클래스로 낭비되는 코드를 줄일 수 있다. 그러나 여전히 return RED.equals(apple.getColor);를 제외한 코드가 많은 공간을 차지한다. 코드의 장황함(verbosity)은 나쁜 특성이다. 여전히 코드 조각을 전달하는 과정에서 객체를 만들고 새로운 동작을 정의하는 메서드를 구현해야 한다. 동작 파라미터화를 이용하면 요구사항 변화에 더 유연하게 대응할 수 있다. 람다 표현식을 이용해서 코드를 작성해보자.

3.2. 여섯 번째 시도 : 람다 표현식 사용

다음은 람다 표현식을 활용한 여섯 번째 필터링 코드이다.

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

람다 표현식을 활용하면 객체를 만들고 메서드를 구현할 필요가 없다.

3.3. 일곱 번째 시도 : 리스트 형식으로 추상화

다음은 리스트 형식으로 추상화한 일곱 번째 필터링 코드이다.

public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (p.test(e)) {
            result.add(e);
        }
    }
    return result;
}

리스트 형식으로 추상화했기 때문에 다양한 형태의 리스트에 필터 메서드를 사용할 수 있다. 여섯 번째 시도처럼 람다 표현식을 사용하면 다음과 같다.

List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));

List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

4. 실전 예제

동작 파라미터화가 유용한 패턴임을 알았으니 실전 예제에 적용해보자.

4.1. Comparator로 정렬하기

컬렉션 정렬은 반복되는 프로그래밍 작업이지만, 변화하는 요구사항에 맞게 다양한 정렬 동작을 수행할 수 있어야 한다. 람다 표현식을 활용하면 간결하게 정렬을 구현 가능하다.

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// 익명 클래스 구현
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

// 람다 표현식 활용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

4.2. Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬 프로그래밍이 가능하다. 자바 7까지는 Thread 생성자에 객체만을 전달할 수 있었다. 따라서 결과를 반환하지 않는 void run 메서드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 게 일반적이었다. 각각의 스레드에 실행할 코드를 알려주려면 Runnable 인터페이스를 활용해 실행할 코드 블록을 지정해야 했다. 람다 표현식을 활용하면 간결하게 스레드를 구현 가능하다.

//java.lang.Runnable
public interface Runnable {
    void run();
}

// Runnable 구현
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello world!");
    }
});

// 람다 표현식 활용
Thread t = new Thread(() => System.out.println("Hello world!"));

4.3. Callable을 결과로 반환

자바 5부터는 ExecutorService 추상화 개념을 지원한다. ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다. ExecutorService를 이용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장한다. 그리고 Callable 인터페이스를 활용하면 결과를 반환하는 태스크를 만든다. 람다 표현식을 활용하면 간결하게 ExecutorService를 구현 가능하다.

// java.util.concurrent.Callable
public interface Callable<V> {
    V call();
}

// ExecutorService 구현
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
    @Override public String call() throws Exception {
        return Thread.currentThread().getName();
    }
});

// 람다 표현식 활용
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName());

4.3. GUI 이벤트 처리

GUI 프로그래밍도 변화하는 요구사항에 대응할 수 있어야 한다. 람다 표현식을 활용하면 간결하게 EventHandler를 구현 가능하다.

// EventHandler 구현
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent event) {
        label.setText("Sent!");
    }
});

// 람다 표현식 활용
button.setOnAction((ActionEvent event) -> label.setText("Sent!"));

참고 자료

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

Last updated