모던 자바 인 액션 - CH02. 동작 파라미터화 코드 전달하기
들어가기에 앞서..
- 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.
- 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있다. 결과적으로 코드 블록에 따라 메서드의 동작이 파라미터화 된다.
- 따라서, 변화하는 요구 사항에 유연하게 대응할 수 있다.
그런데, 동작 파라미터화를 추가하려면 쓸데없는 코드가 늘어난다. 자바 8은 이 문제를 람다 표현식으로 해결한다.
2.1 변화하는 요구사항에 대응하기
하나의 예제를 선정한 다음에 예제 코드를 점차 개션하면서 유연한 코드를 만드는 모범사례로 설명할 것이다.
기존의 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링하는 기능을 추가한다고 가정하자.
2.1.1 첫 번째 시도 : 녹색 사과 필터링
enum Color { RED, GREEN }
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>(); // 사과 누적 리스트
for (Apple appe: inventory) {
if **(GREEN.equals(apple.getColor())** { // 녹색 사과만 선택
result.add(people);
}
}
return result;
}
위 코드는 녹색 사과를 필터링하는 코드이다. 그런데 농부가 변심해서 빨간 사과도 필터링하고 싶어진다면? 이 외에도 좀 더 다양한 색(옅은 녹색, 어두운 빨간색, 노란색)을 필터링하고 싶어진다면?
코드를 복사 붙여넣기해서 새로운 메소드를 만들어야하기 때문에 코드가 중복된다. 이런 상황에서는 다음과 같은 좋은 규칙이 있다.
거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.
2.1.2 두 번째 시도:색을 파라미터화
색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있다.
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple appe: inventory) {
if **(apple.getColor().equals(color))** {
result.add(people);
}
}
return result;
}
다음 처럼 구현한 메소드를 호출할 수 있다.
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 appe: inventory) {
if (apple.getWeight() >= weight) {
result.add(people);
}
}
return result;
}
각 사과에 필터링 조건을 적용하는 부분의 코드가 중복된다. 이는 소포트웨어 공학의 DRY(Don't Repear Yourself) 원칙을 어기는 것이다.
2.1.3 세 번째 시도:가능한 모든 속성으로 필터링
색이나 무게 중 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가하였다. (실전에서는 이 방법을 절대 사용하지 말아야 한다.)
public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) {
List<Apple> result = new ArrayList<>();
for (Apple appe: inventory) {
if (flag && apple.getColor().equals(color)) || !flag && apple.getWeight() > weight) {
result.add(people);
}
}
return result;
}
형편없는 코드이다. 만약에 사과의 크기, 모양, 출하지 등으로 사과를 필터링하고 싶다면? 심지어 녹색 사과 중에 무거운 사과를 필터링하고 싶다면? 요구사항이 바뀌었을 때 유연하게 대응할수 없다.
2.2 동작 파라미터화
참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 선택 조건을 결정하는 인터페이스를 정의하자.
public interface ApplePredicate {
boolean test (Apple apple);
}
다음 예제처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.
// 무거운 사과만 선택
public class AppleHeavyWeightPredicate implements AppplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
// 녹색 사과만 선택
public class AppleColorPredicate implements AppplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}
이를 `전략 디자인 패턴`이라고 부른다. 전략 다자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리는 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다. 위 예제에서는 ApplePredicate가 알고리즘 패밀리이고, AppleHeavyWeightPredicate, AppleColorPredicate가 전략이다.
2.2.1 네 번째 시도:추상적 조건으로 필터링
filterApples에서 ApplePredicate 객체를 받아 애플의 조건을 검사하도록 메서드를 고쳐야 한다. 이렇게 동작 파라미터화, 즉 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양하게 동작을 수행할 수 있다.
이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고치자. 이렇게 하면 filterApples 메서드 내부에서 `컬렉션을 반복하는 로직`과 `컬렉션의 각 요소에 적용할 동작`을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득이다.
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;
}
코드/동작 전달하기
첫 번째 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워졌다.
이제 농부가 150 그램이 넘는 빨간 사과를 검색해달라고 부탁하면 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다.
public class AppleRedAndHeavyPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return RED.equals(apple.getColor()) && apple.weight() > 150;
}
}
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 즉, 우리는 filterApples의 메서드의 동작을 파라미터화 했다.
한 개의 파라미터, 다양한 동작
한 메서드가 다른 동작을 수행하도록 재활용할 수 있다. 따라서 유연한 API를 만들 때 동작 파라미터화가 중요한 역할을 한다.
2.3 복잡한 과정 간소화
위 코드를 보면 로직과 관련 없는 코드가 많이 추가되었다. 이 때문에 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다. 그리고 익명 클래스에 더해, 간단하게 더 가독성 있는 코드를 구현할 수 있는 람다 표현식이 있다.
2.3.1 익명 클래스
- 익명 클래스는 자바의 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념이다.
- 익명 클래스는 말 그대로 이름이 없는 클래스이다.
- 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
- 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.
2.3.2 다섯 번째 시도:익명 클래스 사용
다음은 익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현한 코드이다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
익명 클래스로도 아직 부족한 점이 있다. 익명클래스는 여전히 많은 공간을 차지한다. 그리고 많은 프로그래머가 익명 클래스 사용에 익숙하지 않다.
2.3.2 여섯 번째 시도:람다 표현식 사용
자바 8의 람다 표현식을 이용해서 위 예제 코드를 다음처럼 간단하게 재구현할 수 있다.
List<Apple> result = filterApples(inventoruy, (Apple apple) -> RED.equals(apple.getColor()));
2.3.4 일곱 번째 시도:리스트 형식으로 추상화
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;
}
이제 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다. 다음은 람다 표현식을 사용한 예제다.
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List<Apple> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
이렇게 해서 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있다.
2.4 실전 예제
이 절에서는 코드 전달 개념을 더욱 확실히 익힐 수 있도록 Comparator로 정렬하기, Runnable로 코드 블록 실행하기, Callable을 결과로 반환하기, GUI 이벤트 처리하기 예제를 소개한다.
2.4.1 Comparator로 정렬하기
자바 8의 List에는 sort 메서드가 포함되어 있다. 다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화할 수 있다.
// java.uitl.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator를 구현해서 sort 메서드의 동작을 다양화할 수 있다. 예를들어 익명 클래스를 이용해서 무게가 적은 순서로 목록에서 사과를 정렬할 수 있다.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWegiht().compareTo(a2.getWeight());
}
});
람다 표현식을 이용하면 다음처럼 간단하게 코드를 구현할 수 있다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo)a2.getWeight()));
2.4.2 Runnable로 코드 블록 실행하기
자바 8까지는 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않은 void run 메소드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 것이 일반적인 방법이었다.
자바에서는 Runnable 인터페이스를 이용해서 실행할 코드 블록을 지정할 수 있다.
// java.lang.Runnable
public interface Runnable {
void run();
}
Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello world");
}
});
자바 8부터 지원하는 람다 표현식을 이용하면 다음처럼 스레드 코드를 구현할 수 있다.
Thread t = new Thread(() -> System.out.println("Hello world"));
2.4.3 Callable을 결과로 반환하기
ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다. ExecutorService을 이용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다는 점이 스레드 Runnable을 이용하는 방식과 다르다.
Callable 인터페이스를 이용해 결과를 반화하는 태스크를 만든다. 이 방식은 Runnable의 업그레이드 버전이라고 생각할 수 있다.
// java.util.concurrent.Callable
public interface Callable<V> {
V call();
}
실행 서비스에 태스크를 제출해서 위 코드를 활용할 수 있다. 다음 예제는 태스크를 실행하는 스레드의 이름을 반환한다.
ExecutorService executorService = Excutors.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());
2.4.4 GUI 이벤트 처리하기
자바 FX에서는 setOnAction 메서드에 EventHandler를 전달함으로써 이벤트에 어떻게 반응할지 설정할 수 있다.
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
lable.setText("Sent!!");
}
});
즉, EventHandler는 setOnAction 메서드의 동작을 파라미터화한다. 람다 표현식으로 다음처럼 구현할 수 있다.
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));