Java8 in Action — 2장 #동작 파라미터화 코드 전달하기
예를들어 농부가 재고목록 조사를 쉽게 할 수 있도록 돕는 애플리케이션이 있습니다.
어느날 농부가 “녹색 사과를 모두 찾고 싶어요” 라고 하더니, 다음날 다시 “150그램 이상이면서 녹색인 사과를 모두 찾고 싶어요”, 그다음날 또 …
고객의 요구사항은 계속해서 바뀌고, 변화에 따라 프로그래머는 지속적으로 코드를 수정해야합니다.
‘동작 파라미터화’ 을 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 할 수 있습니다.
동작 파라미터화는 아직 어떻게 실행할 것인지 결정하지 않는 코드 블록을 의미합니다.
1) 첫번째 시도 : 녹색 사과 필터링
public static List filterGreenApples(List inventory){
List result = new ArrayList<>();
for(Apple apple: inventory){
if("green".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
농부가 갑자기 빨간색 사과도 필터링 하고싶다고 요청했습니다.
2) 두번째 시도 : 색을 파라미터화
public static List filterApplesByColor(List inventory, String color){
List result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getColor().equals(color)){
result.add(apple);
}
}
return result;
}
자, 이제 색을 파라미터화 했으니 다양한 색상을 파라미터로 처리 할 수 있게 되었습니다.
그런데 농부가 무게별로도 분류하고 싶어합니다.
그래서 기존에 있던 filterApplesByColor 코드를 그대로 Copy & Paste 해서 filterApplesByWeight라는 메서드를 하나더 생성합니다.
public static List filterApplesByWeight(List inventory, int weight){
List result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getWeight() > weight){
result.add(apple);
}
}
return result;
}
하지만 90% 이상이 동일한 코드가 반복되고 있고, 이는 소프트웨어 공학의 DRY(don’t repeat yourself) 원칙을 어기고 있습니다.
그래서 개발자는 다음과 같이 또 코드를 변경했습니다.
3) 세번째 시도 : 가능한 모든 속성으로 필터링
public static List filterAppes(List inventory, String color, int weight, boolean flag){
List result = new ArrayList<>();
for(Apple apple: inventory){
if((flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}이제 코드가 점점 가독성도 떨어지고 복잡한 로직으로 가기 시작했습니다.개발자는 이제 기존 코드를 버리고 Predicate(프레디케이트) - 선택조건을 결정하는 인터페이스를 도입해야겠다고 생각합니다.예를들면 선택조건을 대표하는 여러 버전의 Predicate를 설계합니다.interface ApplePredicate{
public boolean test(Apple a);
}
static class AppleWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
static class AppleColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
static class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}4) 네번째 시도 : 추상적 조건 필터링Predicate 인터페이스를 설계하면서 다음과 같은 코드로 필터링이 가능해졌습니다.public static List filterApples(List inventory, ApplePredicate p){
List result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}하지만 새로운 새로운 조건을 만들기 위해서는 기존 Predicate를 Copy & Paste 해서 구현해야합니다. 물론 파라미터를 Predicate 타입으로 통일화 했지만, 여전히 조건이 추가될 때 마다 새로운 인터페이스를 구현해야하는 문제가 남아있습니다.그래서 직접 익명 클래스를 파라미터로 넘기는 시도를 해봅니다.5) 다섯번째 시도 : 익명 클래스 사용filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});하지만 여전히 코드가 장황해보인다. 핵심 코드는 "red" Apple을 찾아내는 것인데, 익명클래스를 구현하면서 코드가 길어졌습니다.6) 여섯번째 시도 : 람다 표현식 사용filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor());람다 표현식으로 인해 코드가 매우 간결해졌고, 핵심로직이 잘 드러나 보입니다.이제 마지막 하나의 문제가 남았습니다.농부가 내년부터는 사과를 비롯해 바나나, 오렌지 등 여러가지 과일을 재배할 계획이라고 합니다.Apple 뿐아니라, 여러가지 과일을 필터링 할 수 있는 filter를 만들어보겠습니다.public interface Predicate {
boolean test(T t);
}
public static List filter(List list, Predicate p) {
List result = new ArrayList<>();
for(T e : list) {
if(p.test(e)) {
result.add(e);
}
}
return result;
}
List redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor());
List heavyOranges = filter(inventory, (Orange orange) -> orange.getWeight() > 500);이제는 어떠한 타입이든(T) 다양한 조건으로 (람다) 과일을 필터링 할 수 있게 되었습니다.정리하면
- 동작 파라미터화란 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달하는 것을 말합니다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 좀더 유연하게 대응할 수 있고, 결국 엔지니어링 비용을 줄일 수 있습니다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달 할 수 있는데, 자바8 이전에는 코드를 지저분하게 구현해야 했지만, 자바8부터는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없애고, 람다표현을 사용해 코드를 직접 파라미터화 할 수 있습니다.