안녕하세요
오늘은 동작 파라미터 방식의 디자인 패턴과 Java8에서 추가 된 문법인 람다에 대해서 알아보겠습니다.

 

  • 동작 파라미터화 Behavior Parameterization
List<Apple> inventory = Arrays.asList(
  new Apple(80,"green"),
  new Apple(155, "green"),
  new Apple(120, "red"));


농부의 재고목록 조사를 위한 프로그램이 있습니다.

우선 사과의 색과 무게의 정보가 담긴 List를 생성합니다.
인벤토리에 사과 3개를 담았습니다.

프로그램을 사용하다보면 한 농부 사용자가 "녹색 사과만 찾고싶어요" 라는 요구사항을 낼 수 있습니다.
또 다음날은 "100g 이상인 녹색 사과를 찾고 싶어요" 라는 요구사항이 올 수 있습니다.
이처럼 변화하는 고객의 요구사항에 효과적으로 대응하기 위해 동작 파라미터 디자인 패턴을 사용합니다.



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번 요구사항 "녹색 사과에 빨간색 사과도 찾고싶어요"

/* param: color */
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;
}



3번 요구사항 "무게에 따라 사과를 찾고싶어요"

/* 신규 메서드 추가 */
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;
}


이러한 요구사항이 지속된다고 할 때, 비슷한 양식의 코드들이 무한 증식하게되어
소프트웨어 공학의 DRY(Don't Repeat Yourself) 원칙을 어기게 됩니다.



가능한 모든 속성으로 필터링하기 위한 여러 시도


1번 시도 : Flag 값으로 필터링

/* flag값을 둬서 필터링 */
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;
}


flag값으로 메소드 내부에서 필터링하도록 구현하였더니 코드가 가독성이 떨어지고 복잡해지기 시작합니다.


이러한 상황에서 Java8의 함수형 인터페이스 Predicate를 사용할 수 있습니다.
Predicate를 사용해서 선택조건을 결정하는 인터페이스를 구현합니다.


2번 시도 : 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;
    }
}
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;
}


코드 자체를 인자로 전달하는 방식으로

isGreenApple 메소드가 test 추상메소드의 구현 메소드가 되어
Predicate의 구현 클래스 인스턴스가 생성되고, 이는 매개 변수로 전달되는 것입니다.

하지만 여기서 새로운 조건이 추가되면 기존의 인터페이스를 복사하여 새로 구현해야하는 문제가 남아있습니다.


3번 시도 : 익명 클래스 사용

filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
});


새로운 요구사항마다 인터페이스를 추가하지 않기 위해 익명 클래스를 파라미터로 넘깁니다.
그러나 핵심 로직은 녹색 사과를 찾아내는 것인데 코드 상에 잘 드러나지 않는다는 단점이 있습니다.


4번 시도 : 람다식 사용

여기서! 람다를 사용하면 소스코드를 간략화할 수 있습니다.

List<Apple> greenApples = filterApples(inventory, (Apple a) -> "green".equals(a.getColor())));


한 번만 사용하는 메서드라면 람다를 사용하도록 합니다.

 

  • 람다표현식이란?

 

함수형 인터페이스(Functional Interface)

구현해야할 추상 메서드가 1개인 인터페이스

어차피 구현되어야 할 메서드가 1개뿐이므로 메서드명 등등을 제거하여 컴파일러의 추론에 의지합니다.

 

참고: https://multifrontgarden.tistory.com/124

 



최종 요구사항 : "사과 말고 다른 농작물도 등록해서 필터링하고 싶어요"

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)이든 다양한 조건으로 과일을 필터링할 수 있게 됩니다!

 

 

[ 참고 ]

https://medium.com/chequer/java8-in-action-2%EC%9E%A5-%EB%8F%99%EC%9E%91-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%ED%99%94-%EC%BD%94%EB%93%9C-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0-7c4404e2edaf

복사했습니다!