[Java] 동적 파라미터

조민서·2022년 12월 13일
3

JAVA

목록 보기
9/16
post-thumbnail

우리가 어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀐다.
예를 들어 농부가 재고목록 조사를 쉽게 할 수 있도록 돕는 애플리케이션이 있다고 가정하자.

농부는 말한다.
농부: 녹색 사과를 모두 찾고 싶어요.

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

enum Color {RED, GREEN}

public static List<Apple> filterGreenApples(List<Apple> inventory) {
	List<Apple> result = new ArrayList<>();
	for (Apple apple : inventory) {
		if (Color.GREEN.equals(apple.getColor())) { // 녹색 사과만 선택
			result.add(apple);
		}
	}
	return result;
}

if문은 녹색 사과를 선택하는 데 필요한 조건을 가리킨다. 그런데 갑자기 농부가 변심하여 녹색 사과 말고 빨간 사과도 필터링 하고싶어졌다. 어떻게 고쳐야 할까? 크게 고민하지 않은 사람이라면 메서드를 복사해서 filterRedApples라는 새로운 메서드를 만들어 if문의 조건을 빨간 사과로 바꾸는 방법을 선택할 수 있다. 하지만 나중에 농부가 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절하게 대응할 수 없다.


그리고 다음날 농부는 말한다.
농부: 빨간 사과를 모두 찾고 싶어요.

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

public static List<Apple> filterGreenApples(List<Apple> inventory, Color color) {
	List<Apple> result = new ArrayList<>();
	for (Apple apple : inventory) {
		if (color.equals(apple.getColor())) {
			result.add(apple);
		}
	}
	return result;
}

이제 농부도 만족할 것이다. 다음처럼 구현한 메서드를 호출할 수 있다.

List<Apple> greenApples = filterGreenApples(inventory, Color.GREEN);
List<Apple> redApples = filterGreenApples(inventory, Color.RED);

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

그리고 다음날 농부는 또 말한다.
농부: 음.. 색은 상관없고 무게가 150g 이상인 사과를 모두 찾고 싶어요.

public static List<Apple> filterApplesByWeight(List<Apple> inventory, Integer 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, Integer 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;
}

다음처럼 위 메서드를 사용할 수 있다.

filterApples(inventory, Color.GREEN, 0, true);
filterApples(inventory, null, 150, false);

나의 학부생 1, 2학년때의 코드를 보는거 같다. 정말 형편없다. 대체 flag의 true와 false는 뭘 의미하는가? 앞으로 요구사항이 바뀌었을때 유연하게 대응할 수 없다. 예를 들어 녹색 사과중에 무거운 사과를 필터링하고 싶다면? 이제는 동작파라미터화를 이용해서 이런 더러운 코드는 잊고 유연한 코드를 얻어보자.


네 번째 시도: 추상적 조건으로 필터링 (동작 파라미터화)

위에서 파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법을 알아 볼 필요가 있음을 깨달았다.

우리는 사과의 선택 조건을 사과의 어떤 속성에 기초해서 불리언값을 반환(예를들어 사과가 녹색인가? 150그램 이상인가?)하는 방법이 있다.

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

> ApplePredicate.java

// 선택 조건을 결정하는 인터페이스(Predicate)
public interface ApplePredicate {
    boolean test(Apple apple);
}

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

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

> Main.java (class)

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;
}
    
List<Apple> result = filterApples(inventory, new AppleHeavyWeightPredicate());

AppleHeavyWeightPredicate, AppleGreenColorPredicate 각각의 조건에 따라 filter 메서드가 다르게 동작할 것을 예상할 수 있다. 이를 전략 디자인 패턴 이라고 한다.

전략 디자인 패턴은 각 알고리즘 (전략이라 불리는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다. ApplePredicate가 알고리즘 패밀리고,
AppleHeavyWeightPredicateAppleGreenColorPredicate가 전략이다.

농부는 말한다.
농부: 빨갛고 150g이 넘는 사과를 찾아주세요.

우리는 당황하지 않고 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다.

class AppleRedAndHeavyPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return Color.RED.equals(apple.getColor()) 
                && apple.getWeight() > 150;
    }
}

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

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

class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return Color.GREEN.equals(apple.getColor());
    }
}

class AppleRedAndHeavyPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return Color.RED.equals(apple.getColor())
                && apple.getWeight() > 150;
    }
}

음.. 로직과 관련 없는 코드가 많이 추가 됐다. 익명 클래스(anonymous class)를 이용해보자.

익명 클래스는 말 그대로 이름이 없는 클래스다.
익명 클래스를 이용하면 클래스의 선언과 인스턴스화를 동시에 할 수 있다.
즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

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

ApplePredicate를 구현한 클래스를 만들지 않고 메서드의 동작을 직접 파라미터화 했다.

하지만 익명클래스는 여전히 많은 공간을 차지한다.
그리고 익명클래스를 중첩해서 쓰다보면 코드가 장황해진다. 장황한 코드는 구현하고 유지보수하는데 시간이 오래걸릴 뿐 아니라 다른 개발자한테 놀림 받을 것이다.


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

List<Apple> result = filterApples(inventory, (Apple apple) -> Color.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) -> Color.RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> i % 2 == 0);

동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화 한다. (예를 들면 사과의 다양한 프레디케이트)

자바 API의 많은 메서드를 다양한 동작으로 파라미터화 할 수 있다. 대표적으로 Comparator, Runnable 인터페이스가 있다.


Comparator로 정렬하기

농부는 말한다.
농부: 사과를 무게별로 정렬 해주세요.

하지만 우리는 농부가 다음날 이렇게 말할 것을 알고있다.

농부: 사과를 무게가 아닌 색을 기준으로 정렬 해주세요.

Predicate처럼 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 해보자.

익명 클래스

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

익명 클래스를 이용해서 무게가 적은 순서로 사과를 정렬할 수 있다.

농부의 요구사항이 바뀌면 새로운 요구사항에 맞는 Comparator를 만들어 sort 메서드에 전달 하면 된다.
실제 정렬 세부사항은 추상화 되어 있으므로 신경 쓸 필요가 없다.

람다 표현식

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

람다 표현식을 이용하면 이처럼 간단하게 코드를 구현할 수 있다.

자바 Intger.java소스코드

// interface Comparable
public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
}

// interface Comparator
public static int compare(int x, int y) {
	return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

Comparable 인터페이스 compareTo()를 호출하면 내부적으로 Comparator 인터페이스 compare() 호출됩니다.

profile
내 두뇌는 휘발성 메모리다. 😪

0개의 댓글