아카이브/Java

동작 파라미터화 (behavior parameterization)

될성부른떡잎 2019. 10. 22. 20:37

동작 파라미터화 (behavior parameterization)란?

동적 파라미터화는 아직 어떻게 실행할지 정해지지 않은 코드 블록을 의미한다. 이를 이용하여 변경되는 요구사항에 효과적으로 대응할 수 있다.

동작 파라미터화를 이용해 변하는 요구사항에 대응할 수 있는 코드를 만드는 예제

첫 번째 시도: 녹색 사과 필터링

아래는 녹색 사과를 필터링하는 코드이다. 만약 빨간 사과도 필터링하고 싶을 때에는 filterGreenApples 메서드를 복사해 filterRedApples 메서드를 만들고 if 문의 조건을 빨간 사과로 변경해야 한다.

이런 식으로 요구 사항마다 비슷한 반복된 코드가 추가되게 된다. 코드를 추상화해 반복을 없앤다.

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

 

두 번째 시도: 색을 파라미터화

색상을 파라미터로 추가하여 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있다.

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

    return result;
}

하지만 이게 좋은 방법일까?
filterApplesByWeight 메서드의 대부분의 코드가 색상으로 사과를 필터링하는 filterApplesByColor 메서드의 코드와 중복된다.
이는 DRY(Don't repeat yourself) 원칙에 위배되는 내용이다. 성능 개선을 위해 for 문을 고쳐야 한다면 filterApplesByColor, filterApplesByWeight 등 전체 메서드들을 모두 수정해야 한다.

 

세 번째 시도: 동작 파라미터화를 이용한 추상적 조건으로 필터링

위 문제를 해결하기 위해 원하는 조건을 검사하는 코드를 필터링 메서드에 전달하도록 수정해보자.

원하는 조건을 검사해 참 또는 거짓을 반환하는 ApplePredicate 인터페이스를 정의하고 구현한다.
원하는 조건에 따라 여러 버전의 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());
    }
}

이와 같이 각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의한 후 런타임에 알고리즘을 선택하는 기법을 전략 디자인 패턴이라고 한다.
(알고리즘 패밀리: ApplePredicate, 전략: AppleHeavyWeightPredicate,AppleGreenColorPredicate)

 

filterApples에서 ApplePredicate 객체를 인수로 받아 사과의 조건을 검사하도록 메서드를 수정한다. 이렇게 메서드가 다양한 동작(또는 전략)을 받아서 수행할 수 있게 된다. filterApples 메서드 내부에서 사과 컬렉션을 반복하는 로직과 사과 각각에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻을 수 있다.

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();

    for (Apple apple: inventory) {
        // ApplePredicate 객체로 사과 검사 조건을 캡슐화 했다.
        if (p.test(apple)) {
            result.add(apple);
        }
    }

    return result;
}

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

filterApples에 전달하는 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다.
ApplePredicate 객체의 test 메서드만 전달하면 좋겠지만, 자바에서는 메서드는 인수로 받을 수 없어 ApplePredicate 객체로 감싸서 전달한다.

 

네 번째 시도: 익명 클래스(anonymous class)를 이용한 코드 간소화

ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의해 사용하는 작업은 상당히 번거로운 일이다. 클래스 선언과 인스턴스화를 동시에 수행할 수 있는 익명 클래스를 이용해 코드 양을 줄여보자.

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

익명 클래스를 사용해 클래스를 선언하는 부분을 조금 줄였지만 아직 코드가 장황하다.

 

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

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

 

여섯 번째 시도: 리스트 형식으로 추상화

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

 

전체 코드

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class FilteringApples {
    public static void main(String[] args) {
        List<Apple> inventory = Arrays.asList(
            new Apple(80, Color.GREEN),
            new Apple(155, Color.GREEN),
            new Apple(120, Color.RED),
        );

        List<Apple> greenApples = filterGreenApples(inventory);
        List<Apple> redApples = filterApplesByColor(inventory, Color.RED);

        List<Apple> greenApples2 = filter(inventory, new AppleColorPredicate());
        List<Apple> heavyApples = filter(inventory, new AppleHeavyWeightPredicate());
        List<Apple> redAndHeavyApples = filter(inventory, new AppleRedAndHeavyPredicate());

        List<Apple> redApple2 = filter(inventory, new ApplePredicate() {
            @Override
            public boolean test(Apple a) { return a.getColor() == Color.RED; }
        })
    }

    public static List<Apple> filterGreenApples(List<Apple> inventory) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (apple.getColor() == Color.GREEN) {
                result.add(apple);
            }
        }

        return result;
    }

    public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (apple.getColor() == color) {
                result.add(apple);
            }
        }

        return result;
    }

    public static List<Apple> filter(List<Apple> inventory, ApplePredicate p) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (p.test(apple)) {
                result.add(apple);
            }
        }

        return result;
    }

    enum Color {
        GREEN, RED
    }

    public static class Apple {
        private int weight = 0;
        private Color color;

        public Apple(int weight, Color color) {
            this.weight = weight;
            this.color = color;
        }

        public int getWeight() {
            return weight;
        }

        public void setWeight(int weight) {
            this.weight = weight;
        }

        public Color getColor() {
            return color;
        }

        public void setColor(Color color) {
            this.color = color;
        }
    }

    interface ApplePredicate {
        boolean test(Apple apple);
    }

    static class AppleHeavyWeightPredicate implements ApplePredicate {
        @Override
        public boolean test(Apple apple) {
            return apple.getWeight() > 150;
        }
    }

    static class AppleColorPredicate implements ApplePredicate {
        @Override
        public boolean test(Apple apple) {
            return apple.getColor() == Color.GREEN;
        }
    }

    static class AppleRedAndHeavyPredicate implements ApplePredicate {
        @Override
        public boolean test(Apple apple) {
            return apple.getColor() == Color.RED && apple.getColor() > 150;
        }
    }
}

출처

모던 자바 인 액션 - CHAPTER 2 동작 파라미터화 코드 전달하기