본문 바로가기
Book/모던 자바 인 액션

[정리] 모던 자바 인 액션 - 동작 파라미터화 (Behavior Parameterization)

by 따라쟁이개발자 2023. 1. 3.

동작 파라미터화(Behavior Parameterization 또는 행위 매개변수)란

아직 어떻게 실행할 것인지 결정하지 않은 코드블록을 의미한다.

이 코드블록은 나중에 프로그램에서 호출하며 즉, 코드 블록 실행은 나중으로 미뤄진다.

코드 블록에 메서드의 동작이 파라미터화 된다.

 

동작을 파라미터화 하므로서 변화하는 요구사항에 보다 효율적으로 대응 가능해진다.

다음은 예제와 그 과정이다.

 

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

녹색 사과를 필터링 해달라는 요청이 있어 아래와 같이 구현하였다.

기능은 동작하지만 아래 로직으로는 오로지 녹색 사과만 필터링할 수 있다.

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

 

 

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

빨간 사과도 필터링 되게 해달라는 추가 요청이 왔다.

filterGreenApples를 복사해서 filterRedApples를 추가할까 했지만,

좀 더 유연하게 대응하기 위해 색상을 파라미터화 하여 다른 색상도 적용 가능하게 만들었다.

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>();         // 사과 누적 리스트
    for (Apple apple : inventory) {
        if (color.equals(apple.getColor())) {       // color를 파라미터로 받아와 비교
            result.add(apple);
        }
    }
    return result;
}

 

색상 조건을 완료하였는데 색 이외에 무거운 사과도 필터링해달라는 요청이 들어왔다.

filterApplesByColor을 활용하여 filterApplesByWeight도 만들었다. 역시 무게값을 파라미터화 하였다.

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weigth) {
    List<Apple> result = new ArrayList<>();         // 사과 누적 리스트
    for (Apple apple : inventory) {
        if (apple.getWeight() > weigth) {       // 무게를 파라미터로 받아와 비교
            result.add(apple);
        }
    }
    return result;
}

 

좋은 해결책인듯 했지만, 두 메서드가 if조건 외에는 대부분 중복된다.

이는 소프트웨어 공학의 DRY(Don't Repeat Yourself, 같은 것을 반복하지 말 것) 원칙을 어기는 것이다.

 

 

세 번째 시도 : 가능한 모든 속성으로 필터링

중복 제거를 위해 두 메소드를 합치기로 했다.

무게, 색상을 모두 파라미터로 받아오고 추가로 무게 조건과 색상 조건을 구분하기 위해 Flag라는 파라미터도 받아오게 만들었다.

public static List<Apple> filterApples(List<Apple> inventory, Color color,
                                        int weigth, boolean flag) {
    List<Apple> result = new ArrayList<>(); 
    for (Apple apple : inventory) {
        if ((flag && color.equals(apple.getColor())) ||  // flag true면 색상 비교
            (!flag && apple.getWeight() > weigth)) {     // flag false면 무게 비교
            result.add(apple);
        }
    }
    return result;
}

 

중복은 제거 되었지만 호출하는 부분이 형편없게 되었다.

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, null, 150, false);

true, false가 어떤 역할을 하는지 파악하기 어렵다. 그리고 다른 조건이 더 들어오면 유연하게 대응할 수도 없는 코드다.

 

 

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

우리가 필요한 건 필터링 조건에 대한 true/false 결과였다.

조건을 파라미터화 하기엔 파라미터가 너무 많아지니, true/false를 반환하는 동작을 파라미터화 하기로 하였다.

 

우선, true/false를 반환하는 Predicate 함수를 Interface 로 정의하기로 했다.

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

그리고 여러 버전의 ApplePredicate 구현체를 만들었다.

이제 filter 메서드는 실행되는 조건에 따라 다르게 동작할 수 있게 되었다.

public class AppleGreenColorPredicate implements ApplePredicate {
    @Override
    public boolean filter(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

 

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

이와 같은 패턴을 전략 디자인 패턴(Strategy Design Pattern) 이라고 한다.

전략 디자인 패턴은 각 알고리즘(=전략)을 캡슐화하는 알고리즘 패밀리를 정의해둔 후, 런타임에 알고리즘을 선택하는 기법이다.

ApplePredicate이 알고리즘 패밀리이고, AppleGreenColorPredicate 와 AppleHeavyWeightPredicate이 전략이다.

 

filterApples 메서드는 ApplePredicate 객체를 인수로 받게 고치고, 상황에 따라 다양한 조건을 필터링할 수 있게 만들었다.

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>(); 
    for (Apple apple : inventory) {
        if (p.filter(apple)) {      // Predicate 객체로 사과 필터링 조건을 캡슐화
            result.add(apple);
        }
    }
    return result;
}

동작을 파라미터로 받아와 유연한 대응이 가능해지긴 했지만,

조건이 추가될 때마다 filter 메서드를 감싸고 있는 ApplePredicate 객체가 계속 추가되어야 했다.

 

 

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

익명클래스란 말 그대로 이름없는 클래스이다. 

익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에서 필요한 구현을 만들 수 있다.

 

새로운 요구사항인 빨간색 사과 필터링 조건 익명클래스를 사용해보기로 했다.

List<Apple> redApples = filterApples(inventory, new ApplePredicate() { // 메서드 동작을 직접 파라미터화
            @Override
            public boolean filter(Apple apple) {
                return RED.equals(apple.getColor());    // 빨간색 사과 필터링
            }
        });

하지만, 익명 클래스로도 아직 부족한 점이 있다.

다른 조건도 다 익명클래스로 구현하고 나니 filter의 return문 외에 다른 코드들이 대부분 중복된다.

 

 

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

람다 표현식을 사용하여 익명 클래스를 좀 더 간단하게 재구현하였다.

List<Apple> greenApples = filterApples(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));
List<Apple> heavyApples = filterApples(inventory, (Apple apple) -> apple.getWeight() > 150);
List<Apple> redApples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

이젠 중복없이 간결한 코드로 다양한 조건을 처리할 수 있게 되었다!

 

 

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

조금 더 나아가서, 제네릭을 이용해 Apple 외에 다른 객체들도 필터링할 수 있게 만들어보자

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

 

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

 

List<Apple> greenApples = filter(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i%2 == 0);

 

 

마치며

  • 동작 파라미터화에서는 다양한 동작 수행을 할 수 있도록 메서드를 인수로 전달한다.
  • 동작 파라미터화를 사용하면 요구사항에 유연하게 대응 가능하고, 나중에 드는 엔지리어링 비용도 줄일 수 있다.
  • 람다와 같은 코드 전달 기법을 이용하면 동작을 메서드 인수로 전달할 수있다. (여러 클래스를 구현하는 수고로움을 덜 수 있다.)
  • 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.

댓글