[JAVA] 동작 파라미터화 코드 전달하기
시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까?
엔지니어링적인 비용이 최소화되고 추가되는 기능을 쉽게 구현할 수 있으며 장기적으로 유지보수가 쉬워야 하는 방식으로!
자주 바뀌는 요구사항에 효과적으로 대응할 수있는 동작 파라미터화를 소개해본다.
동작 파라미터화 behavior parameterization
어떻게 실행할 것인지 결정하지 않은 코드 블록
풀어 설명하자면 어떤 동작을 할 수 있으나, 아직 안 하고 있는 코드 블록을 메서드의 파라미터로 넘기는 방법이다.
예시로 사과 농장을 보자.
농부는 수확한 사과 중 녹색 사과만 골라보려고 한다.
동작 파라미터화를 쓰지 않고 녹색 사과를 가져오는 방법은 다음과 같다.
public 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;
}
만일 농부가 도중에 변심하여 빨간 사과를 고르고 싶다고 하면 어떻게 해야 할까?
filterRedApples 메소드를 따로 만들어야 할까? 그 후에 다른 색의 사과를 또 요구한다면? 중복 코드가 늘어나게 될 것이다.
이를 해결하는 방법은 아래와 같이 색을 파라미터화 하는 것이다.
public 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;
}
그런데 농부가 또 변심을 했다. 이번엔 무게가 150g 이상인 사과를 고르고 싶다고 한다.
public 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;
}
색이 무게로 바뀌었다는 점만 빼면 코드 구조가 비슷하다. 또 중복이다! 이럴 순 없다.
나중에 농부가 빨갛고 무게가 150g 이상인 사과를 달라고 하면 어떻게 할 것인가?! 다른 요구사항이 더 추가되면 그 때는 어떻게 할 것인가?
이 문제를 해결하기 위해 닌자 동작 파라미터화가 나타났다. 한번 적용해보자.
빨갛다, 150g 이상이다, 등은 사과의 속성을 나타낸다.
그럼 빨갛고 150g 이상이라는 속성을 만족하게 하면 되는 것 아닌가?
전략 패턴을 적용해 속성을 만족한다 는 인터페이스를 만들어보자.
public interface ApplePredicate {
boolean test (Apple apple);
}
이제 우리가 원하는 빨갛고 150g 이상의 속성을 만족하는 ApplePredicate 클래스를 정의해보자.
public class filterRedAndHeavyApples implements ApplePredicate {
@Override
public boolean test (Apple apple) {
return RED.equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
우린 방금 하나의 동작을 정의했다! 이제 이걸 아래와 같이 파라미터화 하면 된다.
public List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
List<Apple> result = new ArrayList<>();
for ( Apple apple : inventory ) {
if ( predicate.test(apple) ) { // ApplePredicate 객체로 사과 검사 조건을 캡슐화했다.
result.add(apple);
}
}
return result;
}
위와 같은 방식으로 어떠한 동작을 할 수 있는 코드 블록를 파라미터로 받아 우리가 원하는 동작을 수행하도록 하는 게 동작 파라미터화다.
기초는 끝났다. 여기서부턴 동작 파라미터화를 더 간결하게, 더 추상적으로 만들기 위한 얘기이다.
Q. 위의 경우엔 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의하고 인스턴스화 해야한다. 너무 번거롭고 시간 낭비 같다.
A. 그렇다. 시간 낭비가 될 수 있다. 로직과 관련 없는 코드가 추가되기도 한다. 이를 개선하기 위해 익명 클래스가 있다.
익명 클래스 anonymous class
이름이 없는 클래스로, 클래스 선언과 인스턴스화를 동시에 할 수 있어 즉석에서 필요한 구현을 할 수 있다.
예시로 빨간 사과를 고르는 ApplePredicate를 아래와 같이 쓸 수 있다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
filterApples 메소드의 파라미터로 들어가는 ApplePredicate를 즉석에서 구현하여 인스턴스화 하는 것이다.
Q. 익명 클래스보다 더 간결한 것을 원한다.
A. 그럴 줄 알았다. 나도 그랬기 때문이다. 그런 우릴 위해 람다 표현식이 있다.
람다 표현식에 대해선 나중에 다룰테니 예시만 보여주겠다.
List<Apple> redApples =
filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
Q. 사과 말고 다른 과일이나 물건도 고를 수 있게 하고 싶다.
A. 리스트 형식으로 추상화를 해보자!
public interface Predicate {
boolean test (T t);
}
public List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for ( T value : list ) {
if ( predicate.test(value) ) {
result.add(value);
}
}
return result;
}
// 사용 예시
List<Apple> redApples =
filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
참고
[ 모던 자바 인 액션 ] Ch2. 동작 파라미터화 코드 전달하기