모던 자바 인 액션 - 동적 파라미터화
자바 8에서는 메서드 파라미터에 코드 블록 그 자체를 포함
시키는 동적 파라미터
를 제공한다.
어떻게 변수나 객체가 아닌 코드 블록을 파라미터에 포함 시킬 수 있는 걸까?
단계 별로 진행하며 자바8에서 어떤 식으로 동적 파라미터를 지원하는지에 대해 알아보자.
동적 파라미터 도입 전
처음으로 우리가 농장의 사과를 농장 주인의 요구사항에 맞춰 필터링하는 프로그램을 만들었다고 가정하자.
1. 녹색 사과만 필터링
1
2
3
4
5
6
7
8
9
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;
}
그런데 여기서 농장 주인이 녹색 뿐만 아니라 빨간색, 어두운 빨간색 등 다양한 요구 사항을 추가한다고 했을 때, 위 코드는 이런 요구 사항에 적절히 대응하기가 어렵다.
2. 색을 파라미터화
다양한 색을 지원하기 위해 색을 파라미터로 받아 요구사항에 대응했다.
1
2
3
4
5
6
7
8
9
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (color.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
이렇게 변화함으로써 색에 대한 요구 사항은 대응이 가능해졌다.
하지만 여기서 사과의 무게에 대한 필터도 추가해달라는 요구 사항이 더해졌다.
그럼 우리는 위의 코드를 응용해 이러한 메서드를 만들 것이다.
1
2
3
4
5
6
7
8
9
public static List<Apple> filterApplesByWeight(List<Apple> inventory, Integer weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
이렇게 바꾼다면 무게에 대한 것도 대응이 가능하다.
하지만 이런 방식은 DRY(don’t repeat yourself : 같은 것을 반복하지 말 것)를 위반한다.
위와 같은 방식으로 메서드를 설계하면 추후 리팩토링이나 성능 개선하기 위해선 많은 리소스가 필요하게 된다.
이런 문제들을 해결하기 위해 우리는 동적 파라미터가 필요한 것이다.
동적 파라미터 도입 후
동적 파라미터를 도입해 위 코드를 리팩토링 해보자.
우선 객체의 어떤 속성에 기초해 boolean 값을 반환하는 함수가 필요하다. 이런 함수를 프레디케이트(Predicate)라고 부른다.
1. predicate
- 프레디케이트 인터페이스
1
2
3
public interface ApplePredicate {
boolean test(Apple apple);
}
- 무게를 비교하는 프레디케이트 구현체
1
2
3
4
5
6
public class AppleWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
- 색을 비교하는 프레디케이트 구현체
1
2
3
4
5
6
public class AppleColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Green.equals(apple.getColor());
}
}
위와 같은 식으로 하나의 인터페이스를 구현하고 그에 맞는 여러 구현체들을 만들어 원하는 요구 사항에 알맞게 대응이 가능하다.
이런 방식을 전략 디자인 패턴(strategy design pattern)
이라고 부른다.
전략 디자인 패턴
- 각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의한 뒤 런타임에 알고리즘을 선택하는 기법
- 여기서 알고리즘 패밀리 : ApplePredicate(), 각 알고리즘 : AppleWeightPredicate(), AppleColorPredicate()이 된다.
이렇게 구현한 알고리즘 패밀리를 우리의 애플리케이션에 적용시키면 동적 파라미터화, 즉 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
1. 동적 파라미터 적용
1
2
3
4
5
6
7
8
9
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;
}
요청 시
1
List<Apple> list = filterApples(inventory, new AppleColorPredicate());
이와 같이 우리가 만든 Predicate 구현체를 파라미터로 넘겨주기만 하면 끝이다.
이제 우리는 동적 파리미터를 사용해 다양한 요구사항에 대응이 가능해진 것 뿐만 아니라, List 캘렉션을 반복하는 로직과 각 요소에 적용할 동작을 분리해 더욱 객체 지향 적인 설계를 만들 수 있었다.
하지만 위의 방식으로 사용하려면 요구 사항이 바뀔 때마다 새로운 Predicate를 만들어야하는 번거로움이 있다. 이런 부분도 해결해보자.
익명 클래스와 람다 적용
익명클래스는 자바의 지역 클래스(내부 클래스)와 비슷한 개념으로 동작한다.
익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
1
2
3
4
5
List<Apple> list = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
다음과 같이 ApplePredicate 인터페이스를 구현하는 구현체를 익명 클래스로 만듬으로써 클래스를 생성하는 번거로움을 줄일 수 있었다.
다음은 람다를 적용해보자.
1
List<Apple> list = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor());
이와 같이 한결 간결해진 모습을 확인할 수 있다.
제네릭 적용
마지막으로 제네릭을 적용해 유연성까지 적용시킨다면 사과 뿐만 아니라 바나나, 수박 등 다양한 타입의 객체에 이 filter를 적용 시킬 수 있다.
1
2
3
public interface Predicate {
boolean test(T t);
}
1
2
3
4
5
6
7
8
9
public static List<T> filterApples(List<T> inventory, Predicate p) {
List<T> result = new ArrayList<>();
for (T e : inventory) {
if (p.test(e)) {
result.add(e);
}
}
return result;
}