모던 자바 인 액션 - 동작파라미터화

 

Chapter2: 동작 파라미터화 코드

동작파라미터화(behavior parameterization) 을 사용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미하며, 나중에 프로그램이 호출한다. 메서드의 인수로 동작파라미터화 코드 블럭를 전달하면 메서드의 동작이 파라미터화 된다.

같은 메서드에서 수행 가능한 동작들
  • 리스트의 모든 요소에 대해서 ‘어떤 동작’을 수행할 수 있다.

  • 리스트 관련 작업을 끝낸 다음에 ‘어떤 다른 동작을’ 수행할 수 있음

  • 에러가 발생하면 ‘정해진 어떤 다른 동작’을 수행할 수 있음

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

변화에 대응하는 코드를 구현하는 것은 어렵다. 예제 코드의 개선 과정을 보며 유연한 코드를 만들어 보자.

사과 농장의 재고 목록을 관리하는 애플리케이션에서 사과를 필터링하는 기능이 필요하다.

Step1) 녹색 사과를 필터링

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;
}
  • 녹색 사과만을 필터링하는 코드이다.
  • 만약, 빨간 사과를 필터링 하고 싶다면?… 또 다른 filterRedApples 라는 메서드를 만들어야 할 것이다. 코드의 생길 것이다.

#####

Step2) 색을 파라미터화

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);
  • 원하는 색상을 필터링하여 사과를 얻을 수 있게 되었다.
  • 또 다른 문제가 생겼다. 색상 뿐아니라 특정 무게 이상의 사과를 필터링하고 싶다면?… 색상을 필터링하는 코드와 비슷하게 무게를 파라미터로 전달하여 filterApplesByWeight 메서드를 만들 수도 있다. 하지만 비슷한 코드의 중복이 발생한다.

Step3) 가능한 모든 속성으로 필터링

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 = filterApplesByColor(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApplesByColor(inventory, null, 150, false);
  • 한마디로 형편없는 코드이다. 색상, 무게의 속성을 flag에 따라 필터링할 수 있게 만들었다.

    하지만, true, false 가 무엇을 의미하는 것인지 알기 어렵고, 만약 다른 속성이 추가되면 어떻게 대응을 해야 할까? 유연하게 대응하지 못할 것이다.

동작 파라미터화

Step1~3에서 본 문제점들을 동작 파라미터화를 통해서 유연성을 얻는 방법

참, 거짓을 반환하는 프레디케이트를 만들어 보자.
public interface ApplePredicate {
    boolean test(Apple apple);
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150; // 무게가 150 이상인 사과 필터링
    }
}
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor()); // 녹색 사과만 필터링
    }
}

Step4) 추상적 조건으로 필터링

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;
}
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());
  • 특정 조건의 필터링이 필요할 때, 그 조건의 Predicate 객체를 만들어 전달한다.
  • 메서드는 전달 받은 객체를 사용하여 필터링을 진행할 것이다.
  • Apple의 속성과 관련된 모든 변화에 대응할 수 있는 유연한 코드가 되었다. 필터링하고 싶은 속성을 Predicate 객체로 만들어 전달만 하면 된다.
  • 다만 클래스를 매번 만드는 방법은 인터페이스를 구현해야 하는 방식으로 새로 만들어야 한다. 이 또한 번거롭다.

Step5) 익명 클래스 사용

List<Apple> RedApples = filterApples(inventory, new ApplePredicate(){
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor());
    }
});
  • Step4와 동일한 동작을 하지만 클래스를 매번 생성할 필요없이 익명 클래스를 전달한다.
  • 하지만, 반복적으로 사용하려면 코드의 중복이 발생하고, 여전히 많은 공간을 차지한다.

Step6) 람다 표현식 사용

List<Apple> RedApples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
  • 자바 8에서 도입된 람다의 사용은 복잡한 익명 클래스를 대신하고, 훨씬 간결하다.

Step7) 리스트 형식으로 추상화

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

public static <T> List<T> filterApples(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);
  • 제네릭을 사용하여 추상화하였다. 사과를 필터링하는 것에 국한되지 않고 리스트에서 필터링하는 메서드를 사용할 수 있다.
  • 유연성과 간결함을 모두 잡았다.

마치며

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
  • 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
  • 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.