Home [모던 자바 인 액션 ch2] 동작 파라미터화 코드 전달하기
Post
Cancel

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

[Modern Java In Action] ch2. 동작 파라미터화 코드 전달하기

Modern Java In Action 공부를 한 후 정리한 내용입니다.

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

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드블록은 나중에 프로그램에서 호출한다. 즉, 코드불록의 실행은 나중으로 미뤄진다. 예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있고 결과적으로 코드블록에 따라 메서드의 동작이 파라미터화 되는 것이다.

필요성

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

1
2
3
4
5
6
7
8
9
10
11
enum Color { RED, GREEN }

public static List<Apple> filterGreenApple(List<Apple> inventory){
    List<Apple> result = new ArrauList<>();
    for(Apple apple : inventory){
        if(GREEN.equals(apple.getColoe())){
            result.add(apple);
        }
    }
    return result;
}

위의 예제는 녹색 사과를 선택하는데 필요한 조건을 가리킨다. 그런데 여기서 빨간 사과를 필터링 하고 싶다면 어떻게 해야만 할까? filterRedApple 메서드를 새로 만들고 if 문의 조건을 빨간 사과로 바꾸는 방법을 선택할 수 있다. 그러나 만약에 옅은 녹색, 어두운 빨간색 등 여러 색에 따라 필터링을 하고 싶다면, 계속해서 메서드를 만들어 주어야 하고 이는 OCP 설계 원칙을 위반하게 될 것이다. 즉 위의 코드는 변경에 대해 유연하게 대처하지 못한다.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
        }
    }

}
public static void main(String[] args){
    List<Apple> greenApple = filterApplesByColor(inventory, GREEN);
    List<Apple> redApple = filterApplesByColor(inventory, RED);
}

이런식으로 접근할 수 있다. 그런데 여기서 무게 정보를 파라미터로 추가한다면? 다음과 같이 될 것이다.

1
2
3
4
5
6
7
8
9
public static List<Apple> filterApplesByColor(List<Apple> inventory,Color color,int weight){
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory){
        if(apple.getWeight() >weight){
            result.add(apple);
        }
    }

}

즉 이 코드도 좋은 해결책이 될 수 없음을 알 수 있다. 왜냐하면 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링코드와 대부분 중복 되기 때문이다. 이는 소프트웨어 공학의 DRY원칙을 어기는 것이다.

동작 파라미터화

위에서 본 것처럼 파라미터를 추가하는 방법이 아니라 변화하는 요구사항에 좀 더 유연하게 대처할 수 있는 방법이 필요하다는 것을 느꼈다.

참 거짓을 반환하는 함수를 프레디케이트라도 한다. 선택 조건을 결정하는 인터페이스를 정의하자.
1
2
3
public interface ApplePredicate {
    boolean test(Apple apple);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
//무거운 사과만 선택
public class AppleHeavyWeightPredicate implements ApplePredicate{
    public boolean test(Apple apple){
        return apple.getWeight()>150;
    }
}
//녹색 사과만 선택
public class AppleGreenColorPredicate implements ApplePredicate{
    public boolean test(Apple apple){
        return GREEN.equals(apple.getColor());
    }
}

위의 코드를 Strategy Pattern 으로 변경하면, 다양한 전략을을 더 만들어 낼 수 있다. 위으 코드를 클래스 다이어그램으로 나타낸다면 다음과 같다.

ch2ModernJava

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

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

1
2
3
4
5
6
7
8
9
10
//프레디케이트 객체로 사과 검사 조건을 캡슐화 했다.
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를 만들어서 filter 메소드를 호출하면 된다.

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

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

컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.

복잡한 과정 간소화

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

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

익명 클래스 (Anonymous Class)

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

1
2
3
4
5
6
7
8
// filterApples 메서드의 동작을 직접 파라미터화 했다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate(){
    public boolean test(Apple apple){
        return RED.equals(apple.getColor());
    }

});

그렇지만 코드가 장황해진다. 장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐만 아니라 읽는 즐거움을 뺏는다.

람다 표현식 사용

자바 8의 람다 표현식을 사용해서 위 예제 코드를 다음과 같이 간결하게 만들 수 있다.

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

리스트 형식으로 추상화

1
2
3
4
5
6
7
8
9
10
11
12
public interface Predicate<T>{
    boolean 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;
}

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

1
2
List<Apple> redApples = filter(inventory, (Apple apple)-> RED.equals(apple.getColor()));
List<Integer> evenNumber = filter(numbers, (Integer i)->i%2==0);

Comparator로 정렬하기

컬렉션 정렬은 반복되는 작업이다. 요구사항이 쉽게 바뀔 수 있다. 자바 8의 List에는 sort 메서드가 포함되어 있는데, java.util.Comparator 객체를 이용해서 sort 동작을 파라미터화 할 수 있다.

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

위의 Comparator를 구현해서 sort 메서드의 동작을 다양화 할 수 있다. 예를 들어 익명 클래스를 이용해서 무게가 적은 순서로 목록에서 사과를 정렬할 수 있다.

1
2
3
4
5
inventory.sort(new Comparator<Apple>(){
    public int Compare(Apple o1, Apple o2){
        return o1.getWeight().compareTo(o2.getWeight());
    }
});

람다를 이용하면 다음처럼 간결하게 할 수 있다.

1
inventory.sort((Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight()));

Runnable로 코드 블록 실행하기

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

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

1
2
3
4
5
6
7
8
9
public interface Runnable{
    void run();
}

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

람다를 이용해서도 구현가능하다

1
Thread t = new Thread(()-> System.out.println("hello World"));
This post is licensed under CC BY 4.0 by the author.

[모던 자바 인 액션 ch1] 자바 8,9,10,11 : 무슨 일이 일어나고 있는가?

[자바의 정석] 14장 람다와 스트림(Lambda & Stream)