※ 해당 내용은 JAVA8 in action 책을 공부하며 작성하였습니다.
동작의 파라미터화란?
아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록
동작 파라미터화의 효과
- 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. (아래는 구체적 효과)
- 엔지니어링 비용 효율화.
- 새로 추가한 기능을 쉽게 구현할 수 있음.
- 장기적 관점에서 유지보수 용이.
동작 파라미터화의 특징
- 어떻게 실행할지 결정되지 않은 코드블록을 나중에 프로그램에서 호출.
- 즉 코드블록의 실행은 나중으로 미뤄진다.
- 실행될 메서드를 파라미터로 넘겨준다.
예)
- 리스트의 모든 요소에 '어떤 동작'을 수행할 수 있음.
- 리스트 관련 작업을 끝낸 다음에 '어떤 다른 동작'을 수행할 수 있음.
- 에러가 발생하면 '정해진 어떤 다른 동작'을 수행할 수 있음.
변화하는 요구사항에 대응하기
위 특징을 기억하고 코드를 통해 동작 파라미터화가 진행되는 과정과 다양한 동작 파라미터화의 방법에 대해서 알아보자.
첫번째 시도. 녹색 사과 필터링
가정) 농장 재고목록 어플리케이션이 있다. 재고 리스트에서 녹색사과만 필터링하는 기능을 추가하자.
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> List = new ArrayList<>(); //<-필터링된 사과 리스트
for (Appple apple : inventory){
if ("green".equals(apple.getColor()){//<-녹색사과만 선택
result.add(apple);
}
}
}
위 코드를 보게되면 "green".equals(apple.getColor())를 통해 녹색 사과만 필터링하여 결과 List에 포함시키고 있음을 확인할 수 있다. 하지만 이때 농부가 빨간사과도 필터링하고 싶다면 어떨까? 아래와 같이 녹색 사과를 필터링하는 메서드를 복사하여, 빨간 사과를 필터링하는 메서드를 만들면 될 것이다.
public static List<Apple> filterRedApples(List<Apple> inventory){
List<Apple> List = new ArrayList<>(); //<-필터링된 사과 리스트
for (Appple apple : inventory){
if ("red".equals(apple.getColor()){//<-빨간사과만 선택
result.add(apple);
}
}
}
하지만 만약에 농부가 좀 더 다양한 색(옅은 녹색, 어두운 빨간색, 노란색 등)의 사과를 필터링해달라고 요청을 한다면?
위와같은 방식으로는 앞으로의 변화에는 효과적인 대응이 불가능 할 것이다.
이러한 상황에서는 "비슷한 코드를 구현한 다음에 추상화"하라 라는 좋은 규칙을 적용해 볼 수 있다.
두번째 시도. 색을 파라미터화
다양한 색을 필터링 해달라는 요청에 유연하게 대응하기 위해, 우리는 색을 파라미터화할 수 있다.
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color){
List<Apple> List = new ArrayList<>(); //<-필터링된 사과 리스트
for (Appple apple : inventory){
if (apple.getColor().equals(color)){//<-color와 일치하는 색의 사과만 선택
result.add(apple);
}
}
}
위 메서드를 아래와 같이 사용할 수 있다. 매우 간단하다!
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
//...
그런데 갑자기 농부가 와서 "색 외에 가벼운 사과와 무거운 사과로 구분할 수 있으면 좋겠네요~^^ 보통 무거운 사과는 150그램 이상이에요."라고 요구한다.
농부의 요구사항을 듣다보면 색과 마찬가지로 앞으로 무게의 기준도 얼마든지 바귈 수 있다는 사실을 눈치챘을 것이다. 그래서 다음과 같이 무게정보도 파라미터화 하여 추가시켜보았다.
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight){
List<Apple> List = new ArrayList<>(); //<-필터링된 사과 리스트
for (Appple apple : inventory){
if (apple.getWeight() > weight){//<-weight 조건 필터링
result.add(apple);
}
}
}
위 코드도 좋은 해결책이라 할 수 있지만 색깔을 파라미터로 필터링하는 코드와 많은 부분에서 코드 중복이 발생한다. (SW개발자로서 이런 부분은 본능적으로 신경이 쓰일 것이다!)
그렇다면 필터링하는 메서드를 filter로 통일시킨다면? 우리는 어떤 기준으로 필터링할지 flag로 기준을 정해주어야할 것이다. (매우 안좋은 방식!)
세번째 시도. 가능한 모든 속성으로 필터링
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color,
int weight, boolean flag){
List<Apple> List = new ArrayList<>(); //<-필터링된 사과 리스트
for (Appple apple : inventory){
//색이나 무게를 선택하는 방법이 맘에 들지 않는다.
if((flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight)){
result.add(apple);
}
}
}
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
//...
좋지 못한 코드이지만 한번 작성해본 모습니다. 우선 메서드를 사용할 때 true와 false는 무엇을 의미하는지도 불명확해 보인다. 그리고 녹색사과이면서 무거운 사과를 필터링하는 경우나 새로운 기준이 추가된다면? 위와 같이 flag로 표현을 하려면 말그대로 답이 없을 것이다.
결론
위 세가지 시도를 통해 요구조건의 변화에 어떻게 대응할 수 있을지 확인해보았다. 하지만 위 방법은 정확한 요구조건이 정의 되었을 경우 적용해볼 만 하지만, 유연성은 떨어지므로 시시각각 변하는 조건에 대응하기에는 비싼 방식이다.
동작 파라미터화
앞의 세가지 방법을 통해 우리는 요구사항에 조금 더 유연한 방식이 필요함을 느꼈다.
작성했던 코드들을 통해 우리는 filter 메서드가 대부분의 코드는 동일하였지만 필터링 조건 부분에서만 변화함을 알 수 있었다. (예를 들어 "녹색 사과인가?", "150 그램 이상인가?"등.) 이러한 동작은 Predicate라고 부르는데 이부분을 결정하는 인터페이스를 작성해보자.
public interface ApplePredicate {
boolean test (Apple apple);
}
위 ApplePredicate 인터페이스를 이용하여 아래와 같이 다양한 버전을 구현해낼 수 있다.
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());
}
}
우리는 위의 다양한 조건에 따라 filter메서드가 다르게 동작할 것이라는 것을 예상할 수 있다. 위 방식은 Strategy Design Pattern으로 런타임에 적절한 알고리즘을 선택하는 기법이다. 즉 AppleHeavyWeightPredicate나 AppleGreenColorPredicate 등의 구현체를 ApplePredicate라는 인터페이스로 캡슐화하고 실행 시점에 파라미터로 받은 구현체를 실행하는 것이다. (조건을 검사하는 것)
이러한 방식으로 동작 파라미터화, 즉 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
네번째 시도. 추상적 조건으로 필터링
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){//predicate객체로 검사 조건을 캡슐화
result.add(apple);
}
}
return result;
}
위와 같이 filterApples 메서드에서 필터 조건을 ApplePredicate로 캡슐화하여 앞선 코드보다 훨씬 유연성 있는 코드가 만들어 졌다.
filterApples메서드는 아래와 같이 사용하면 된다.
//빨간색에 무거운 사과 조건.
public class AppleRedAndHeavyPredicate implements ApplePredicate {
public boolean test(Apple apple){
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples = filter(inventory, new AppleRedAndHeavyPredicate());
이 방법에서 아쉬운 점은 filter에 조건을 넘겨주기 위하여 구현체(ex. AppleRedAndHeavyPredicate 클래스)를 만들고 new를 통해 객체를 생성하여 넘겨주는 번거로운 과정이 필요하다는 것이다. 수많은 종류의 조건이 추가될 것을 상상해보라.
다섯번째 시도. 익명클래스 사용
익명클래스를 이용하면 구현체 class를 정의해야하는 번거로움은 사라진다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
//메서드의 동작을 직접 파라미터화
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
하지만 여전히 사용할 때 많은 공간을 차지한다. 더불어 많은 개발자들이 익명클래스 사용에 익숙치 않은 문제도 있다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
//메서드의 동작을 직접 파라미터화
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
button.setOnAction(new EventHandler<ActionEvent>() {
//메서드의 동작을 직접 파라미터화
public void handler(ActionEvent event){
System.out.println("Clicked!");
}
});
여섯번째 시도. 람다 표현식 사용
자바8의 람다 표현식을 이용하면 위 예제 코드를 아래와 같이 간단하게 재구현 가능하다.
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
이전 코드보다 훨씬 간단해지고 문제를 더 잘 설명해주고 있는 코드가 되었다.
추가적으로 filterApple은 Apple과 관련된 동작만 수행하고 있는데 만약 다양한 물건에서 필터링이 동작하도록 하고 싶다면 어떻게 하면 좋을까?
일곱번째 시도. 리스트 형식으로 추상화
다양한 물건에서도 필터링이 작동하도록 하기 위하여 리스트 형식을 추상화 할 수 있다.
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;
}
위와 같이 filter를 정의한다면 Apple외에도 다양한 물건을 filter할 수 있다. 위 코드에서 T는 객체 타입을 의미한다. 따라서 Apple이 될 수도 있고 Banana가 될 수도 있다.
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<String> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
요약
여러 스텝을 통해 요구사항 변화에 적합하지 않은 케이스와 동작 파라미터화의 적용으로 변화에 유연하게 대응하는 방법들을 알아보았다.
실제로 변화에 유연하지 못한 코드를 많이 사용하고 있었는데, 이것들을 어떻게 리팩토링할 수 있을지에 대한 insight를 얻을 수 있었다.
배운 내용을 wrap up 하면 아래와 같다.
- 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.