2장. 동작 파라미터화 코드 전달하기

Posted by yunki kim on February 13, 2022

  소비자 요구사항은 항상 바뀐다. 따라서 변경에 대한 비용을 최소화 하고 기능 추가가 용이해야 한다.

  동작 파라미터화(behavior parameterization)를 사용하면 바뀌는 요구사항에 효과적 대응이 가능하다. 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록의 실행을 나중으로 미뤄진다. 따라서 코드 블록에 따라 메서드의 동작이 파라미터화 된다.

동작 파라미터화

  사과를 무게 또는 색으로 필터하는 기능이 있다고 해보자. 이 기능은 다음과 같이 구현될 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static List<Apple> filterAplles(List<Apple> inventory, Color, color, 
        int weight, boolean flag) {
    List<Apple> result = new ArryaList<>();
    for (Apple apple : inventory) {
        // 색이나 무게 중 하나의 조건을 만족하면
        // flag 로 조건이 무게인지 색인지 판별
        if ((flag && apple.getColor().equals(color)) 
                || (!flag && apple.getWeight() > weight)) {
            result.add(apple);
        }
    }
    return result;
}
cs

  위 코드는 변경이 용이하지 않고 요구 사항이 늘어나면 거대한 메서드가 된다. 

  이는 동작 파라미터화를 통해 해결할 수 있다. 동작 파라미터화는 유연성을 확보한다.

  사과의 속성에 기초해 불리언 값을 반환하는 프리디케이트 함수를 만들면 된다. 조건에 따라 참 또는 거짓을 반환하는 함수를 프리디케이트 함수라 한다.

  위 구조는 조건에 따라 filter 메서드가 다르게 동작한다. 이를 전략 디자인 패턴(strategy design pattern)이라 한다. 전략 디자인 패턴은 각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의하고 런타임에 알고리즘을 선택하는 방식이다. 위 구조에서는 ApplePredicate가 알고리즘 패밀리다.

  이런 동작 파라미터화를 통해 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
// 동작 파라미터화로 컬렉션 반복 로직과 각 요소에 적용할 동작을 분리해
// 이득을 얻었다.
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArryaList<>();
    for (Apple apple : inventory) {
        if (p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}
cs

  이런 구현은 동작을 추상화해 변화에 유연히 대응할 수 있다. 하지만 동작을 filterApples에 전달하기 위해 인스턴스를 구현한 클래스들을 정의하고 인스턴화 하는 것은 여간 귀찮은 일이 아니다. 이를 해결하기 위해 익명 클래스(anonymous class)를 사용하면 된다.

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

  하지만 이 방법은 그저 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금 줄였을 뿐 여전히 많은 공간을 차지한다. 또 한 많은 프로그래머는 익명 클래스 사용에 익숙치 않다.

  람다를 사용하면 이 문제가 해결된다. 아래 코드 처럼 람다를 사용하면 코드가 간결해지고 문제를 더 잘 설명한다.

1
2
List<Apple> redApples = filterApples(inventory,
    (APPLE APPLE) -> RED.EQUALS(apple.getColor()));
cs

 위에서 말한 내용을 그림으로 표현하면 다음과 같다

실전 예제

  동작 파라미터화는 변화하는 요구사항에 유연히 대처하는 패턴이다. 동작 파라미터화 패턴은 동작(코드 조각)을 캡슐화해 메서드로 전달해서 메서드의 동작을 파라미터화한다. 이제 코드 전달 개념을 더 확실히 하기 위해 Comparator를 통한 정렬, Runnable로 코드 블록 실행, Collable을 결과로 반환, GUI 이벤트 처리를 살펴보자.

Comparator로 정렬하기

  컬랙션 정렬은 반복되는 프로그래밍 작업이다. 그리고 요구사항은 항상 변한다. 따라서 요구사항변화에 유연히 대처하는 정렬을 하는 코드가 필요하다.

  자바 8의 List에는 sort 메서드가 포함되 있다.

1
2
3
4
5
6
7
8
9
10
default void sort(Comparator<super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}
 
cs

  이 메서드는 Comparator 객체를 이용해 sort의 동작을 파라미터화할 수 있다.

1
2
3
4
5
// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}
 
cs

  Comparator 사용 예시는 다음과 같다.

1
2
3
4
5
// 요구사항이 바뀌면 그에 맞는 Comparator를 만들어 sort 메서드에
// 전달하면 된다.
inventory.sort(
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
 
cs

Runnable로 코드 블록 실행

  자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있고 여러 스레드가 각자 다른 코드를 실행할 수 있다. 여기서 Runnable 인터페이스를 사용하면 실행할 코드 블록을 지정할 수 있다.

1
2
Thread t = new Thread(() -> System.out.println("Hello world"));
 
cs

Callable을 결과로 반환하기

  Callable 인터페이스를 이용하면 결과를 반환하는 태스크를 만들 수 있다. 이는 Runnable의 업그레이드 정도로 생각할 수 있다. 아래 예제에서 ExecutorService는 자바 5부터 지원하는 인터페이스로 태스트 제출과 실행 과정의 연관성을 끊는다. ExecutorService를 이용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다.

1
2
Future<String> threadName = executorService.submit(
    () -> Thread.currentThreadName());
cs

GUI 이벤트 처리하기

  일반적으로 GUI 프로그래밍은 이벤트에 대응하는 동작을 수행하는 식으로 동작한다. 자바FX에서는 setOnAction 메서드에 EventHandler를 전달해 이벤트에 대응 방식을 설정할 수 있다. 여기서 EventHanlder는 setOnAction 메서드의 동작을 파라미터화한다.

1
2
button.setOnAction((ActinoEvent event) -> label.setText("Sent!!"));
 
cs

 

 

출처 - 모던 자바 인 액션