8 min read

모던 자바 인 액션 - Chapter 2 <동작 파리미터화>

동작 파라미터화 코드 전달하기

책에서 해당 챕터의 서론으로 얘기하는것은 어떻게 시시각각 변하는 사용자 요구 사항에 효율적으로 대응할 수 있을까이다.효율적이다라는 것은 크게 두가지로 나뉜다.우선 엔지니어링적인 비용이 최소인점이고 나머지는 새로 추가할 기능의 구현과 유지보수가 쉬워야 한다는 것이다.

해당 챕터의 제목인 동작 파라미터화를 이용하면 자꾸 바뀌는 요구사항에 효율적으로 대응할 수 있다. 동작 파라미터화라는 것은 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.즉, 해당 코드 블록은 미래의 어느 시점에 실행된다.예를 들어 작성된 메서드의 인자로 코드 블록을 전달할 수 있고, 이는 곧 전달되는 코드 블럭에 의해 메서드의 동작이 파라미터화 된다는 것이다. 조금 더 구체적인 예는 다음과 같다.

  1. 리스트의 모든 요소에 대해서 ‘어떤 동작’ 수행할 수 있음
  2. 리스트 관련 작업을 끝낸 다음 ‘어떤 다른 동작’을 수행할 수 있음
  3. 에러가 발생하면 ‘정해진 다른 동작’을 수행할 수 있음

기존의 자바에서의 동작 파라미터화의 예시는 스레드로 어떠한 동작을 수행하는 코드 블럭을 실행하는 경우이다.

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

아래의 순차적인 예제는 요구사항을 반영하며 유연한 코드를 만드는 예시이다.예시 상황은 기존의 농장 재고목록을 관리하는 서비스에서 리스트에 녹색 사과만 필터링하는 기능을 추가한다고 가정하겠다.

  1. 첫번째 시도 : 녹색 사과 필터링
    /*
    * 1. Filter green apples
    * */
    public static List<Apple> filterGreenApples(List<Apple> inventory) {
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory) {
            if(Color.GREEN.equals(apple.getColor())) {
                result.add(apple);
            }
        }
        return result;
    }

위 코드의 if문을 통해 녹색 사과를 필터링한다.만약 빨간 사과도 필터링하는 요구사항이 들어온다면 어떻게 고쳐야 할까? 가장 쉬운 방법으로는 filterRedApples라는 메서드를 하나 더 만들면 된다.하지만 이러한 방식은 색깔이 여러개가 들어올 경우 확장하기에 굉장히 불편하다.다음의 규칙은 보고 다음 단계로 넘어가자.

  • 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.
  1. 두번째 시도 : 색을 파라미터화
    /*
    * 2. Parameterize the color
    * */
    public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory) {
            if(color.equals(apple.getColor())) {
                result.add(apple);
            }
        }
        return result;
    }

이번에는 color 필드를 파라미터화시켜 좀 더 유연한 코드를 만들어보았다.만약 여기서 사과의 무게가 150g 이상이도록 필터링이 가능하도록 만들어 달라는 요구 사항이 들어왔다고 생각해보자.그래서 아래와 같은 메서드를 추가해 요구사항을 반영했다.

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

하지만 누가봐도 중복되는 코드가 사용되고 있다.그래서 이러한 중복을 없애기 위해 두 가지 메서드를 flag라는 인수를 추가로 둬 합쳐보자.

  1. 세 번째 시도 : 가능한 모든 속성으로 필터링
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 && color.equals(apple.getColor())) || (!flag && apple.getWeight() > weight)) {
                result.add(apple);
            }
        }
        return result;
    }

위 메서드는 color로 필터링을 할 경우 flag를 true를 주고, 무게로 필터링을 할 경우는 flag를 false를 줘야하는 코드이다.심지어 앞으로 요구 사항이 변경될 경우에 유연하게 대응하지 못한다.다음 단계로는 동작 파라미터화를 이용해서 코드에 유연성을 부여해보자.

동작 파라미터화

우리가 사과를 필터링하는 조건을 잘 생각해보자.사과의 어떤 속성에 기초해서 불리언값을 반환하는 방법이 존재한다.참 또는 거짓을 반환하는 함수를 predecate라고 한다.이와 같이 선택 조건을 결정하는 인터페이스를 만들자.

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

그리고 위 인터페이스를 구현하는 클래스 두가지를 아래와 같이 만들자.즉 아래의 구현체들을 통해 동작 파라미터화를 구현할 수 있다.

  • AppleGreenColorPredicate
  • AppleHeavyWeightPredicate

위와 같이 구현할 경우 전략 패턴임을 확인할 수 있다.이는 런타임에 실제로 실행될 구현체를 선택하는 기법이다.즉, 다양한 전략(동작)을 받아서 내부적으로 다양한 동작을 수행하도록 하여 동작 파리미터화를 구현하였다.

이를 기반으로 기존의 filter함수를 변경해보자.

  1. 네 번째 시도 : 추상적 조건으로 필터링
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory) {
            if(predicate.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }

앞선 코드들에 비해 훨씬 더 유연한 코드를 얻었으며 동시에 사용하기도 편해졌다.이제 필요한만큼 ApplePredicate 구현체를 만들어서 filterApples() 메서드에 전달할 수 있다.

test() 메서드의 동작을 다양하게 만들기 위해 이를 ApplePredicate로 감싸고 구현한 다음 이를 전달한다.직접 함수를 전달하는 것은 아니지만 이는 곧 코드를 전달하는 것이라고 할 수 있다.3장에서는 람다를 통해 조금 더 간결하게 동작을 전달해 볼 예정이다.

위 그림을 참고해보면 한개의 메서드가 다른 동작을 수행하도록 재활용할 수 있다.따라서 유연한 설계에는 동작 파라미터화가 중요한 역할을 수행한다.

복잡한 과정 간소화

지금까지의 방법으로는 동작 파라미터를 하기 위해 매번 새로운 구현체를 만들어야하는 단점이 존재한다.지금부터는 이를 익명 클래스를 통해 간소화 시켜보자.

익명 클래스는 즉석에서 필요한 구현을 만들어 사용할 수 있다.

즉,직접 구체 클래스를 생성하지 않고 다음과 같이 동작을 전달할 수 있다.

  1. 익명 함수 사용
@Test
    void anonymousClass() {
        List<Apple> inventory = new ArrayList<>();
        filterApples(inventory, new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return Color.RED.equals(apple.getColor());
            }
        });
    }

위 코드의 장황함을 더 개선시키기 위해 람다를 사용할 수 있다.

  1. 람다 사용
@Test
    void lambda() {
        List<Apple> inventory = new ArrayList<>();
        filterApples(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));
    }

훨씬 더 간결해진것을 확인할 수 있다.

  1. 리스트 형식으로 추상화

쉽게 말해 Apple 객체뿐만 아니라 다양한 객체에 필터 메서드를 사용하기 위해 제네릭을 사용하였다.

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

실전 예제

간단한 실전 예제들이다.

Comparator로 정렬하기

List의 내장 함수인 sort()는 인자로 Comparator를 받고 있다.이를 다음과 같이 익명함수로 구현하여 sort()를 커스텀 할 수 있다.

@Test
    void comparatorTest() {
        List<Apple> inventory = new ArrayList<>();
        inventory.sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight().compareTo(o2.getWeight());
            }
        });
    }

Callable을 결과로 반환하기

Runnable과 달리 Callable은 결과를 Future 객체에 저장할 수 있다.이를 간단하게 아래와 같이 구현 할 수 있다.책의 내용에 따르면 Runnable과 Callable은 추후 챕터에서도 자세히 다룬다고 한다.

@Test
    void callableTest() {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> result = executorService.submit(() -> "Hello");
    }

이번 챕터를 정리해보면 실행하고자 하는 코드를 전달하여 동작 파라미터화를 구현할 수 있었다.동작 파라미터화가 무엇인지 그리고 왜 사용되는지를 이해한다면 이번 챕터를 잘 읽었다고 볼 수 있을것 같다.