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

728x90

모던 자바 인 액션 책을 작년 이맘때쯤에 우형 없는 우테코에서 진행했었는데, 아이러니하게도 우형 있는 우테코(라임을 맞추고 싶은 욕구가 들끓었다)에서 다시 진행하고있다. 스터디를 할지말지 고민을 했는데 다시 공부하게 된 원인은 아무래도 처음 공부할 때 어려운 챕터들은 스킵했었고, 이해가 안됨에도 억지로 읽고 지나갔던 내용들도 꽤 많았기 때문에 이번 2회차는 확실하게 이해하고 넘어가겠다는 전제를 깔고 스터디에 임하려고한다.

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

시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까?

  • 엔지니어링적인 비용이 가장 최소화되어야 한다.
  • 새로 추가한 기능은 쉽게 구현할 수 있어야 한다.
  • 장기적인 관점에서 유지보수가 쉬어야 한다.

동작 파라미터화(behavior parameterization)를 이용하면 이렇게 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

동작 파라미터화란?

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.

이 코드 블록은 나중에 프로그램에서 호출한다. 결과적으로 코드 블럭에따라 메서드의 동작이 파라미터화된다.

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

먼저 전제로 사과 색을 정의하는 Color enum class가 존재한다고 가정한다.

enum Color { RED, GREEN }

녹색 사과 필터링

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor() == GREEN) {
            result.add(apple);
        }
    }
    return result;
}

위의 코드는 inventory에서 녹색 사과만 추출한 결과를 얻는다.
이 코드를 농부의 변심으로 빨간 사과도 필터링한다면, 어떻게 고쳐야 할까? 빨간 사과에 대한 if 조건문을 추가하여 쉽게 구현할 수 있을 것이다. 하지만 이와 같은 방법으로 당장의 원하는 결과는 얻을 수 있겠지만, 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절하게 대응할 수 없다. 이런 경우 코드를 추상화해야 한다.

색을 파라미터화

색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있다.

public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor() == color) {
            result.add(apple);
        }
    }
    return result;
}

분명 이전보다 유연한 코드가 됐다는 것은 확실하다. 하지만 여기서 색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있도록 조건을 부여하려고 한다면, Color에 이어서 weight에 대한 정보또한 메서드 파라미터로 추가해야 할 것이다.

나쁜 방법은 아니지만 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다. 반복이 발생하고 있기 때문에 이는 지양하는 것이 좋다는 것을 본능적으로(?) 알고 있을 것이다. 이를 개선하기 위해서는 한 줄이 아니라 메서드 전체 구현을 고쳐야 한다.

가능한 모든 속성으로 필터링

다음은 모든 속성을 메서드 파라미터로 추가한 모습이다.

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 && apple.getColor().equals(color)) ||
            (!flag && apple.getWeight() > weight)) {
            result.add(apple);
        }
    }
    return result;
}

위의 코드는 최악의 코드이다. 일단 flag의 의미가 불명확하다. 또한 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.
물론 문제가 잘 정의되어 있는 상황에서는 이 방법이 동작할 수도 있다. 하지만 filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있으면 더 좋을 것이다.

동작 파라미터화

앞의 내용을 통해서 파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 필요하다는 것을 확인했다.
우리의 선택 조건을 다음처럼 결정할 수 있다. 사과의 어떤 속성에 기초해서 불리언값을 반환하는 방법이 있다. 선택 조건을 결정하는 인터페이스를 정의하자.

참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다.

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

다음 예제처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

무거운 사과만 선택하는 Predicate

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

녹색 사과만 선택하는 Pedicate

public class AppleGreenPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getColor() == GREEN;
    }
}

위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 패턴(Strategy Pattern)이라고 부른다. 즉 ApplePredicate는 사과 선택 전략을 캡슐화 한 것이다.

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

그렇다면 ApplePredicate는 어떻게 다양한 동작을 수행할 수 있을까? filterApples에서 ApplePredicate 객체를 받아 애플의 조건을 검사하도록 메서드를 고쳐야 한다. 이렇게 동작 파라미터화, 즉 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고친다. 이렇게 하면 filterApples 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.

추상적 조건으로 필터링

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

코드/동작 전달하기

처음 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌고 심지어 사용하기도 쉬워졌다. 필요한대로 다양한 ApplePredicate를 만들어서 filterApples 메서드로 전달할 수 있다. 유연성을 마음껏 누리자!

우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 즉, 우리는 filterApples 메서드의 동작을 파라미터화한 것이다. 가장 중요한 구현은 test 메서드다. filterApples 메서드의 새로운 동작을 정의하는 것이 test 메서드다. 안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다. test 메서드를 구현하는 객체를 이용해서 불리언 표현식 등을 전달할 수 있으므로 이는 코드를 전달할 수 있는 것이다 다름없다.

한 개의 파라미터, 다양한 동작

지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 따라서 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다. 따라서 유용한 API를 만들 때 동작 파라미터화가 중요한 역할을 한다. 동작을 추상화해서 변화하는 요구사항에 대응할 수 있는 코드를 구현하는 방법을 배웠다. 하지만 여러 클래스를 구현해서 인스턴스화하는 과정이 조금은 거추장스럽게 느껴진다. 이 부분을 개선할 방법을 다음 절에서 제시한다.

복잡한 과정 간소화

지금 상태는 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화해야 하는데 이는 상당히 번거롭다. 게다가 이는 로직과 관련 없는 코드가 많이 추가되게된다.

이는 익명 클래스(anonymous class)라는 기법을 통해 개선할 수 있다. 익명 클래스를 이용하면 코드의 양을 줄일 수 있다. 하지만 익명 클래스가 모든 것을 해결하는 것은 아니다. 이는 람다 표현식으로 더 가독성 있는 코드를 구현할 수도 있다.

익명 클래스

익명 클래스는 자바의 지역 클래스(local class)와 비슷한 개념이다. 익명 클래스는 말 그대로 이름이 없는 클래스다. 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에 필요한 구현을 만들어서 사용할 수 있다.

익명 클래스 사용

익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현한 코드이다.

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

익명 클래스도 부족한 점이 있다. 1) 익명 클래스는 여전히 많은 공간을 차지한다. 2) 많은 프로그래머가 익명 클래스의 사용에 익숙치 않다.

코드의 장황함은 나쁜 특성이다. 장황한 코드는 구현하고 유지보수하는 데 시간이 오래걸릴 뿐 아니라 읽는 즐거움을 빼앗는 요소로, 개발자로부터 외면받는다. 한눈에 이해할 수 있어야 좋은 코드다. 익명 클래스로 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금줄일 수 있지만 여전히 만족스럽지 않다. 보는 것처럼 동작 파라미터화를 이용하면 요구사항 변화에 더 유연하게 대응할 수 있으므로 모든 프로그래머가 동작 파라미터화를 사용하도록 권장한다. 람다 표현식이라는 더 간단한 코드 전달 기법을 도입하면 코드의 장황함을 없애면서도 동작 파라미터화의 장점을 활용할 수 있다.

람다 표현식의 사용

자바 8의 람다 표현식을 이용해서 위 예제 코드를 다음처럼 간단하게 재구현할 수 있다.

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

이전 코드보다 훨씬 간단해졌고, 문제를 더 잘 설명하는 코드가 되었다.

리스트 형식으로 추상화

public interface Predicate<T> {
    boolean text(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) -> apple.getColor() == RED);

List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

이렇게 해서 유연성관 간결함이라는 두 마리 토끼를 모두 잡을 수 있다!

교재에서는 동작 파라미터의 예제로 Comparator, Runnable, Collable(생소한데.. GUI 이벤트 처리시에 사용할 수 있는 것 같다), EventHandler 등을 예로들고 있는데 이 코드의 javadoc을 보면 아래와 같다.

Comparator

image

Runnable

image

Collable

image

EventHandler

image

모두 @FunctionalInterface 라는 애너테이션이 붙은 것을 확인할 수 있다. 이 언급을 함으로써 이미 감을 잡은 사람도 있겠지만, 람다식은 FunctionalInterface 즉 함수형 인터페이스인 경우에 사용이 가능하다.

함수형 인터페이스단 1개의 추상 메서드를 가지는 인터페이스를 칭한다. 애너테이션은 이 인터페이스가 함수형 인터페이스다 라는 주석 정도로 생각하면 된다(사용하면 간지력도 올라간다😎). 단순히 주석만의 기능을 가지지는 않는다. 해당 인터페이스가 함수형 인터페이스가 아니라면 아래와 같이 컴파일 오류가 발생한다!

함수형인터페이스가 아닌 곳에 @FunctionalInterface 애너테이션이 있으면 컴파일 오류가 발생한다.

image

추상메서드를 한 개로 변경했더니 컴파일 오류가 사라진다.

image

@FunctionalInteface javadoc

image

728x90