Application/JAVA & Kotlin

[모던 자바 인 액션 정리] 2장. 동작 파라미터화 코드

seungh0 2020. 12. 24. 17:11
반응형

"소프트웨어의 모든 것은 변한다. 요구사항은 변한다. 설계도 변한다. 비지니스도 변한다. 기술도 변한다. 팀도 변한다. 팀 구성원도 변한다. 변화는 반드시 일어나기 때문에, 문제가 되는 것은 변화가 아니다. 변화를 극복하지 못하는 우리의 무능력이 문제다." - 켄트백

좋은 코드란 "변경하는 요구사항에 효율적으로 대응하는 코드" 임을 되뇌이며 들어갑시당

 

1. 동작 파라미터화란 (behavior parameterization)

  • 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록
  • 동작 파라미터화를 통해 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

도대체 무슨 소리지?? 🙄 다음 예제를 통해 이해해봅시다~

 


2. 변화하는 요구사항에 효과적으로 대응하기

사과 리스트에서 녹색 사과만 필터링하는 기능을 추가한다고 가정하자.

// inventory는 다음과 같이 여러 사과가 들어있는 사과 리스트이다.
List<Apple> inventory = Arrays.asList(
		new Apple(80, Color.GREEN),
		new Apple(155, Color.GREEN),
		new Apple(120, Color.RED));

 

2-1. 첫 번째 시도: 녹색인 사과를 필터링

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
      if (apple.getColor() == Color.GREEN) { // GREEN 색상의 사과를 필터링한다.!
        result.add(apple);
      }
    }
    return result;
  }

...

// 호출하는 코드
List<Apple> greenApples = filterGreenApples(inventory);

다음과 같이 코드를 짤 수 있을 것이다.

 

하지만 녹색 사과가 아닌 빨간 사과도 필터링 하고 싶어지면?? - 적절하게 대응할 수 없다.

 

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

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
      if (apple.getColor() == color) { // 매개변수로 전달되는 색상에 따라 사과를 필터링한다.
        result.add(apple);
      }
    }
    return result;
  }

...

// 호출하는 코드
List<Apple> redApples = filterApplesByColor(inventory, Color.RED);

 

사과의 색상이 변경되는 요구사항에 적절하게 대응할 수 있게되었다.

 

But..! 색과 동시에 무게에 따라 가벼운 사과와 무거운 사과로 구분하고 싶어지면?... - 적절하기 대응 할 수 없다.

 

2-3. 세번째로 가능한 모든 속성으로 필터링을 진행

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

...

// 호출하는 코드
List<Apple> apples = filterApples(inventory, Color.GREEN, 150);

 

  1. 150이 정확히 뭘 의미하는지 알 수 없다.
  2. 요구사항이 바뀌면 유연하게 대응할 수 없다. (색상, 무게 말고 또 다른 방법으로 요구사항이 들어온다면??)

 

2-4. 동작 파라미터화 & 추상적 조건으로 필터링

public interface ApplePredicate {

	boolean test (Apple apple)

}

// 사과의 색상에 따라 분류하는 ApplePredicate
public class AppleGreenColorPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
      return apple.getColor() == Color.GREEN;
    }

 }

// 사과의 무게에 따라 분류하는 ApplePredicate
public class AppleHeavyWeightPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
      return apple.getWeight() > 150;
    }

}

 

위와 같이 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

 

ApplePredicate 전략 패턴

 

이러한 디자인 패턴을 전략 패턴(strategy pattern)이라고 부른다.

각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임시에 알고리즘을 선택하는 기법이다.

그러면 ApplePredicate를 이용한 필터메소드를 다음과 작성할 수 있게 된다.

 

녹색 사과를 검색해달라고 요구사항이 들어오면 우리는 ApplePredicate를 적절하게 구현하는 클래스(AppleGreenColorPredicate)만 만들면 된다.

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

List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
  • 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메소드의 동작이 결정된다
  • 컬렉션 탐색 로직각 항목에 적용할 동작분리할 수 있다는 것이 동작 파라미터화의 강점이다.

 

하지만.. 아직 불-편하다...

public class AppleGreenColorPredicate implements ApplePredicate {

    	@Override
    	public boolean test(Apple apple) {
      		return apple.getColor() == Color.GREEN;
    	}

 }

List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());

 

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

 

 

2-5. 익명 클래스 사용

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

 

익명 클래스란?

  • 이름이 없는 클래스로, 클래스 선언과 동시에 인스턴스화를 동시에 할 수 있다.
  • 위와 같이 한번 쓰고 버려지는 클래스의 경우에 바로 익명 클래스를 사용하면 효율적이다.
List<Apple> redApples = filter(inventory, new ApplePredicate() {
		@Override
		public boolean test(Apple a) {
			return a.getColor() == Color.RED; // 빨간 사과를 필터링
		}
});

 

But... 아직도 많은 코드를 짜야하고 불-편하다...

  • 참고로 코드의 장황함은 나쁜 특성이다.
    • 장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐만 아니라, 읽는 즐거움을 빼앗는 요소...
    • 한눈에 이해할 수 있는 코드야 진정한 좋은 코드이다..!

 

2-6. 람다 표현식 사용

List<Apple> greenApples = filter(inventory, (Apple a) -> Color.GREEN == a.getColor());

List<Apple> heavyApples = filter(inventory, (a) -> a.getWeight() > 150);
// 람다의 형식 추론으로 Apple 타입을 생략할 수 있다. (자세한 내용은 다음 3장에서..!)

 

2-6. 리스트 형식으로 추상화

  • 제네릭 타입을 통해 더욱 유연하게 제공할 수 있다.
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;
}

...

// 호출 코드

List<Apple> greenApples = filter(inventory, (Apple a) -> Color.GREEN == a.getColor());

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

 

  • 지금까지 동작 파라미터화가 변화하는 요구사항에 쉽게 적응하는 유용한 패턴임을 확인했다.

 

정리

  • 동작 파라미터화는 메소드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메소드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있다.
반응형