모던 자바 인 액션 2장
모던 자바 인 액션 2장 : 동작 파라미터화 코드 전달하기
2.1 변화하는 요구사항에 대응하기
작업을 하다보면 요구사항이 변하는건 불가피하다.
2.1장에서는 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링하는 예제 코드를 먼저 보여주며 점차 개선해나가는 식으로 어떻게 변화하는 요구사항에 대응하는지 보여준다.
2.1.1 첫 번째 시도 : 녹색 사과 필터링
사과 색을 정의하는 다음과같은 Color num이 존재한다고 가정하자.
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);
}
}
}
그러나 갑자기 농부가 녹색사과 말고 빨간 사과도 필터링하고 싶다고한다.
- 메서드를 복사하여 filterRedApples 라는 메서드를 만들면 될까?
- 그러다 다른색도 추가하면 같은부분이 많고 색깔만 비교하는 부분의 코드만 다른, 즉 코드 한줄만 다르고 나머지는 다 같은 메서드가 여러개 존재하게 될 것이다.
비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.
2.1.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);
}
}
}
이런식으로 구현하면 다음처럼 구현한 메소드를 호출하면 된다
List<Apple> greenApples = filterApplesByColor(inventory, GREEN);
List<Apple> redApples = filterApplesByColor(inventory, RED);
이제 무게에 대한 필터링도 요구한다면 위와같이 파라미터로 받으면 될것같다.
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);
}
}
}
그러나, 무게를 필터링하는 부분을 제외하면 색을 필터링하는 코드와 대부분이 중복되는 것을 볼 수 있다.
만약 필터링하는 부분을 제외하고 탐색하는 부분의 로직을 개선하기위해 수정하면 어떻게될까? 아마 모든 메서드를 수정해야할 것이다.
이는 엔지니어링 적으로 비싼 대가를 치러야 한다.
2.1.3 세 번째 시도 : 가능한 모든 속성으로 필터링
이번에는 무게, 색깔 등의 가능한모든 속성을 메서드 파라미터로 추가하였다.
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);
}
}
}
그리고 다음처럼 위 메서드를 사용할 수 있다.
List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> redApples = filterApples(inventory, null, 150, false);
파라미터를 보고 어떤기준으로 필터링하는지 알 수가없다.또한 나중에 요구사항이 더 추가되면 거대한 필터메서드를 구현해야한다.
filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있으면 좋을것같다. 다음절에서는 동작 파라미터화를 이용해서 유연성을 얻는 방법을 알려준다고 한다.
2.2 동작 파라미터화
앞절에서 파라미터로 변화하는요소를 때려박는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법의 필요성에 대하여 알아봤다.
첫번째로 나오는 개념은 프레디케이트 이다.
프레디케이트는 선택 조건을 결정하는 인터페이스이다.
public interface ApplePredicate{
boolean test(Apple apple);
}
프레디케이트는 Apple을 파라미터로 받아 boolean값을 리턴한다.
우리는 apple의 선택 조건을 test라는 메소드로 구현하여 true, false로 구분짓는건가보다.
바로 다음 코드로 프레디케이트를 구현하는 예제를 보여준다.
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150; // 150 무게 이상의 사과만 선택
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return GREEN.equals(apple.getColor()); // 초록 사과만 선택
}
}
ApplePredicate 인터페이스는 filter메서드의 세부 구현내용을 캡슐화하여 filter메서드를 사용하는 구현체에 따라 다르게 동작할 것이라고 예상할 수 있다.
이를 전략 디자인 패턴이라고 한다고 한다.
전략 패턴
각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법
여기서는 ApplePredicate가 알고리즘 패밀리고 이의 구현체가 전략이다.
이제 filterApples 메서드에서는 ApplePredicate 객체를 받아 다양한 동작을 수행할 수 있다.
다음 예제에서 ApplePredicate객체를 인수로 받도록 고쳐보자. 이렇게하면 filterApples 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다.
2.2.1 네 번째 시도 : 추상적 조건으로 필터링
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);
}
}
}
이제 필터링 조건을 수정하려면 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다. 이제 Apple의 속성과 관련한 모든 변화에 대응할 수 있는 유연한 코드를 준비한 것이다.
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return RED.equals(apple.getColor()) && apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples =
filterApples(inventory, new AppleRedAndHeavyPredicate());
우리가 전달한 ApplePredicate 객체로 인해 filterApples 메서드의 동작을 파라미터화 한것이다.
한 개의 파라미터, 다양한 동작
지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 따라서 아래 그림에서 보여주는 것처럼 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다.

지금까지 동작을 추상화해서 변화하는 요구사항에 대응할 수 있는 코드를 구현하는 방법을 살펴보았다. 하지만 여러 클래스를 구현해서 인스턴스화 하는 과정은 조금 번거롭게 느껴질 수 있다. 이 부분을 다음 절에서 어떻게 개선할지 살펴보자.
2.3 복잡한 과정 간소화
앞서 살펴본대로 ApplePredicate를 파라미터로 받는 부분에 우리는 자기입맛대로 구현체 클래스를 정의한 다음 인스턴스화 해서 넘겨줘야한다.
이는 상당히 번거로우며 시간낭비이다.
자바는 클래스와 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다.
2.3.1 다섯 번째 시도 : 익명 클래스 사용
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple){
return RED.equals(apple.getColor());
}
});
이 책에서는 익명 클래스로도 부족한점 2가지를 말한다.
-
익명클래스는 여전히 많은 공간을 차지

-
많은 프로그래머가 익명클래스에 익숙치 않다.
위 사진은 책에서 제공해주는 퀴즈이다.
코드 내부에서 this는 Runnable을 참조하기 때문에 정답은 5이다.
코드의 장황함은 나쁜 특성이다. 따라서 다음 절에서는 드디어 람다 표현식을 이용해서 어떻게 코드를 간결하게 정리할 수 있는지 간단히 살펴본다.
2.3.2 여섯 번째 시도 : 람다 표현식 사용
자바 8의 람다 표현식을 이용해서 위 예제 코드를 다음처럼 간단하게 재구현할 수 있다.
List <Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
코드가 훨씬 간결해졌다!
2.3.3 일곱 번째 시도 : 리스트 형식으로 추상화
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:result){
if(p.test(e)){
result.add(e);
}
}
return result;
}
이제 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 적용할 수 있다.
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers,(Integer i) -> i%2 == 0);
2.4 실전 예제
2.4.1 Comparator로 정렬하기
일단 Comparator의 코드를 먼저 보고가자
// java.util.Comparator
public interface Comparator<T>{
int compare(T o1, T o2);
}
Comparator는 compare의 결과로 정렬을 수행한다. 자세한건 구글에 검색해서 찾아보자.
우리는 Comparator를 구현해서 sort 메서드의 동작을 다양화할 수 있다.
// 익명클래스 방식
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
})
// 람다 방식
inventory.sort(inventory, (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
2.4.2 Runnable로 코드 블록 실행하기
자바 스레드를 이용하면 병렬로 코드를 실행할 수 있다.
Runnanble 인터페이스로 각 스레드마다 실행할 코드 블록을 지정할 수 있다.
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.5 마치며
-
동작 파라미터 화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
-
동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
-
코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8이전에는 코드를 지저분하게 구현해야 했다. 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
-
자바 API의 많은 메서드는 정렬, 스레드 등을 포함한 다양한 동작으로 파라미터화할 수 있다.