Java/모던자바인액션요약

동작 파라미터화 코드 전달하기

Junuuu 2022. 8. 6. 00:01
반응형

동작 파라미터화란?

아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미합니다.

 

등장 배경

어떤 상황에서 일을 하든 소비자의 요구사항을 항상 바뀔 수 있습니다.

 

농부가 재고목록 조사를 쉽게 할 수 있도록 애플리케이션을 만든다고 가정해보겠습니다.

 

초기의 농부의 요구사항은 다음과 같습니다.

 

"녹색 사과를 찾고 싶어요"

 

하지만 다음날에는 요구사항이 바뀝니다.

 

"150그램 이상인 사과를 모두 찾고 싶어요"

 

다음날에 또 요구사항이 바뀌었습니다.

 

"150그램 이상이면서 녹색인 사과를 모두 찾을 수 있으면 좋을 것 같아요"

 

이런 상황에 대응하기 위해서 동작 파라미터화가 등장하게 되었습니다.

 

동작 파라미터화를 통해 사용자의 요구사항에 대한 변화에 가장 최소화된 비용으로 대응할 수 있습니다.

 

 

코드를 통한 예시

동작 파라미터화를 적용하지 않은 상황에서 발생하는 코드들과 적용했을 때의 장점들을 알아보겠습니다.

 

기본적인 녹색 사과 필터링

사과 색을 정의하는 Color enum이 존재합니다.

 

enum Color {RED, GREEN}

 

 

녹색 사과를 필터링하는 코드입니다.

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

 

이때 농부가 빨간사과도 필터링하고 싶다면 어떻게 해야 할까요?

여기서는 색을 파라미터화하여 해결할 수 있습니다.

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

color를 인자로 받음으로써 해결할 수 있습니다.

 

 

이때 농부가 무게를 기준으로 필터링하고 싶다면 어떻게 해야 할까요?

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

 color 대신에 weight를 활용하여 weight를 파라미터화 해서 사용할 수 있습니다.

 

하지만 코드를 보면 위의 색을 기준으로 필터링할 때와 if문만 다르고 유사한 코드를 볼 수 있습니다.

 

즉, 코드가 반복되는 문제가 발생합니다.

 

만약 성능개선이 일어난다면 메서드 전체 구현을 고쳐야 하는 유지보수성이 떨어지게 됩니다.

 

이를 해결하기 위해서 flag를 두어 두 메서드를 합치는 방법 또한 존재하지만 true/false가 무엇을 의미하는지도 모르고 요구사항이 추가적으로 들어오는 경우에는 더 복잡해지는 경우가 발생할 수 있습니다.

 

 

 

동작 파라미터화 예시

위의 코드들의 문제점을 해결할 수 있는 방안인 동작 파라미터 화입니다.

 

바로 인터페이스를 활용하는 방법입니다.

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

참 또는 거짓을 반환하는 함수를 Predicate라고 합니다.

ApplePredicate 인터페이스는 Apple을 인자로 받고 boolean을 반환하는 추상 메서드인 test 메서드를 가지고 있습니다.

 

이제 위의 인터페이스를 활용하여 다양한 조건들을 설정할 수 있습니다.

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

 

위의 클래스들을 어떻게 활용할 수 있을까요??

public static List<Apple> filterApples(List<Apple> apples, ApplePredicate p){
	List<Apple> result = new ArrayList<>();
	for(Apple apple : apples){
		if(p.test(apple)){
			result.add(apple);        
		}
	}
    return result;
}

즉, ApplePredicate라는 동작을 파라미터로 받아 수행합니다.

 

이제 변화에 대응하기 위해서는 ApplePredicate 인터페이스를 구현하는 새로운 클래스만 있으면 됩니다.

 

 

단점 : 너무 많아지는 클래스

인터페이스를 구현해서 파라미터로 사용합니다.

즉, 클래스들이 여러 개 생길 수 있습니다.

이를 해결하기 위해서는 익명 클래스라는 기법을 활용할 수 있습니다.

 

익명 클래스를 활용한 클래스 줄이기

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

 

 

실제로 파라미터 인자로 ApplePredicate를 직접 구현해서 넣는 것이 아니라 인자에서 클래스를 구현하는 형태입니다.

 

 

더 나아가서 : 람다 표현식 사용

익명 클래스를 활용하게 되면 구현 클래스를 줄일 수 있습니다.

하지만 여전히 필요 없는 코드인 public boolean test(Apple apple){}과 같은 코드가 반복되게 됩니다.

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

 

하지만 만약 구현해야하는 코드가 길어진다면 익명 클래스를 활용하는 것도 하나의 방법입니다.

 

끝판왕 : 제너릭을 활용한 추상화

Apple 이라는 타입에 국한되지 않고 여러 타입을 위한 제너릭 <T>를 활용합니다.

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

 

추상화가 되면 Apple에 국한되지 않고 다음과 같이 활용할 수 있습니다.

List<Integer> evenNumbers = filter(numbers, (Integer i) -> i% 2 == 0 );