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

김성혁·2022년 2월 21일
0

모던 자바 인 액션

목록 보기
2/7
post-thumbnail

소비자의 요구사항을 항상 바뀐다. 변화하는 요구사항 속 엔지니어링적인 비용이 가장 최소화되어야 하며, 새로 추가한 기능은 쉽게 구현할 수 있어야 하고, 장기적인 관점에서 유지보수가 쉬워야 한다.

👨🏻‍💻 동작 파라미터화란?

  • 동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있습니다.
  • 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록
  • 변화하는 요구사항에 유연하게 대처하기 위해 동작을 파라미터화시켜 메서드의 인수로 전달합니다.

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


첫 번째 시도: 녹색 사과 필터링
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;
}

그런데 갑자기 농부가 다시 나타나서는 '색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있다면 좋겠네요. 보통 무게가 150그램 이상인 사과가 무거운 사과입니다'라고 요구합니다.

public static List<Apple> filterAppleByColor(List<Apple> inventory, int weight) {
	List<Apple> result = new ArrayList<>();
	for(Apple apple: inventory) {
		if(apple.getWeight() > weight) {
			result.add(apple);
		}
	}
	return result;
}

위 코드도 좋은 해결책이라 할 수 있지만, 구현 코드를 자세히 보면 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복됩니다. 이는 소프트웨어 공학의 DRY(같은 것을 반복하지 말 것) 원칙을 어기는 것입니다.

색과 무게를 filter라는 메서드로 합치는 방법도 있지만, 어떤 기준으로 사과를 필터링할지 구분하는 또 다른 방법이 필요합니다. 따라서 색이나 무게 중 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가할 수 있습니다.


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

public static List<Apple> filterAppleByColor(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;
}

형편없는 코드입니다. 대체 true와 false가 뭘 의미하는 걸까요? 게다가 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수도 없습니다.

동작 파라미터화

  • 선택 조건을 결정하는 인터페이스를 정의하고 참 또는 거짓을 반환하는 함수,프레디케이트,를 사용하자
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());
	}
}

다음과 같이 사과 선택 전략을 캡슐화하였습니다. 이를 **전략 디자인 패턴**이라고 부릅니다. 전략 디자인 패턴은 각 알고리즘(전약이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법입니다.

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


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

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

filterApples 메서드의 새로운 동작을 정의하는 것은 test 메서드입니다. 안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 합니다. 람다를 이용하면 여러 개의 ApplePredicate 클래스를 정의하지 않고도 표현식을 filterApples 메서드로 전달할 수 있습니다.

지금까지 동작을 추상화해서 변화하는 요구사항에 대응할 수 있는 코드를 구현하는 방법을 살펴봤다. 하지만 여러 클래스를 구현해서 인스턴스화하는 과정이 조금은 거추장스럽게 느껴질 수 있는데 이 부분을 어떻게 개선할 수 있는지 확인해보자.

인터페이스를 정의하고 여러 구현 클래스를 정의한 다음에 인스턴스화하는 작업은 상당히 번거로운 작업이며 시간 낭비입니다.

익명 클래스

  • 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공합니다.
  • 익명 클래스를 이용하면 코드의 양을 줄일 수 있습니다.
  • 하지만 익명 클래스가 모든 것을 해결하는 것은 아닙니다.
  • 말 그대로 이름이 없는 클래스

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

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

익명 클래스의 단점
  • 많은 공간을 차지합니다.
  • 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않습니다.

장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐 아니라 읽는 즐거움을 빼앗는 요소로, 개발자로부터 외면받습니다. 한눈에 이해할 수 있어야 좋은 코드입니다.

익명 클래스로 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금 줄일 수 있지만 코드 조각을 전달하는 과정에서 결국은 객체를 만들고 명시적으로 새로운 동작을 정의하는 메서드를 구현해야 한다는 점은 변하지 않습니다. 람다 표현식을 이용해서 어떻게 코드를 간결하게 정리할 수 있는지 간단히 살펴보겠습니다.


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

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

0개의 댓글