자바/모던 자바 인 액션

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

Rudtjs 2022. 8. 31. 18:59

어떤 상황에서 일을 하든 소바자의 요구 사항은 항상 바뀐다. 시시각각 변하는 사용자 요구 사항에는 어떻게 대응해야 할까?

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

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.

 

 

2.1 녹색 사과 필터링

예제를 통하여 코드를 점차 개선하면서 유연한 코드로 만들어 보자.

  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.1 두 번째 시도: 색을 파라미터화

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

이렇게 호출하면 색깔별로 호출을 할 수 있을 것이다. 하지만 다른 요구(무게)로 파라미터화를 시키면 새로운 메서드를 만들어서 해결해야 한다.

 

새로운 메서드를 만들면 해결은 할 수 있지만, 코드가 대부분 중복될 것이다. 이는 DRY 원칙을 어기는 것이다.

 

 

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

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> green = filterApples(inventory , GREEN, 0 , true);

위 메서드는 값 , 무게, boolean으로 판단하여 사용할 수 있다. 

다른 개발자가 봤을 때 안에 들어있는 값이 어떤 걸 의미하는 지를 모르고, 요구사항이 바뀌었을 때 유연하게 대응할 수 없다. 

 

 

2.2 동작 파라미터화

이전 메서드들은 변화에 유연하게 대응할 수 없었다는 것을 확인했다. 이번에는 선택 조건을 boolean 함수를 이용하여 결정하는 인터페이스로 만들어보자.

 

 

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

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

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

선택 조건에 따라 여러 버젼으로 메서드를 정의할 수 있다. 위 조건처럼 filter 메서드가 다르게 동작하는 것을 전략 디자인 패턴 이라고 한다. 

  • 전략 디자인 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다. 
  • ApplePredicate가 알고리즘 패밀리, filter 처리한 메서드를 전략이라고 볼 수 있다.
  • filterApples에서 ApplePredicate 객체를 받아 조건을 검사하는 것을 동작 파라미터화 즉 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행하는 것이다.

 

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

 이제는 프레디케이트로 다양하게 만들어서 필터링이 가능해졌다. 즉 객체의 속성과 관련한 모든 변화에 대응할 수 있다는 것이다.

 

다양한 시도를 해보므로써 동작 파라미터화는 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리한다는 것이 큰 강점인걸 알 수 있었다. 따라서 유연한 API를 만들고 싶다면 동작 파라미터화를 이용해보자.

 

 

2.3 복잡한 과정 간소화

앞서 예제에서 사용했던 filterApples 메서드로 새로운 동작을 전달하려면 Predicate 인터페이스를 구현하는 여러 클래스를 정의하고 인터페이스화 해야 한다. 이는 상당히 번거로운 작업이다. 이걸 익명 클래스, 람다식등 다양한 방법으로 더 줄여보겠습니다.

익명 클래스는 말 그대로 이름이 없는 클래스이다. 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.

 

 

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

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

익명 클래스를 사용 하면 전에 비해 줄일 수는 있지만 여전히 많은 공간을 차지하고, 다른 프로그래머들이 익명 클래스 사용에 익숙하지 않다.

 

 

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

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

람다 표현식을 이용하면 코드가 훨씬 간결해지고 복잡성 문제를 해결할 수 있다. 람다식의 자세한 건 3장에서 다뤄보겠다.

 

 

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

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

이제 클래스, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다.

 

 

2.4.0 코드 전달 : Comparator로 정렬

개발자에게는 변화하는 요구사항에 쉽게 대응할 수 있는 다양한 정렬 동작을 수행할 수 있는 코드가 절실하다.

자바 8에서는 java.util.Comparator 객체를 이용해서 sort의 동작을 쉽게 파라미터화 할 수 있다.

// java.util.Comparator
public interface Comparaor<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());
        }
 });

클라이언트의 요구사항에  맞는 Comparator를 만들어 sort 메서드에 전달할 수 있다.

 

람다 표현식으로 만든다면 더 간결하게 만들 수 있다.

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

 

 

마치며

동작 파라미터화를 이용하면 변화하는 요구사항에 잘 대응할 수 있게 코드를 구현할 수 있고, 코드 전달 기법을 이용하여 코드를 보다 더 깔끔하게 만들 수 있다.