티스토리 뷰

728x90

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

동작 파라미터화 를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.
이 코드 블록은 나중에 프로그램에서 호출한다. 즉 코드 블록의 실행은 나중으로 미뤄진다.

변화하는 요구사항에 대응하기

첫 번째 시도

농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링 하는 기능을 추가

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

녹색 말고 빨간색 사과로 바꾸고 싶다면?

똑같은 메소드를 복사해서 green 을 red로 바꾸면 되겠지만 그렇게 되면 다른 색으로 변경할 때마다 계속 같은 메서드를 추가해야함.

변화에 대해서 적절하게 대응할 수 없게된다.

이럴 경우 다음과 같은 규칙을 적용할 것. 비슷한 코드를 구현한 다음에 추상화하라

두 번째 시도

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

가장 쉽게 생각할 수 있는 메서드에 파라미터를 추가해서 분기해보자!

색을 파라미터화 해서 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 코드를 작성

지금 까지는 색으로 구분했지만 색 이외에도 무게를 구분했으면 좋겠다고 한다.

방금 색과 마찬가지로 무게도 무거운 사과와 가벼운 사과의 무게가 변화할 수 있기 때문에 다양한 무게에 대응할 수 있도록 무게 정보 파라미터도 추가

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

무게에 대응할 수 있는 코드를 만들고보니 구현부의 코드가 색을 구분하는 코드와 굉장히 중복된다는 것을 알 수 있다.

프로그래밍의 원칙 중 중복을 제거해야 될 것 같다.

색과 무게를 구분할 수 있는 flag값을 넣으면 어떨까?

세 번째 시도

public static List<Apple> filterApplesByWeight(List<Apple> inventory, String 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, "", 150, false);

파라미터가 가독성이 좋지않고, true, false가 무엇을 의미하는지 잘 모르겠는 코드이다.

앞으로 요구사항이 바뀔때마다 유연하게 대응할 수도 없다.

결국 여러 중복된 필터 메서드를 만들거나 아니면 모든 것을 처리하는 거대한 하나의 필터 메서드를 구현해야 할 것이다.

fillterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것같다.

동작 파라미터화

이 전에 파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 절실하다는 것을 확인했다.

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

이와 같은 동작을 프레디케이트(Predicate) 라고 한다.

선택 조건을 결정하는 인터페이스 를 생성

interface ApplePredicate {
    public boolean test(Apple a);
}
public static class AppleWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
public static class AppleColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

예제에서는 ApplePredicate 가 전략 객체이고 AppleGreenColorPredicate와 AppleHeavyWeightPredicate가 전략이다.

filterApples에서 ApplePredicate 객체를 받아 애플의 조건을 검사하도록 메서드를 고쳐야한다.

이렇게 동작 파라미터화 즉, 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고쳐보자.

이렇게 함으로써 filterApples 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작(우리 예제에서는 프레디케이트)을 분리할 수 있다는 점에서 소프트 웨어 엔지니어링적으로 큰 이득을 얻는다.

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

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

코드/동작 전달하기

이제 필요한 대로 다양한 ApplePredicate를 만들어서 filterApples 메서드로 전달할 수 있다.

예를 들어 농부가 150그램이 넘는 빨간 사과를 검색해달라고 하면 ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다.

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

List<Apple> redAndHeavyApples = filterAplles(inventory, new AppleRedAndHeavyPredicate());

우리가 전달한 ApplePredicate 객체에 의해 filter 메서드의 동작이 결정된다. 즉 filter 메서드의 동작을 파라미터화한 것이다!!

위 예제에서 가장 중요한 구현은 test 메서드다.

filter메서드의 새로운 동작을 정의하는 것이 test 메서드다.

안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다.

test 메서드를 구현하는 객체를 이용해서 불린 표현식 등을 전달할 수 있으므로 이는 '코드를 전달'할 수 있는것이나 다름없다.

한 개의 파라미터, 다양한 동작

지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.

한 메서드가 다른 동작을 수행하도록 재활용할 수 있다.

복잡한 간소화 과정

filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화 하는 작업이 상당히 번거롭다.

//무거운 사과를 선택하는 프레디케이트
static class AppleWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
//녹색 사과를 선택하는 프레디케이트
static class AppleColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

public class FilteringApples {

    public static void main(String... args) {

        List<Apple> inventory = Arrays.asList(new Apple(80, "green"),new Apple(155, "green"),new Apple(120, "red"));

        // [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
        List<Apple> greenApples = filterApples(inventory, new AppleColorPredicate());
        System.out.println(greenApples2);
                // 결과 리스트는 녹색 사과 2개를 포함한다.

        // [Apple{color='green', weight=155}]

        List<Apple> heavyApples = filterApples(inventory, new AppleWeightPredicate());
        System.out.println(heavyApples);
                //결과 리스트는 155그램의 사과 1개를 포함한다.
    }

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

자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스 라는 기법을 제공한다.

익명 클래스를 이용하면 코드의 양을 줄일 수 있다. 하지만 익명 클래스가 모든 것을 해결하는 것은 아니다.

익명 클래스

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

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

익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 에제를 다시 구현한 코드

// [Apple{color='red', weight=120}]
List<Apple> redApples2 = filter(inventory, new ApplePredicate() {
    public boolean test(Apple a) {       //filterApples 메서드의 동작을 직접 파라미터화 했다.
        return a.getColor().equals("red");
    }
});

익명 클래스는 여전히 많은 코드를 사용하고 반복되어 지저분한 코드가 나온다.

코드의 장황함은 나쁜 특성이다.

장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐 아니라 읽는 즐거움을 빼앗는 요소로, 개발자로부터 외면받는다. 한눈에 이해할 수 있어야 좋은 코드다.

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

  • 자바 8이 람다 표현식을 이용해서 위 예제 코드를 다음처럼 간단하게 재구현할 수 있다.
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
System.out.println(result);

일곱 번째 시도: 리스트 형식으로 추상화

현재 filterApples는 Apple과 관련한 동작만 수행한다.

하지만 Apple 이외의 다양한 물건에서 필터링이 작동하도록 리스트 형식을 추상화할 수 있다.

public interface Predicate<T>{
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {   //형식 파라미터 T 등장
    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) -> "red".equals(apple.getColor()));

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

이렇게 해서 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있었다.

요약

동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.

동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.

코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다. → 람다

자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함