[모던 자바인 액션] chp 2. 동작 파라미터화 코드 전달하기

sameul__choi·2022년 2월 16일
0

[모던 자바인 액션]

목록 보기
2/11
post-thumbnail

어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀐다.

변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제다. 시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까 ? 특히 우리의 엔지니어링적인 비용이 가장 최소화 될 수 있으면 좋겠다. 그뿐 아니라 새로 추가하는 기능은 쉽게 구현할 수 있어야 하며 장기적인 관점에서 유지보수가 쉬워야 한다.

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

동작파라미터화를 추가하려면 쓸데없는 코드가 늘어난다. 자바 8은 람다 표현식으로 이 문제를 해결한다. 3장에서는 람다 표현식을 어떻게 만들고, 어디에 사용하고, 람다 표현식으로 코드를 간결하게 만드는 방법을 알아보자.

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

변화에 대응하는 코드를 구현하는 것은 어렵다. 때문에 하나의 예제를 선정하여 코드를 점차 개선해 나가면서 유연한 코드를 만드는 과정으로 배워 보자.

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

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.equals(apple.getColor() 은 녹색 사과를 선택하는 데 필요한 조건을 가르킨다. 그런데 갑자기 농부가 변심하여 녹색 사과 말고 빨간 사과도 필터링하고 싶어졌다. 어떻게 고쳐야할까 ? 일단 filterRedApples() 메서드를 만들어서 if 문의 조건을 빨간 사과로 바꾸는 방법이 있겠다. 하지만 농부가 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절하게 대응할 수 없게 된다. 이런 상황에서는 좋은 규칙이 있다.

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

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

색을 파라미터화 할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있겠다.

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

이제 농부도 만족할 것이다. 하지만 ~? 무게로 나누어 가벼운 사과와 무거운 사과를 구분할 수 있으면 좋겠다며 150g 이상인 사과를 무거운 사과로 분류하여 달라고 요구했다면...? 여기까지 오면 농부가 제시하는 무게의 기준도 얼마든지 바뀔 수 있음을 눈치 챘을 것이다. 그래서 다음 코드에서 확인할 수 있는 것처럼 앞으로 바뀔 수 있는 다양한 무게에 대응할 수 있도록 무게 정보 파라미터도 추가하자.

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

위 코드도 좋은 해결책이라 할 수 있지만 DRY(dont repeat yourself) 원칙을 어기게 된다. 각 사과에 필터링 조건을 적용하는 부분의 코드가 중복이 많이 된다. 탐색 과정을 고쳐 성능을 개선하려고 하면 한 줄이 아니라 메서드 전체 구현을 고쳐야 한다. (비싼 대가)

03 세번째 시도: 가능한 모든 속성으로 필터링

색과 무게를 filter라는 메서드로 합치는 방법도 있다. 색이나 무게 중 어떤 것을 기준으로 필터링 할 지 가르키는 플래그를 추가한다. (참고로 실무에서는 절대 사용해선 안되는 방법이다.)

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

필자는 이 코드가 형편없는 코드라고 말한다. boolean 값의 의미가 명확하게 와닿지 않고, 요구사항이 바뀌었을 때 유연한 대응도 할 수 없다. 문제가 잘 정의되어 있는 상황에서는 이 방법이 잘 동짝할 수 있다. 하지만 더 효과적으로 필터링 하기 위해서는 동작 파라미터화를 이용하여 유연성을 얻는 방법을 사용해야한다.

동작 파라미터화

파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 절실하다는 것을 확인했다. 한 걸음 물러서 전체를 본다면 우리의 선택조건을 사과의 어떤 속성에 기초하여 불리언값을 반환하는 방법이 있다. 참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 선택 조건을 결정하는 인터페이스를 정의해보자.

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 메서드가 다르게 동작할 것으로 예상할 수 있다. 이를 전략 디자인 패턴이라고 부른다.

이제 filterApples()메소드에서 ÀpplePredicate를 받아 사과의 조건을 검사하도록 메서드르 고쳐야 한다. 이렇게 동작 파라미터화, 즉 메서드가 다양한 동작(전략)을 받아 내부적으로 다양한 동작을 수행할 수 있다.

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

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

첫 번째 코드에 비해 더 유연한 코드를 얻음과 동시에 가독성도 좋아졌고 더 사용하기 편리해졌다.

이제 우리는 요구사항에 따라 ApplePredicate를 적절히 구현하는 클래스만 만들면 된다. 우리가 전달한 ApplePredicate 객체에 의해 필터링 동작이 결정된다. 즉, filterApples() 메서드 동작을 파라미터화 하였다.

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

하지만 여러 클래스를 구현해야한다는 점과 인스턴스화 하는 과정이 조금 불편할 수 있겠다. 이를 어떻게 개선해야 할까 ?

익명클래스

위의 물음을 해결하기 위해 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다. 익명 클래스를 이용하면 코드의 양을 줄일수 있다.

익명 클래스는 자바 지역 클래스와 비슷한 개념이다. 말 그대로 이름이 없는 클래스이고 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

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

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

익명 클래스를 활용하여 예제를 다시 구현한 코드이다. 하지만 아직 부족한 점이 있다. 익명클래스는 여전히 많은 공간을 차지하고 불필요하게 장황하다. 가독성을 매우 안 좋게 만들고 있음을 한눈에 확인할 수 있다. 한눈에 이해 할 수 있는 코드가 좋은 코드인데 한눈에 이해하기 어렵다.

람다 표현식 미리보기

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

자바 8의 람다 표현식을 이용하여 위 예제 코드를 다음처럼 간단하게 재구현 할 수 있다.

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

훨씬 간단해짐과 동시에 문제를 더 잘 설명하는 코드가 되었다.

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

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

이제 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다. 자바 8을 이용하여 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있다.

마치며

모던 자바인 액션 두번 째 장으로 동작 파라미터에 대해서 살펴보았다. 1장보다는 어렵지 않았던 것 같다. 예시와 적절한 설명이 이해를 잘 도와주었다. 이번 장에서는 람다를 맛만 보았는데, 다음 장인 3장에서는 람다에 대해서 다룰 예정이다. 조금 더 유연하 간결한 코드를 작성하기 위해 부단히 노력해야겠다는 생각이 들었다 !

0개의 댓글