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

이 내용은 모던 자바 인 액션 책을 읽고 개인적으로 정리한 내용입니다.

동작 파라미터화

behavior parameterization이란 코드 블록 자체를 파라미터로 삼아서 메서드로 전달하는 것을 말한다. 이를 통해 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 이번 장에서는 동작 파라미터화에 대해서 알아본다.

각 예제에 대한 소스코드는 깃에서 볼 수 있다.

고전적인 filter 방법

  • 방법1: for 과 if 를 사용해서 리스트에 담아서 리턴
    • 빨간 사과를 필터링 하려면 filterRedApples 메서드를 또 만들어야 함
  • 방법2: Color를 파라미터로 넘김
    • 하지만, 칼러가 아니라 무게로 필터링을 해야하면 또 filterApplesByWeight 라는 메서드를 만들어야 된다.
  • 방법3: 플래그 값으로 가능한 모든 속성으로 필터링
    • 정말 좋지 않은 방법이다. 다른속성이 들어오면 또 어떻게 할 것인가

결국 여러 중복된 필터 메서드를 만들거나 아니면 모든것을 처리하는 거대한 하나의 필터 메서드를 구현해야 한다. 하지만 동작 파라미터화로 보다 유연하게 코드를 구현할 수 있다.

Predicate

참 또는 거짓을 반환하는 함수를 Predicate라고 한다. 아래와 같이 사과의 필터 조건을 선택하는 인터페이스를 정의하자

/**
 * Predicate 선언
 * @author eldorado
 *
 */
public interface ApplePredicate {
  boolean test(Apple apple);
}

/**
 * 필터조건 1 : 무거운 사과만 선택
 * @author eldorado
 *
 */
public class AppleHeavyWeightPredicate implements ApplePredicate {
  @Override
  public boolean test(Apple apple) {
    return apple.getWeight() > 80;
  }		
}

/**
 * 필터조건 2 : 초록 사과만 선택
 * @author eldorado
 *
 */
public static class AppleGreenColorPredicate implements ApplePredicate {
  @Override
  public boolean test(Apple apple) {
    return Color.GREEN.equals(apple.getColor());
  }		
}

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 void main(String[] args) {
  Apple[] appleList = {
              new Apple(Color.GREEN, 100),
              new Apple(Color.GREEN, 30),
              new Apple(Color.GREEN, 40),
              new Apple(Color.RED, 50),
              new Apple(Color.RED, 80),
              new Apple(Color.GREEN, 10)
          };

  System.out.println(filterApples(Arrays.asList(appleList), new AppleGreenColorPredicate()));

}

조건을 뜻하는 predicate를 파라미터로 넘겨서 test 한 조건에 true이면 result 에 담는다.

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

ApplePredicate 라는 객체에 test() 메서드를 감싸서, 메서드에 파라미터로 전달했다. 람다를 이용하면 ApplePredicate 클래스를 정의하지 않고도 핵심 조건식 (Color.GREEN.equals(apple.getColor())) 을 바로 전달할 수 있다.

여러개의 Predicate 클래스 간소화 : 익명 클래스

위와 같이 Predicate로 유연성을 높였지만, 항상 ApplePredicate 인터페이스를 구현하는 구현 클래스를 만들어야 하는 것이 부담스럽다.

익명클래스를 이용해서 클래스의 선언과 인스턴스화를 동시에 수행할 수 있다. 그러므로써 코드의 양을 줄일 수 있다!

System.out.println(filterApples(Arrays.asList(appleList), new ApplePredicate() {			
  @Override
  public boolean test(Apple apple) {
    return Color.RED.equals(apple.getColor());
  }
}));

이렇게 익명클래스를 사용해서 코드 양을 줄였지만, 그래도 @Override 코드가 길어보인다. 핵심은 Color.RED.equals(apple.getColor()) 이 부분이니까 말이다. 이것을 람다로 바꿔보자!

더 간소하게! : 람다

람다 표현식을 이용하면 더 간단하게 구현할 수 있다. 딱 필터링할 조건만 명시함으로써 코드도 명확해졌다.

System.out.println(filterApples(Arrays.asList(appleList), (Apple apple) -> Color.RED.equals(apple.getColor())));

메소드를 람다 표현식으로 표현하면, 클래스를 작성하고 객체를 생성하지 않아도 메소드를 사용할 수 있습니다. 그런데 자바에서는 클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성할 수 있는 클래스를 익명 클래스라고 합니다. 따라서 자바에서 람다 표현식은 익명 클래스와 같다고 할 수 있습니다.

람다 표현식 : (매개변수목록) -> { 함수몸체 }

제네릭을 사용해서 filter 메서드를 확장

현재 우리의 filter 메서드는 Apple 클래스만 사용할 수 있다.

List<Apple> filterApples(List<Apple> inventory, ApplePredicate p)

이것을 제네릭을 사용해서 모든 타입에 적용가능 하도록 만들어 보자

/**
 * 제네릭을 사용한 Predicate
 * @author eldorado
 *
 * @param <T>
 */
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;
}

이제 Apple 클래스 뿐만 아니라, Integer, String과 같은 다른 타입의 filter로도 사용할 수 있다.

실전 예제

Comparator로 정렬하기

정렬기준은 항상 달라진다. 어떤 기준으로 정렬할 지에 대한 코드를 파라미터화 해서 메서드의 동작을 유연하게 구현해보자

비교를 위해서 java.util.Comparator 인터페이스를 구현한 객체를 이용한다. List의 sort 메서드를 사용하고, 파라미터로 Comparator 익명 클래스를 전달한다.

Arrays.asList(appleList).sort(new Comparator<Apple>() {
  @Override
  public int compare(Apple o1, Apple o2) {
    return o1.getWeight().compareTo(o2.getWeight());
  }			
});

람다식으로 표현하면 다음처럼 간단하게 구현할 수 있다.

Arrays.asList(appleList).sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 보통 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 Runnable 인터페이스를 구현한 객체 를 전달하는 것이 일반적인 방법이었다.

하지만 람다로 간단하게 스레드에서 수행할 메서드를 지정할 수 있다.

Thread t = new Thread(new Runnable() {			
  @Override
  public void run() {
    System.out.println("Hi Five!");

  }
});

Thread lamda_t = new Thread(() -> System.out.println("Hello Lamda!"));

GUI 이벤트 처리하기

ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다. (배치와 관련 있는 듯하다) ExecutorService를 사용하면 태스크를 스레드 풀로 보내고 Future로 저장할 수 있다는 점이 스레드와 Runnable을 이용하는 방식과 다르다. (뒷부분에서 더 살펴볼것이다.)

지금은 Callable만 알아두자

Callable 인터페이스 : 결과를 반환하는 태스크

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

아래 코드는 실행 서비스에 태스크를 제출해서 현재 태스크를 실행하는 스레드의 이름을 반환한다.

ExecutorService executorService =  Executors.newCachedThreadPool();

// 현재 수행하고 있는 스레드의 이름을 반환한다.
Future<String> threadName = executorService.submit(new Callable<String>() {

  @Override
  public String call() throws Exception {
    return Thread.currentThread().getName();
  }
});

System.out.println(threadName.get()); // pool-1-thread-1

람다로 작성하면 아래와 같다.

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

이런 코드는 병렬 배치 플랫폼에서 많이 쓰이는 것 같다.

Summary

  • 동작 파라미터화는 메서드 내부적으로 다양한 동작을 유연하게 할 수 있도록 코드를 메서드 인수로 전달하는 것이다.
  • 코드 전달 기법(람다)을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 익명 클래스로도 구현할 수 있지만 람다를 이용하면 더욱 깔끔하다
  • 자바API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.

댓글남기기