[모던 자바 인 액션] 02. 동작 파라미터화 코드 전달하기

seony·2023년 2월 26일

모던 자바 인 액션

목록 보기
2/6
post-thumbnail

동작 파라미터화 를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

  • 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미한다.
  • 즉, 코드 블럭의 실행은 나중으로 미뤄진다.
    ➡️ 코드 블럭에 따라 메서드의 동작이 파라미터화된다.

2.1 변화하는 요구사항에 대응하기

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

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);
        }
    }
    return result;
}
  • 녹색 사과 말고 빨간 사과도 필터링하고 싶어졌다.

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

  • 색을 파라미터화할 수 있도록 메서드에 파라미터를 추가
public static List<Apple> filterAppleByColor(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 = filterAppleByColor(inventory, GREEN);
List<Apple> redApples = filterAppleByColor(inventory, RED);
  • 색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있다면 좋겠다고 한다...
  • 보통 무게가 150그램 이상인 사과가 무거운 사과라고 요구
public static List<Apple> filterAppleByWeight(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) 원칙 어김

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

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);
  • 매우 형편 없는 코드
    - truefalse
    • 앞으로 요구사항이 바뀌면 유연하게 대응 X

2.2동작 파라미터화

  • 참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다.
  • 선택 조건을 결정하는 인터페이스를 정의하자.
public interface ApplePredicate {
	boolean test(Apple apple);
}

<무거운 사과만 선택>

publc 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 메서드가 다르게 동작할 것이라고 예상할 수 있다.
  • 이를 전략 디자인 패턴이라고 한다.
    • 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법
      • ApplePredicate가 알고리즘 패밀리
      • AppleHeavyWeightPredicateAppleGreenColorPredicate가 전략

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

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);
        }
    }
}
  • 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정
  • 즉, filterApples 메서드의 동작을 파라미터화한 것이다.

  • 가장 중요한 구현은 test 메서드
  • 안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다.
    • 추후 람다를 이용해서 여러 개의 ApplePredicate 클래스를 정의하지 않고 전달

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

  • 동작 파라미터화의 강점
    • 컬렉션 탐색 로직, 각 항목에 적용할 동작을 분리


2.3 복잡한 과정 간소화

  • 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스를 제공한다.

익명 클래스

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

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

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

익명 클래스로도 아직 부족한 점

  1. 익명 클래스는 여전히 많은 공간을 차지한다.
    • 사실 return 부분만 있어도 충분
  2. 많은 프로그래머가 익명 클래스의 사용에 익숙치 않다.

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

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(number, (Integer i) -> i % 2 == 0);
  • 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있다.

2.4 실전 예제

  • 동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화한다(예를 들면 사과의 다양한 프레디케이트).
  • 자바 API의 많은 메서드를 다양한 동작으로 파라미터화할 수 있다.
  • 또한 이들 메서드를 익명 클래스와 자주 사용하기도 한다.
    • Comparator로 정렬하기
    • Runnable로 코드 블록 실행하기
    • Callable을 결과로 반환하기
    • GUI 이벤트 처리하기

Comparator로 정렬하기

  • 다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화할 수 있다.
// java.util.Comparator
public interface Comparator<T> {
	int compare(T o1, T o2);
}

<익명 클래스를 이용해서 무게가 적은 순서로>

inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
    	return a1.getWeight().compareTo(a2.getWeight());
    }
});

<람다 표현식 사용>

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

Runnable로 코드 블록 실행하기

  • 자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 어떤 코드를 실행할 것인지를 스레드에게 알려줄 수 있을까?
    • Thread 생성자에 객체만을 전달할 수 있었음.
    • 보통 결과를 반환하지 않는 void run 메소드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 것이 일반적인 방법이었다.
// java.lang.Runnable
public interface Runnable {
	void run();
}
  • Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.
Thread t = new Thread(new Runnable() {
	public void run() {
    	System.out.println("Hello world");
    }
});

<람다 표현식>

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

Callable을 결과로 반환하기

  • Callable 인터페이스를 이용해 결과를 반환하는 태스크를 만든다.
    • Runnable의 업그레이드 버전이라고 생각할 수 있음
// java.util.concurrent.Callable
public interface Callabe<V> {
	V call();
}
  • 실행 서비스에 태스크를 제출해서 위 코드를 활용할 수 있다.
  • 다음 예제는 태스크를 실행하는 스레드의 이름을 반환하다.
ExecutorService executorService = Executor.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
	@Override
    public String call() throws Exception {
    	return Thread.currentThread().getName();
    }
});

<람다 사용>

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

GUI 이벤트 처리하기

Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
	public void handle(ActionEvent event) {
    	label.setText("Sent!");
    }
});
  • EventHandlersetOnAction 메서드의 동작을 파라미터화한다.

<람다 표현식>

button.setOnAction((ActionEvent event) -> label.setText("Sent!"));

2.5 마치며

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
  • 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다.
  • 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.

0개의 댓글