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

0

모던 자바 인 액션

목록 보기
1/1

개요

우리가 어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀐다. 변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제다. 시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까?

동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드블록은 나중에 프로그램에서 호출한다. 즉, 코드블록의 실행은 나중으로 미뤄진다.

예제


상황

사과를 사용자가 원하는 조건으로 분류하자.

사과 색은 열거형으로 정의한다.

enum Color {RED, GREED}

다음은 사과 클래스이다.

class Apple {
   Color color;
   int weight;
   
   public Color getColor() {
      return color;
   }
   
   public int getWeight() {
      return weight;
   }
}

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

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

만약 위 경우에서 갑자기 사용자가 빨간 사과를 찾고싶다면 어떻게 해야할까? 물론 filterRedApples라는 새로운 메서드를 만들고 if문의 조건을 수정하면 되겠지만, 나중에 사용자가 좀 더 다양한 색(파란색, 주황색 등) 으로 필터링하는 등의 변화에는 적절히 대응할 수 없다. 이런 상황에서는 다음과 같은 좋은 규칙이 있다.

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.

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

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

위 메서드는 다음가 같이 호출할 수 있다.

List<Apple> greenApples = filterApplesByColor(inventory, GREEN);
List<Apple> redApples = filterApplesByColor(inventory, RED);

근데 만약 이제 사용자가 사과를 무게로 구분하고 싶어하면 어떨까? 예를 들어 150g 이상의 사과만 분류하고 싶다면 어떻게 할까?

세 번째 시도: 무게로 필터링

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

위 코드로도 충분히 해결할 수 있다. 하지만 구현 코드를 자세히 보면 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다 이는 소프트웨어 공학의 DRY(don`t repeat yourself, 같은 것을 반복하지 말 것) 원칙을 어기는 것이다. 만약 위 코드에서 탐색 과정을 고쳐서 성능을 개선하려면 위에 메서드 전체 구현을 고쳐야 한다. 즉 엔지니어링적으로 나중에 비싼 대가를 치러야 한다.

그렇다면 어떻게 해결해야 할까?

네 번째 시도: 필터 속성 설정

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

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

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, null, 150, false);

딱 봐도 형편이 없는 코드이다. 대체 true와 false는 뭘 의미하는 걸까? 게다가 앞으로 요구사항이 바뀌었을 때 유연하게 대응하기도 힘들다. 예를 들어 사과의 크기, 모양, 생산지 등으로 필터링하고 싶다면? 심지어 녹색 사과 중에 무거운 사과를 필터링 하고 싶다면? 결국 여러 중복된 필터 메서드를 만들거나 아니면 모든 것을 처리하는 거대한 하나의 필터 메서들르 구현해야 한다.

동작 파라피터화


선택 조건을 다음처럼 결정할 수 있다. 사과의 어떤 속성에 기초해서 boolean 값을 반환하는 방법이 있다.(ex: 사과가 녹색인가?, 무게가 150 이상인가?) 참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다.

우선은 선택 조건을 결정하는 인터페이스를 정의하자.

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

위와 같은 기법을 전략 디자인 패턴(strategy design pattern)이라고 부른다. 전략 디자인 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리(ApplePredicate)를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.

다섯 번째 시도: 추상적 조건으로 필터링

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> greenApple = filterApples(inventory, new AppleGreenColorPredicate());

복잡한 과정 간소화 방법


사용하기 복잡한 기능이나 개념을 사용하고 싶은 사람은 아무도 없다. 현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화 해야 한다. 그 과정은 상당히 번거로우며, 주 로직과 관련이 없는 코드를 많이 추가해야 한다. 과연 이를 어떻게 개선할 수 있을까? 이는 익명 클래스 람다 표현식으로 코드의 양을 줄이면서 가독성을 높일 수 있다. 익명 클래스와 람다 표현식은 다음 게시글에서 알아보도록 하자.

참고


모던 자바 인 액션 - 전문가를 위한 자바 8,9,10 기법 가이드
(리올-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 지음)

0개의 댓글