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

SunYerim·2024년 1월 23일
0

언어

목록 보기
2/11
post-thumbnail

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

이는 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.

동작 파라미터화를 추가하려면 쓸데없는 코드가 늘어나게 되는데, 이는 람다 표현식으로 해결한다.


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

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

  • 사과 색을 정의하는 Color enum class
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;
}
  • 이때, 녹색 사과 말고 빨간 사과로 필터링하고 싶어졌다면? 다른 여러가지 색깔 사과로 필터링 하고싶다면?

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

여기서 ‘색 이외에도 사과를 무게로도 구분하면 좋겠다’라는 요구사항이 생긴다면? → 무게 파라미터를 추가하면 된다.

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

그러나 이는 형편 없는 코드다. boolean값이 무엇을 의미하는지 알 수가 없기 때문이다.

문제가 잘 정의되어 있는 상황에서는 문자열, 정수 불리언 등의 값으로 filterApples 메서드를 파라미터화 한 것이 잘 동작할 수 있다. 하지만 filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것이다.

다음 절에서 알아본다.


2.2 동작 파라미터화

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

public interface ApplePredicate {
	boolean test (Apple apple);
}
  • 무거운 사과만 선택
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 메서드가 다르게 동작할 것인데, 이를 전략 디자인 패턴이라고 부른다.

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

메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다. (동작 파라미터화)

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

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

코드/동작 전달하기

  • 필요한 대로 다양한 ApplePredicate를 만들어서 filterApples 메서드로 전달할 수 있다.
public class AppleRedAndHeavyPredicate implements ApplePredicate {
	public boolean test(Apple apple) {
		return RED.equals(apple.getColor()) && apple.getWeight() > 150;
	}
}

List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
  • 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다.

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

컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 한 메서드가 다른 동작을 하도록 재활용할 수 있다. ⇒ 유연한 API를 만들 때 동작 파라미터화가 중요한 역할을 한다.


2.3 복잡한 과정 간소화

자바는 클래스의 선언과 인스턴스화를 동시에 할 수 있도록 익명 클래스라는 기법을 제공한다. 이를 이용하면 코드의 양을 줄일 수 있다.

2.3.1 익명 클래스

익명 클래스는 자바의 지역 클래스와 비슷한 개념이다. 이는 이름이 없는 클래스인데 클래스 선언과 인스턴스화를 동시에 할 수 있다. ⇒ 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

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

익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현한 코드이다.

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

그러나 익명 클래스는 많은 공간을 차지한다는 단점이 있다. 또 많은 프로그래머가 익명 클래스의 사용에 익숙치 않다.

동작 파라미터화를 이용하면 요구사항 변화에 더 유연하게 대응할 수 있으므로 동작 파라미터화를 사용하도록 권장한다.

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

람다 표현식을 사용하면 2.3.2의 코드를 간단하게 재구현 할 수 있다.

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

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

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 : result) {
		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);

2.4 실전 예제

동작 파라미터화 패턴은 동작을 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화 한다.

2.4.1 Comparator로 정렬하기

컬렉션 정렬은 반복되는 프로그래밍 작업이다. 변화하는 요구사항에 쉽게 대응할 수 있는 다양한 정렬 동작을 수행할 수 있는 코드가 필요하다.

List에는 sort 메서드가 포함되어 있다. 아래와 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort 동작을 파라미터화할 수 있다.

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

Comparator을 구현해서 sort 메서드의 동작을 다양화할 수 있다.

2.4.2 Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 여러 스레드가 각자 다른 코드를 실행할 수 있는데, 나중에 실행할 수 있는 코드를 구현할 방법이 필요하다.

자바에서는 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"));

2.4.3 Callable을 결과로 반환하기

Callable 인터페이스를 활용해 결과를 반환하는 태스크를 만든다. 이 방식은 Runnable의 업그레이드 버전이다.

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

실행 서비스를 태스크에 제출해서 위의 코드를 활용할 수 있다. 아래의 예제 코드는 태스크를 실행하는 스레드의 이름을 반환한다.

ExecutorService executorService = Executors.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());

2.4.4 GUI 이벤트 처리하기


💡 정리 💡

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있다.
  • 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 인터페이스를 상속받아 여러 클래스를 구현해야되는 번거로움을 없앨 수 있는 방법을 제공한다.
  • 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.
profile
내 안에 있는 힘을 믿어라.

0개의 댓글