[모던 자바 인 액션] Chapter 02. 동작 파라미터화 코드 전달하기
이 장의 내용
💡 변화하는 요구사항에 대응
💡 동작 파라미터화
💡 익명 클래스
💡 람다 표현식 미리보기
💡 실전 예제: Comparator, Runnable, GUI
2.1 변화하는 요구사항에 대응하기
변화에 대응하는 코드를 구현하는 것은 어려운 일이다. 그러나 변화를 피할 수는 없는 것 같다. 이 장에서는 동작 파라미터화 코드에 왜 유용한지를 설명한다.
2.1.1 첫 번째 시도: 녹색 사과 필터링
농장 재고 목록 애플리케이션에 녹색 사과만 필터링 하는 기능을 추가한다고 가정하자. 비교적 간단한 작업이라는 생각이 들 것이다.
if 문 안에서 녹색 사과인지를 필터링한다. 그런데 갑자기 농부가 변심하여 녹색 사과 말고 빨간 사과도 필터링하고 싶어졌다. 어떻게 고쳐야 할까? 크게 고민하지 않은 사람이라면 메서드를 복사해서 filterRedApples 라는 새로운 메서드를 만들수도 있다. 이와 같은 방법으로 빨간 사과를 필터링할 수는 있겠지만 나중에 농부가 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절히 대응할 수 없다. 이런 상황에서는 다음과 같은 좋은 규칙이 있다.
거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.
2.1.2 두 번째 시도: 색을 파라미터화
어떻게 해야 filterGreenApples의 코드를 반복하지 않고 filterRedApples를 구현할 수 있을까? 색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀더 유연하게 대응하는 코드를 만들 수 있다.
이제 농부도 만족할 것이다. 그런데 갑자기 농부가 다시 나타나서는 "색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있다면 정말 좋겠네요. 보동 무게가 150그램 이상인 사과가 무거운 사과입니다" 라고 요구한다.
다음 코드에서 확인할 수 있는 것처럼 무게 정보 파라미터도 추가했다.
물론 위 코드도 좋은 해결책이라 할 수 있다. 하지만 구현 코드를 자세히 보면 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다. 이는 소프트웨어 공학의 DRY(don't repeat yourself) 원칙을 어기는 것이다. 탐색 과정을 고쳐서 성능을 개선하려면 무슨 일이 일어날까? 한 줄이 아니라 메서드 전체 구현을 고쳐야 한다. 즉, 엔지니어링적으로 비싼 대가를 치러야 한다.
색과 무게를 filter 라는 메서드로 합치는 방법도 있다. 그러면 색과 무게 중에 어떤 기준으로 사과를 필터링할지에 대한 또 다른 방법이 필요하다. 따라서 색이나 무게 중 어떤 것을 기준으로 할지 가리키는 플래그를 추가할 수 있다. (하지만 실전에서는 절대 이 방법을 사용하지 말아야 한다.)
2.1.3 세 번째 시도: 가능한 모든 속성으로 필터링
다음은 만류에도 불구하고 모든 속성을 메서드 파라미터로 추가한 모습이다.
형편없는 코드다! 대체 true와 false는 뭘 의미할까? 게다가 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수 없다.
2.2절에서는 동작 파라미터화를 이용해서 유연성을 얻는 방법을 설명한다.
2.2 동작 파라미터화
2.1절에서 파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 절실하다는 것을 확인했다. 한 걸음 물러서서 전체를 보자. 사과의 어떤 속성에 기초해서 불리언값을 반환하는 방법이 있다. 참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 선택 조건을 결정하는 인터페이스를 정의하자.
위 조건에 따라 fiter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인 패턴(strategy design pattern) 이라고 부른다. 이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고치자. 이렇게 하면 filterApples 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.
2.2.1 네 번째 시도: 추상적 조건으로 필터링
잠시 자축할 시간을 갖자. 첫 번째 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워졌다. 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다니 정말 멋지지 않은가! 즉, 우리는 filterApples 메서드의 동작을 파라미터화 한 것이다.
지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 따라서 유연한 API를 만들 때 동작 파라미터화가 중요한 역할을 한다. 하지만 여러 클래스를 구현해서 인스턴스화하는 과정이 조금은 거추장스럽게 느껴질 수 있다. 이 부분을 어떻게 개선할 수 있는지 확인하자.
2.3 복잡한 과정 간소화
인터페이스를 추가하면 로직과 간련 없는 코드가 많이 추가된다. 이를 어떻게 개선할 수 있을까? 자바는 클래스의 선언과 동시에 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(anonymous class)라는 기법을 제공한다. 익명 클래스를 사용하면 코드의 양을 줄일 수 있다. 2.3.3절에서는 간단하게 람다 표현식으로 더 가독성 있는 코드를 구현하는 방법을 설명한다.
2.3.1 익명 클래스
익명 클래스는 자바의 지역 클래스(local class)와 비슷한 개념이다. 익명 클래스는 말 그대로 이름이 없는 클래스다. 익명클래스는 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
2.3.2 다섯 번째 시도: 익명 클래스 사용
다음은 익명 클래스를 이용해서 Appleredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현한 코드다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
하지만 여전히 많은 공간을 차지하고 있다. 또한 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다.
지금까지 살펴본 것처럼 동작 파라미터화를 이용하면 요구사항 변화에 더 유연하게 대응할 수 있으므로 모든 프로그래머가 동작 파라미터화를 사용하도록 권장한다. 3장에서는 자바 8 언어 설계자가 람다 표현식이라는 더 간단한 코드 전달 기법을 도입해서 이 문제를 해결했음을 보게될 것이다.
2.3.3 여섯 번째 시도: 람다 표현식 사용
자바 8의 람다 표현식을 사용해서 위 예제 코드를 다음처럼 간단하게 재구현할 수 있다.
List<Apple> result =
filterApples(inventory, (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()));
이렇게 해서 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있었다. 자바 8이 아니면 불가능한 일이다.
2.4 실전 예제
이 절에서는 코드 전달 개념을 더욱 확실히 익힐 수 있도록 Comparator로 정렬하기, Runnable로 코드 블록 실행하기, Callable을 결과로 반환하기, GUI 이벤트 처리하기 예제를 소개한다.
2.4.1 Comparator로 정렬하기
컬렉션의 정렬은 반복된느 프로그래밍 작업이다. 예를 들어 처음에는 농부가 무게를 기준으로 목록에서 사과를 정렬하고 싶다고 말할 것이다. 하지만 곧 마음을 바꿔 색을 기준으로 사과를 정렬하고 싶어질 수 있다. 따라서 개발자에게는 변화하는 요구사항에 쉽게 대응할 수 있는 다양한 정렬 동작을 수행하는 코드가 필요하다.
자바 8의 List에는 sort 메서드가 포함되어 있다. 다음과 같은 인터페이스를 갖는 java.util.Comparator객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
// 익명 클래스
inventory.sort(new Compatator<T> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
// 람다
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
2.4.2 Runnable로 코드 블록 실행하기
자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 어떤 코드를 실행할 것인지를 스레드에게 알려줄 수 있을까? 여러 스레드가 각자 다른 코드를 실행할 수 있다. 나중에 실행할 수 있는 코드를 구현할 방법이 필요하다.
public interface Runnable {
void run();
}
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello");
}
});
Thread t = new Thread(() -> System.out.println("Hello"));
2.4.3 Callable을 결과로 반환하기
이 절은 조금 낯선데 ExecutorService 인터페이스를 이용하면 태스크 제출과 실행 과정의 연관성을 끊어준다고 한다. 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다. 당장은 Callable 인터페이스를 이용해서 결과를 반환하는 태스크를 만든다는 것만 기억하자. 이 방식은 Runnable의 업그레이드 버전이라고 생각할 수 있다.
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());
2.4.4 GUI 이벤트 처리하기
Button button = new Button("send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
2.5 마치며
- 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 람다를 제공한다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.