본문 바로가기

프로그래밍/Java & Spring Framework

동작 파라미터화 (Behavior Parameterization)

반응형

동작 파라미터화 (Behavior Parameterization) 는 "다양한 동작을 수행할 수 있는 코드 블럭을 메서드 파라미터로 전달"함으로써 자주 변경되는 요구사항에 효과적으로 대응할 수 있는 방식을 의미합니다.

여기서는 자주 변경되는 요구사항의 예시를 기반으로 동작 파라미터화를 한 후 Java8에서 추가된 Lambda로 코드를 간소화하는 과정을 보여줍니다.

요구사항

우리는 농장 재고 애플리케이션을 개발합니다. 농부는 녹색 사과 만 필터링 하는 기능을 추가하길 원했습니다.

시도 1 : 녹색 사과 필터링

아래와 같이 녹색 사과만 필터링하는 메서드를 구현했습니다.
그런데 농부가 빨간 사과 를 필터링하는 기능을 추가하길 원했습니다.

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

시도 2 : 색을 파라미터화

메서드의 파라미터로 색을 추가하여 유연하게 개선했습니다.
그런데 이번엔 150g 이상 무게가 나가는 사과 를 필터링하길 원했습니다. 즉, 필터의 기준이 계속해서 변경될 가능성이 존재합니다.

아래와 같이 파라미터(color, weight)를 다르게 하여 원하는 결과를 얻을 수 있지만 중복된 코드가 존재합니다.

/**
 * 색(Color)를 기준으로 필터링
 */
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}

/**
 * 무게(Weight)를 기준으로 필터링
 */
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);
        }
    }
    return result;
}

시도 3 : 가능한 모든 속성으로 필터링

앞서 농부가 요구했던 "색"과 "무게" 요구사항을 모두 합친 필터링 메서드를 구현합니다.
그러나 이 방식은 많은 문제점이 존재합니다.

만약 농부가 "150g 이상의 빨간 사과를 필터링"하고 싶어 한다면 또다시 코드가 중복되는 비슷한 메서드를 추가하거나, 조건절이 늘어나는 거대한 메서드를 만들게 될 것입니다.
또한 파라미터의 flag 값의 의미를 단번에 알 수 없습니다.

public static List<Apple> filterApples(List<Apple> inventory, String 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, "", 150, false);

시도 4 : 동작 파라미터화

사과의 어떤 속성에 기초하여 Boolena 값을 반환하는 메서드를 가진 인터페이스를 사용합니다. 이와 같은 동작을 프레디케이트 (Predicate) 라고 합니다.

Predicate 의 사전적 의미는 "술어" 입니다.
술어 의 사전적 의미는 "논리의 판단. 명제에서 주사에 대하여 긍정 또는 부정의 입언을 하는 개념" 입니다.

아래의 예제에선 전략 디자인 패턴 (Strategy Design Pattern) 을 사용하여 사과를 선택하는 조건을 캡슐화 했습니다.
전략 디자인 패턴 (Strategy Design Pattern)은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해 두고, 런타임에 알고리즘을 선택하는 기법입니다.
여기서 "알고리즘 패밀리"는 AppleHeavyWeightPredicate 메서드와 AppleGreenColorPredicate 메서드`가 됩니다.

만약 농부가 새로운 필터링 조건을 요구하더라도 ApplePredicate 인터페이스 를 구현하는 메서드를 추가하면 됩니다.
이와 같이 메서드의 파라미터로 코드 블럭을 전달함으로써 메서드의 동작을 파라미터화 한 것 입니다.

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

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

// 색과 무게를 동시에 필터링 조건으로 갖는 메서드(전략 or 알고리즘 패밀리) 추가
public class AppleRedAndHeavyPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor()) && apple.getWeight() > 150;
    }
}

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> greeApples = filterApples(inventory, new AppleGreenColorPredicate());
// 무게가 150g 이상인 사과만 필터링
List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());

시도 5 : 익명 클래스를 통한 코드 간소화

앞선 예제는 동작 파라미터화를 위해 여러 개의 클래스를 정의했습니다.
익명 클래스 (Anonymous Class) 를 통해 클래스 정의 없이 필터링 구현이 가능합니다.

그러나 자칫하면 익명 클래스로 인해 코드가 장황해져 유지보수가 어려워 질 수 있습니다.

// 빨간 사과만 필터링
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    } 
});

시도 6 : 람다 표현식 사용

자바8의 람다 표현식으로 위 예제 코드를 간단하게 구현할 수 있습니다.

List<Apple> redApples = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

시도 7 : 리스트 형식으로 추상화

사과 이외에도 다양한 물건을 필터링 할 수 있도록 추상화 할 수 있습니다. (Generic 사용)

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<String> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
반응형

'프로그래밍 > Java & Spring Framework' 카테고리의 다른 글

스트림(Stream) 활용  (0) 2019.11.16
스트림 (Stream)  (0) 2019.11.16
람다 표현식 (Lambda Expression)  (0) 2019.11.16
Private 메소드 단위 테스트하기  (0) 2019.11.16