동작 파라미터화
를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.
이는 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.
동작 파라미터화를 추가하려면 쓸데없는 코드가 늘어나게 되는데, 이는 람다 표현식
으로 해결한다.
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;
}
색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 유연하게 대응할 수 있을 것이다.
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;
}
여기서 ‘색 이외에도 사과를 무게로도 구분하면 좋겠다’라는 요구사항이 생긴다면? → 무게 파라미터를 추가하면 된다.
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);
}
}
return result;
}
필터링을 적용하는 부분의 코드가 중복되는 것을 알 수 있는데, 이는 DRY 원칙을 어기는 것이다. 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가하여 이를 해결할 수 있다.
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;
}
그러나 이는 형편 없는 코드다. boolean값이 무엇을 의미하는지 알 수가 없기 때문이다.
문제가 잘 정의되어 있는 상황에서는 문자열, 정수 불리언 등의 값으로 filterApples 메서드를 파라미터화 한 것이 잘 동작할 수 있다. 하지만 filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것이다.
다음 절에서 알아본다.
참 또는 거짓을 반환하는 함수를 프레디케이트
라고 한다. 선택 조건을 결정하는 인터페이스
를 정의하자.
public interface ApplePredicate {
boolean test (Apple apple);
}
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());
}
}
조건에 따라 filter 메서드가 다르게 동작할 것인데, 이를 전략 디자인 패턴
이라고 부른다.
ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPredicate, AppleGreenColorPredicate가 전략이다.
메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다. (동작 파라미터화)
ApplePredicate를 이용한 필터 메서드다.
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;
}
코드/동작 전달하기
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());
한 개의 파라미터, 다양한 동작
컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것
이 동작 파라미터화의 강점이다. 한 메서드가 다른 동작을 하도록 재활용할 수 있다. ⇒ 유연한 API를 만들 때 동작 파라미터화가 중요한 역할을 한다.
자바는 클래스의 선언과 인스턴스화를 동시에 할 수 있도록 익명 클래스
라는 기법을 제공한다. 이를 이용하면 코드의 양을 줄일 수 있다.
익명 클래스
는 자바의 지역 클래스와 비슷한 개념이다. 이는 이름이 없는 클래스인데 클래스 선언과 인스턴스화를 동시에 할 수 있다. ⇒ 즉석에서 필요한 구현을 만들어서 사용할 수 있다.
익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현한 코드이다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { // filterApples 메서드의 동작을 직접 파라미터화했다.
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
}
그러나 익명 클래스는 많은 공간을 차지한다는 단점이 있다. 또 많은 프로그래머가 익명 클래스의 사용에 익숙치 않다.
동작 파라미터화를 이용하면 요구사항 변화에 더 유연하게 대응할 수 있으므로 동작 파라미터화를 사용하도록 권장한다.
람다 표현식을 사용하면 2.3.2의 코드를 간단하게 재구현 할 수 있다.
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) { // 형식 파라미터 T 등장
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);
동작 파라미터화 패턴은 동작을 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화 한다.
컬렉션 정렬은 반복되는 프로그래밍 작업이다. 변화하는 요구사항에 쉽게 대응할 수 있는 다양한 정렬 동작을 수행할 수 있는 코드가 필요하다.
List에는 sort 메서드가 포함되어 있다. 아래와 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort 동작을 파라미터화할 수 있다.
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator을 구현해서 sort 메서드의 동작을 다양화할 수 있다.
자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 여러 스레드가 각자 다른 코드를 실행할 수 있는데, 나중에 실행할 수 있는 코드를 구현할 방법이 필요하다.
자바에서는 Runnable 인터페이스를 이용해 실행할 코드 블록을 지정할 수 있다.
// java.lang.Runnable
public interface Runnable {
void run();
}
Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello world");
}
});
Thread t = new Thread(() -> System.out.println("Hello world"));
Callable 인터페이스를 활용해 결과를 반환하는 태스크를 만든다. 이 방식은 Runnable의 업그레이드 버전이다.
// java.util.concurrent.Callable
public interface Callable<V> {
V call();
}
실행 서비스를 태스크에 제출해서 위의 코드를 활용할 수 있다. 아래의 예제 코드는 태스크를 실행하는 스레드의 이름을 반환한다.
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
Future<String> threadName = executorService.submit( () -> Thread.currentThread().getName());
💡 정리 💡