동작 파라미터화(behavior parameterization)을 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 동작 파라미터란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미힌다. 이 코드 블록은 나중에 프로그램에서 호출하며 즉, 실행은 나중으로 미뤄진다.
농부가 처음에는 녹색 사과만 필터링하고 싶다고 요구하여 다음과 같이 사과 리스트에서 녹색 사과만 리스트에 추가한다.
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;
}
하지만 빨간 사과도 추가로 필터링하고 싶다고 요구한다.
색을 파라미터화하여 요구사항에 유연하게 대응한다.
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);
}
}
return result;
}
그런데 이번에는 무게에 따라 사과를 구분하고 싶다고 한다. 색을 필터링할 때와 마찬가지로 메서드를 만들면 되지만 대부분의 코드가 중복되며 소프트웨어 공학의 DRY(Don't Repeat Yourself) 원칙을 어기게 된다.
메서드 전체 구현을 고쳐 색과 무게를 메서드 파라미터로 추가한다.
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;
}
메서드를 반환하여 결과를 살펴보면
List<Apple> greenApples = filterApples(inventory, Color.GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, Color.RED, 150, false);
이는 좋지 않은 코드임을 알 수 있다. true나 false가 무엇을 의미하는지 알 수 없고, 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.
파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 필요하다. 사과의 특정 속성에 기초해서 불리언값을 반환하는 방법을 이용한다. 불리언 값을 반환하는 함수를 Predicate라고 하며 선택 조건을 결정하는 인터페이스를 정의한다.
public interface ApplePredicate {
boolean test(Apple apple);
}
이제 선택 조건에 따라 구현체를 생성한다.
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Color.GREEN.equals(apple.getColor());
}
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략(Strategy) 디자인 패턴이라고 부르며 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 알고리즘을 선택하는 기법이다. 여기서는 인터페이스가 알고리즘 패밀리고, 구현체가 전략이다.
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
//predicate 객체로 사과 검사 조건을 캡슐화
if (predicate.test(apple)) {
result.add(apple);
}
}
return result;
}
이제 새로운 요구사항이 생겨도 ApplePredicate의 적절한 구현체만 만들면 된다.
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Color.RED.equals(apple.getColor()) && apple.getWeight() > 150;
}
}
이것은 ApplePredicate 객체에 의해(캡슐화된 boolean 표현식) filterApples 메서드의 동작이 결정되는 것으로 메서드의 동작을 파라미터화한 것이다.
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
하지만 filterApples 메서드에서 새로운 동작을 정의하는 것은 test 메서드이다. 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다. 추후 람다를 사용하면 여러 개의 클래스를 정의하지 않고도 표현식을 filerApples 메서드로 전달할 수 있다.
현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 클래스들을 인스턴스화해야 한다. 이는 로직과 관련 없는 코드와 함께 상당히 번거로운 작업이다. 자바는 클래스 선언과 인스턴스화를 동시에 할 수 있는 익명 클래스를 제공한다.
//메서드의 동작을 직접 파라미터화
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
하지만 익명 클래스는 여전히 많은 공간을 차지하며 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다. 코드의 장황함(verbosity)은 나쁜 특성이다. 장황한 코드는 구현하고 유지보수하는데 많은 비용이 든다. 그리고 코드 조각을 전달하는 과정에서 결국은 객체를 만들고 명시적으로 새로운 동작을 정의하는 메서드를 구현해야 한다는 점은 변하지 않는다.
자바 8의 람다 표현식을 이용하면 다음처럼 간단하게 구성할 수 있다.
List<Apple> filterApples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
형식 파라미터 T를 이용하여 메서드를 추상화한다. 이제 사과뿐만 아니라 바나나, 오렌지, 정수, 문자열 등의 리스트에 filter메서드를 사용할 수 있다.
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T e : list) {
if (predicate.test(e)) {
result.add(e);
}
}
return result;
}
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
개발자에게는 변화하는 요구사항에 쉽게 대응할 수 있는 다양한 정렬 동작을 수행할 수 있는 코드가 필요하다. 자바 8의 List에는 sort 메서드가 포함되어 있다. 다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator를 구현해서 sort 메서드의 동작을 다양화할 수 있다. 다음은 사과를 무게가 적은 순서로 정렬한다.
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
});
람다 표현식을 이용하면 다음처럼 간단하게 구현할 수 있다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight() - a2.getWeight())
자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 여러 스레드가 각자 다른 코드를 실행할 수 있다. 이때 나중에 실행할 수 있는 코드를 구현할 방법이 필요하다. 자바 8부터 람다 표현식을 이용하면 다음처럼 스레드 코드를 구현할 수 있다.
Thread t = new Thread(() -> System.out.println("Hello world"));
자바 5부터 지원하는 ExecutorService 인터페이스는 태스크 제출과 실행 과저으이 연관성을 끊어준다. 이는 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다는 점이 Runnable을 이용하는 방식과는 다르다.
다음 예제는 태스크를 실행하는 스레드의 이름을 반환한다.
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName());