본문 바로가기
그룹 스터디 공부(IT 서적)/모던 자바 인 액션

chaper 2.동적 파라미터화 코드 전달하기

by hanyugyeong 2023. 7. 21.
반응형
SMALL

1. 동작 파라미터화란

  • 동작 파라미터화란, 어떻게 실행할지 결정하지 않은 코드 블록을 의미한다.
  • 이 코드블록은 나중에 호출되어 사용되어질 때, 실행됀다.
  • 자주 바뀌는 요구사항에 효과적으로 대응할 수 있음을 의미한다.
  • 나중에 실행될 메서드의 인수로 코드블록을 전달할 수 있고, 결과적으로 코드블록에 메서드의 동작이 파라미터화 되어 전달된다.

2.변화하는 요구사항에 대응

요구사항

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

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

public static List<Apple> filterGreenApples(List<Apple> inventory){
    List<Apple> result  = new ArrayList<>(); // 사과 누적 리스트
    for(Apple apple : inventory){
      if(Color.GREEN.equals(apple.getColor())){ //녹색 사과만 선택
        result.add(apple);
      }
    }
    return result;
  }

위 코드에서 만약 빨간 사과도 필터링하고 싶어지면 메서드를 복사해서 filterRedApples라는 새로운 매서드를 만들고, if문의 조건을 빨간 사과로 바꾸는 방법을 선택할 수 있지만 나중에 농부가 더 다양한 색으로 필터링하는 등의 변화에는 적절하게 대응할 수 없다.

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

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

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

구현한 메서드를 호출할 수 있다.

List<Apple> greenApples = filterApplesByColor(inventory, Color.GREEN);
List<Apple> redApples = filterApplesByColor(inventory,Color.RED);

다양한 무게에 대응할 수 있는 무게 정보 파라미터

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(같은 것을 반복하지 말 것) 원칙을 어기는 것이다.

2.1.3 세 번째 시도: 가능한 모든 속성으로 필터링

 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,Color.GREEN,0,true);
List<Apple> heavyApples = filterApples(inventory,null,150,false);

사과의 크기, 모양, 출하지 등으로 사과를 필터링하고 싶다면? 녹색 사과 중에 무거운 사과를 필터링하고 싶다면?

여러 중복된 필터 매서드를 만들거나 아니면 모든 것을 처리하는 거대한 하나의 필터 매서드를 구현해야 한다.

2.2 동작 파라미터화

참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 선택 조건을 결정하는 인터페이스를 정의하자

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

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

public class AppleHeavyWeightPredicate implements ApplePredicate{ // 무거운 사과만 선택
    @Override
    public boolean test(Apple apple) {
      return apple.getWeight() > 150;
    }
  }


  public class AppleGreenColorPredicate implements ApplePredicate{ //녹색 사과만 선택
    @Override
    public boolean test(Apple apple) {
      return Color.GREEN.equals(apple.getColor());
    }
  }

ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPredicate와 AppleGreenColorPredicate가 전략이다.

동작 파라미터화, 즉 매서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

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

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

코드/동작 전달하기

public static class AppleRedAndHeavyPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple) {
      return Color.RED.equals(apple.getColor()) && apple.getWeight() > 150;
    }
  }
List<Apple> redAndHeavyApples = filterApples(inventory,new AppleRedAndHeavyPredicate());

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

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

예제 유연한 prettyPrintApple 매서드 구현하기

public class PrettyPrintApple {
    public static void main(String[] args) {
        List<PrettyApple> inventory = Arrays.asList(
                new PrettyApple(80, PrettyColor.GREEN),
                new PrettyApple(155, PrettyColor.GREEN),
                new PrettyApple(160, PrettyColor.RED));
        prettyPrintOut(inventory,new appleHeavy());
    }

    enum PrettyColor {
        RED,
        GREEN
    }

    public static class PrettyApple {
        private int weight = 0;
        private PrettyColor color;

        public PrettyApple(int weight, PrettyColor color) {
            this.weight = weight;
            this.color = color;
        }

        public int getWeight() {
            return weight;
        }

        public void setWeight(int weight) {
            this.weight = weight;
        }
        @SuppressWarnings("boxing")
        @Override
        public String toString() {
            return String.format("Apple{color=%s, weight=%d}", color, weight);
        }

    }

    interface prettyPrintApplePredicate{
        String accept(PrettyApple a);
    }
    static class  appleWeight implements  prettyPrintApplePredicate{
        @Override
        public String accept(PrettyApple a) {
            String character = a.getWeight()>150? "heavy":"light";
            return character +" Apple "+ String.valueOf(a.getWeight())+ "g";
        }
    }

    static class appleHeavy implements prettyPrintApplePredicate{
        @Override
        public String accept(PrettyApple a) {
            String heavyApple = "";
            if(a.getWeight() > 150){
                heavyApple += a.getWeight() + "g";
            }
            return heavyApple;
        }
    }

    public static void prettyPrintOut(List<PrettyApple> inventory,prettyPrintApplePredicate p) {
        for (PrettyApple apple : inventory) {
            System.out.println(p.accept(apple));
        }
    }
}

위와 같은 여러 클래스를 구현해서 인스턴스화하는 과정이 조금은 거추장스럽게 느껴질 수 있다.

2.3 복잡한 과정 간소화

-> filterApples 매서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화해야 한다.
자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다. 익명 클래스를 이용하면 코드의 양을 줄일 수 있다.

2.3.1 익명 클래스

익명 클래스는 자바의 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념이다. 익명 클래스는 말 그대로 이름이 없는 클래스다.

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

//filterApples 메서드의 동작을 직접 파라미터화했다!
    List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
      @Override
      public boolean test(Apple apple) {
        return Color.RED.equals(apple.getColor());
      }
    });

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

//람다 표현식
List<Apple> result = filterApples(inventory,(Apple apple)->Color.RED.equals(apple.getColor()));

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

  public static void main(String... args) {
    List<Apple> inventory = Arrays.asList(
        new Apple(80, Color.GREEN),
        new Apple(155, Color.GREEN),
        new Apple(120, Color.RED));
    List<Integer> numbers = new ArrayList<Integer>(Arrays.asList(1,2,3));

    List<Apple> redApples = filter(inventory,(Apple apple)->Color.RED.equals(apple.getColor()));
    List<Integer> evenNumbers = filter(numbers,(Integer i)->i%2 == 0);
  }

 public interface Predicate<T>{
    boolean test(T t);
  }

  public static <T> List<T> filter(List<T> list, Predicate<T> p){ // 형식 파라미터 T 등장
    List<T> result = new ArrayList<>();
    for(T e:list){
      if(p.test(e)){
        result.add(e);
      }
    }
    return result;
  }

2.4.1 Comparator로 정렬하기

컬렉션 정렬은 반복되는 프로그래밍 작업이다.

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

Comparator를 구현해서 sort 매서드의 동작을 다양화할 수 있다.
익명 클래스를 이용해서 무게가 적은 순서로 목록에서 사과를 정렬할 수 있다.

// ex) java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2)l
}
// anonymous class
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

// lamda
inventory.sort(
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

2.4.2 Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다.
아래 코드에서 볼 수 있는 것처럼 코드 블록을 실행한 결과는 void다

//java.lang.Runnable
public interface Runnable{
    void run();
}

Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다. 아래 코드에서 볼 수 있는 것처럼 코드 블록을 실행한 결과는 void다

//java.lang.Runnable 
public interface Runnable{
    void run();
}

Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.

Thread t = new Thread(new Runnable(){
    public void run(){
        System.out.println("Hello world");
    }
});

자바 8부터 지원하는 람다 표현식을 이용하면 다음처럼 스레드 코드를 구현할 수 있다.

Thread t = new Thread(()->System.out.println("Hello world"));

2.4.3 Callable을 결과로 반환하기

-> Runnable의 업그레이드 버전

//java.util.concurrent.Callable
  public interface Callable<V>{
    V call();
  }

테스크를 실행하는 스레드의 이름을 반환한다.

ExecutorService executorService = Executors.newCachedThreadPool();
  Future<String> threadName = executorService.submit(new java.util.concurrent.Callable<String>() {
    @Override
    public String call() throws Exception {
      return Thread.currentThread().getName();
    }
  });

람다를 이용하면 다음처럼 코드를 줄일 수 있다.

  Future<String> threadName2 = executorService.submit(
      ()->Thread.currentThread().getName()
  );

2.5 마치며

  • 동작 파라미터화에서는 매서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 매서드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
  • 코드 전달 기법을 이용하면 동작을 매서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
  • 자바 API의 많은 매서드는 정렬,스레드,GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.
반응형
LIST