[모던 자바 인 액션] 2장 - 동작 파라미터화

변화하는 요구사항에 대응하기 좋은 코드

밑의 상황들을 통해 변화하는 요구사항에 대해서 대응하기 위해, 어떤 식으로 코드를 바꾸는 것이 좋은 지 알아보자.

상황 1. 녹색 사과만 필터링 하는 기능 구현하기

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

상황 2. 빨간 사과도 필터링 할 수 있도록 기능 변경

→ 바뀌는 부분을 파라미터로 추출

녹색, 빨간색 이외에도 여러가지 색깔로 바뀔 수 있다는 것을 감안해서, 필터링할 색깔을 파라미터로 받기로 한다. 이로 인해 녹색, 빨간색 이외의 다른 색깔에 대한 요구사항이 들어오더라도 쉽게 기능을 추가할 수 있게 된다.

아직까지 깔끔한 코드!

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;
}
// 녹색 사과를 필터링 하고 싶은 경우
filterApplesByColor(inventory, GREEN);

// 빨간 사과를 필터링 하고 싶은 경우
filterApplesByColor(inventory, RED);

상황 3. 무게로도 필터링을 할 수 있도록 기능 추가

→ 전략패턴을 활용 + Stream 활용

무게로도 필터링을 할 수 있는 기능을 요청받자 밑과 같은 생각들이 떠오른다.

  • 하나의 메소드로 ‘색깔’과 ‘무게’를 선택해서 필터링 할 수 있는 기능을 개발하면 좋을 텐데…
  • ‘무게’도 색깔과 비슷하게 어떤 값을 기준으로 필터링할 지 파라미터로 값을 받으면 좋을 텐데…

3.1) 비효율적인 코드!

public static List<Apple> filterApple(
    // 인자로 color, weight를 받음으로써 어떤 색깔이든 어떤 무게든 필터링을 자유자재로 할 수 있도록 구현
    // 인자로 falg를 받음으로써 색깔로 필터링을 할지, 무게로 필터링을 할 지 결정 가능
    List<Apple> inventory, Color color, int weight, boolean flag) {
    
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        // flag가 true일 경우, 색깔 필터링만 적용
        if (flag) {
            if (apple.getColor().equals(color)) {
                result.add(apple);
            }
        } 
        
        // flag가 false일 경우, 무게 필터링만 적용
        if (!flag) {
            if (apple.getWeight() > weight) {
                result.add(apple);
            }
        }
    }
}

위의 코드는 앞으로 사과의 크기, 모양, 생성지역 등으로 사과를 필터링한다는 추가적인 요구사항에 유연하게 대응할 수가 없다. 즉, 유지보수가 어려운 코드이다.

3.2.) 개선한 코드!

전략 패턴을 사용해서 위의 비효율적인 코드를 개선해보자.

AppleFilter 인터페이스 구현을 통해, Filter를 추상화시켜 다형성을 활용할 수 있도록 코드 구성

public interface AppleFilter {
    boolean test(Apple apple);
}
// 무거운 사과만 필터링
public class AppleHeavyWeightFilter implements AppleFilter {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

// 녹색 사과만 필터링
public class AppleGreenColorFilter implements AppleFilter {
    @Override
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

AppleFilter를 파라미터로 받는 메서드 생성

public static List<Apple> filterApples(List<Apple> inventory, AppleFilter filter) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (filter.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

추상화된 AppleFilter를 파라미터로 받음으로써, 원하는 Filter(AppleHeavyWeightFilter, AppleGreenColorFilter)를 골라서 파라미터로 전달할 수 있게 되었다.

→ 필터링하는 로직(동작, Behavior)을 파라미터로 받을 수 있도록 만들었다고 해서, 동작 파라미터화(Behavior Parameterization)라는 말을 썼다.

3.3) Stream을 활용해 한 번 더 개선한 코드!

AppleHeavyWeightFilter 클래스를 생성하지 않고, 다음과 같이 Stream을 활용해서 filterApples()를 사용할 수 있다.

filterApples(inventory, (Apple apple) -> apple.getWeight() > 150);

AppleGreenColorFilter 클래스를 생성하지 않고, 다음과 같이 Stream을 활용해서 filterApples()를 사용할 수 있다.

filterApples(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));

정리

  • Stream을 활용함으로써 AppleFilter에 대한 자식 클래스(AppleHeavyWeightFilter, AppleGreenColorFilter)를 생성하지 않아도 되서 전체 코드가 간결해졌다.
  • filterApples를 사용할 때, AppleFilter라는 객체를 생성해서 파라미터로 넣을 필요 없이 바로 람다식을 넣을 수 있어서 코드가 생성할 필요가 없어졌다.

상황 4. 사과 뿐만 아니라 오렌지에 대해서 필터링 기능 추가

→ **Predicate 사용, 제네릭 타입을 T로 추상화**

기존의 코드

public static List<Apple> filterApples(List<Apple> inventory, AppleFilter filter) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (filter.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

개선한 코드

import java.util.function.Predicate;

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

제네릭 타입을 T로 추상화함으로써 사과 뿐만 아니라 다양한 타입에 대해서 필터링이 가능하게 되었다. 즉, 여러 요구사항의 변화에 유연하게 대응할 수 있는 코드로 개선할 수 있게 되었다.

filterFruits(inventory, (Apple apple) -> RED.equals(apple.getColor()));
filterFruits(inventory, (Orange orange) -> ORANGE.equals(orange.getColor()));