ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [책 정리]Modern Java In Action 2장
    책정리/Modern Java In Action 2020. 5. 18. 00:34

    동작 파라미터화 코드 전달하기

    소비자의 요구사항은 항상 바뀌기 마련입니다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 동작 파라미터화(behavior parameterization)
    을 이용하면됩니다.

    동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미합니다. 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.

    EXAMPLE

    기존의 농장 재고목록 애플리케이션에 리스트에서 녹색(green) 사과만 필터링하는 기능을 추가한다고 가정하자.

    첫 번째 시도 : 녹색사과 필터링

    사과 색을 정의하는 ENUM 클래스

    enum Color { RED, GREEN }

    첫 번째 시도

    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);
        }
      }
    }

    만약 농부가 빨간 사과도 필터링 하고 싶다고하면 어떻게 할까? 메서드를 하나 더만들 수 도 있지만 만약 또 농부가 노란색, 어두운 빨간색등 요구사항이
    점차 늘어난다면 메서드가 많이 생길 것이다. 이런경우 다음과 같은 좋은 규칙이 있다.

    거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화 한다.

    두 번째 시도 : 색을 파라미터화

    public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
      List<Apple> result = new ArrayList<>();
      for(Apple apple : inventory) {
        if(apple.getColor().equals(color)) {
          result.add(apple);
        }
      }
    }

    이제 농부가 어떤 색을 원하든 해당 메서드만 호출하면, 색으로 필터링 할 수 있다. 하지만 농부가 색 이외에도 무거운 사과와 가벼운 사과, 무게로
    사과를 필터링하고 싶다고하면 어떻게 해야 할까?

    색을 파라미터화 한 것처럼 무게를 파라미터화 하면 된다.

    public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
      List<Apple> result = new ArrayList<>();
      for(Apple apple : inventory) {
        if(apple.getWeight() > weight) {
          result.add(apple);
        }
      }
      return result;
    }

    위 코드도 좋은 해결책이지만, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다.

    이는 소프트웨어 공학의 DRY(Don't Repeat yourself) 같은 것을 반복하지말 것 원칙을 어기는 것이다. 이를 해결하기 위해서 색과 무게를 filter라는
    메서드로 합치는 방법도 있다. 따라서 색이나 무게 중 어던 것을 기준으로 필터링할 지 가리키는 플래그를 추가할 수 있다.
    (하지만 실전에서는 절대 이 방법을 사용하지 말아야 한다.)

    절대 사용하지 말아야 하는 방법

    • 절대 사용하지 말아야 하는 방법
    public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) {
      List<Apple> result = new ArrayList<>();
      for(Apple apple : inventory) {
        if((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {
          result.add(apple);
        }
      }
      return result;
    }

    위 메서드를 아래처럼 호출 할 수 있다.

    List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
    List<Apple> heavyApples = filterApples(inventory, null, 150, false);

    하지만 정말 마음에 들지 않는 코드이다. 대체 true와 false는 무엇을 의미하는 것일까?... 게대가 앞으로 요구사항이 바뀌었을때 유연하게 대처할 수 없다.
    에를 들어 사과으 크기, 모양, 출하지 등으로 사과를 필터링 하고싶어지는 경우 어떻게 해야할까?

    아래에서 동작 파라미터화를 이용해서 유연성을 얻는 방법에 대해 설명한다.

    동작 파라미터화

    사과의 어떤 속성에 기초해서 불리언 값을 반환(예를 들어 사과가 녹색인가? 150그램 이상인가?). 참 또는 거짓을 반환하는 함수를 프레디케이트 라고 한다.
    선택 조건을 결정하는 인터페이스를 정하자.

    public interface ApplePredicate {
      boolean test (Apple apple);
    }

    다음 예제 처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

    public class AppleHeavyWeightPredicate implements ApplePredicate {
      public boolean test(Apple apple) {
        return apple.getWeight() > 150;
      }
    }
    
    public class AppleGreenColorPredicate implements ApplePredicate {
      public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
      }
    }

    위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인패턴(strategy design patter)이라고 부른다.

    전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.

    여기서는 ApplePredicate가 알고리즘 패밀리이며, 이를 구현한 클래스들이 전략이다.

    네 번째 시도 : 추상적 조건으로 필터링

    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.getWeight() > 150;
      }
    }

    우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다. 즉, 우리는 filterApples 메서드의 동작을 파라미터화 한 것이다.

    즉, 우리는 전략 디자인 패턴(Strategy Design Pattern)과 동작 파라미터화를 통해서 필터 메서드에 전략(Strategy)을 전달 함으로써 더 유연한 코드를
    만들었다.

    한 개의 파라미터, 다양한 동작

    지금까지 살펴본 것첢 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.

    QUIZ

    사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성(커스터마이즈된 다양한 toString) 메서드와 같이) 할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현하시오. 예를 들어 prettyPrintApple 메서드가 각각의 사과 무게를 출력하도록 지시할 수 있다. 혹은 각각의 사과가 무거운지, 가벼운지
    출력하도록 지시할 수 있다.

    • interface 만들기

    참 또는 거짓을 반환하는 함수를 프레디케이트 라고 했습니다. 위 문제에서 String 타입을 반환하라고 했으니 interface 네이밍을 xxxPredicate보다는
    Formatter라든지 이 외의 다른 좋은 네이밍으로 정하는게 좋습니다.

    public interface AppleFormatter {
      String accept(Apple a);
    }
    
    public class AppleFancyFormatter implements AppleFormatter implements AppleFormatter {
      public String accept(Apple apple) {
        String characteristic = apple.getWeight() > 150 ? "heavy" : "light";
        return "A " + characteristic + " " + apple.getColor() + " apple";
      }
    }
    
    public class AppleSimpleFormatter implemetns AppleFormatter { 
      public String accept(Apple apple) {
        return "An apple of " + apple.getWeight() + "g";
      }
    }
    
    public static void prettyPrintApple(List<Apple> inventory, AppleFormatter foramtter) {
      for(Apple apple : inventory) {
        String output = formatter.accept(apple);
        System.out.println(output);
      }
    }

    복잡한 과정 간소화

    위에서 보여준 예시는(Predicate 인터페이스를 선언하고, 이를 구현하여 인스턴스화 하여 사용하는 것) 상당히 번거로운 작업이며, 시간 낭비이다.

    이를 개선 하기 위해서 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다.

    익명 클래스는 자바의 지역 클래스와 비슷한 개념이다.

    다섯 번째 시도 : 익명 클래스 사용

    List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
      public boolean test(Apple apple) {
        return RED.equals(apple.getColor());
      }
    }

    익명 클래스의 단점은 아직도 코드의 줄이 길다는 것이며, 많은 개발자들이 익명 클래스 사용에 익숙하지 않는다는 점 이다.

    여섯 번째 시도 : 람다 표현식 사용

    List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

    일곱 번째 시도 : 리스트 형식으로 추상화

    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<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

    Comparator로 정렬하기

    java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.

    public interface Comparator<T> {
      int compare(T o1, T o2);
    }

    무게가 적은 순서로 목록에서 사과를 정렬

    inventory.sort(new Comparator<Apple>() {
        public int comapre(Apple a1, Apple a2) {
          return a1.getWeight().compareTo(a2.getWeight());
        }
    }
    inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
    반응형

    댓글

Designed by Tistory.