[모던 자바 인 액션] 2장. 동작 파라미터화 코드 전달하기

June·2022년 3월 5일
0

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록은 나중에 프로그램에서 호출한다. 예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달하고, 코드 블록에 따라 메서드의 동작이 파라미터화된다. 동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

필요성 예시

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

녹색 사과만 필터링하는 함수다. 만약 요구 조건이 바뀌어 빨간 사과를 필터링하고 싶으면 if문 안의 조건을 바꿔야한다. 물론 색을 파라미터화 할 수도 있지만, 색이 아닌 무게도 기준으로 추가하고 싶다면 무게 파라미터도 추가해야한다. 이런식으로 요구사항이 변경하면 위의 코드는 변경에 유연하게 대처하지 못한다.

동작 파라미터화

참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다.
선택 조건을 결정하는 인터페이스를 정의하자.

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

``` java
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test (Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

조건에 따라 filter가 다르게 동작하는데 이를 전략 디자인 패턴이라 한다.

각 전략을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 알고리즘을 선택하는 것이다.

전략 디자인패턴 예시 및 정리

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

filterApples 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작(예제에서 predicate)를 분리할 수 있다는 점에서 큰 이점이다.

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 메서드로 전달할 수 있다.

List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());

메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다. test 메서드를 구현하는 객체를 이용해서 불리언 표현식 등을 전달할 수 있으므로, 이는 '코드를 전달'히는 것과 다름 없다.

복잡한 과정 간소화

현재는 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클르새를 정의한 다음 인스턴스화해야 한다.

자바는 클래스 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다.

익명 클래스

익명 클래스는 말 그래도 이름이 없는 클래스다. 클래스 선언과 인스턴스화를 동시에 할 수 있다.

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

하지만 익명클래스는 부족하다. 코드가 장황해진다.

람다 표현식 사용

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

리스트 형식으로 추상화

public interface Predicate<T> {
    booelan 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;
}

이제 사과가 아니더라도 바나나, 오렌지, 정수, 문자열 등의 리스트에도 필터 메서드를 적용할 수 있다.

실전 예제

Comparator로 정렬하기

컬렉션 정렬은 반복되는 작업이다. 요구사항이 쉽게 바뀔 수 있다.

자바 8의 List에는 sort 메서드가 포함되어 있는데, java.util.Comparator 객체를 이용해서 sort 동작을 파라미터화 할 수 있다.

public interface Comparator<T> {
    int compare(T o1, T o2);
}

아래와 같이 익명클래스를 만들어서 전달할 수 있다.

inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

아래처럼 람다 표현식을 이용할 수도 있다.

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 어떤 코드를 실행할 것인지 스레드에게 알려줄 수 있다. 자바 8까지는 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않는 void run 메소드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 것이 일반적인 방법이었다.

자바에서는 Runnable 인터페이스를 이용해서 실행할 코드 블록을 지정할 수 있다.

public interface Runnable {
    void run();
}
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello World");
});

람다 표현식을 이용해서도 구현가능하다.

Thread t = new Thread(() -> System.out.println("Hello world"));

2개의 댓글

comment-user-thumbnail
2023년 7월 25일

이해하기 쉽지 않군요...

1개의 답글