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

김종준·2023년 1월 15일
0

모던자바

목록 보기
1/15

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

제목으로 추측하였을 때 이 쳅터에서 집중해야 하는 부분은 "동작 파라미터화가 무엇"이고 "코드 전달은 어떻게 이루어지는가?" 인 거 같다.

동작 파라미터화

우선 동작 파라미터화란 아직 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다고 한다.

아직 결정되지 않았기에 실행은 나중으로 미뤄지고 프로그램에서 호출된다.

이 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있다.

즉 코드 블록에 따라 메서드의 동작이 파라미터화 된다.

책에서는 변화하는 요구사항에 대응하는 코드를 보여주며 동작 파라미터화를 이해시켜 준다.

코드를 보면서 아직 어떻게 실행할 것인지 결정하지 않은변화하는 요구사항이에 대응하는 것 같다고 생각하였다.

책에서 제시한 변화하는 요구사항과 코드를 살펴보자.

우선 사과를 을을 기준으로 필터링할 수 있었으면 하는 요구사항을 제시하고 이후 무게을을 기준으로 필터링할 수 있었으면 하는 요구사항이 추가 된다.

이 요구사항을 반영한 코드를 보자.

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

  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 원칙을 어기는 것이다.

그렇다면 중복되지 않게 코드를 합치는 방법이 떠오를 것이다.

합친 코드를 살펴보자.

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

파라미터로 color와 weight를 모두 받아 하나의 코드를 통해 사과를 비교할 수 있게 되었지만 메서드 사용한 부분을 보면 메서드의 의미를 정확히 파악할 수 있을 것이라는 생각이 들지 않는다.

그리고 위와 같이 코드를 작성할 경우 요구사항이 추가될 때마다 파라미터가 늘어날 것인데 이는 점점 더 코드의 가독성을 안 좋게 할 것이 분명하다.

이러한 문제를 해결하는 방법이 요구사항을 수행하는 코드를 파라미터화하여 그 코드를 전달하는 것이다.

즉, 요구사항 수행 부분은 결정되지 않은 채 놔두고 필요한 코드를 전달하는 것이다.

이렇게 되면 변화하는 요구사항에 조금 더 유연하게 대응할 수 있다.

Predicate 인터페이스

결정되지 않은 채 놔둔다는 말이 조금 안 와닿을 수 있다.

코드로 이를 확인해 보자.

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

결정되지 않은 ApplePredicate 를 통해 선택 조건을 결정하는 인터페이스만 정의하였다.

결정되지 않았지만 최소한 어떠한 동작을 받을 것인지를 인터페이스를 통해 정의한 것이다.

위와 같이 인터페이스를 정의하였으면 아래와 같이 이를 요구사항 변경에 맞게 구현하면 된다.

  public class AppleWeightPredicate implements ApplePredicate {

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

  }

  public class AppleColorPredicate implements ApplePredicate {

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

  }

이처럼 코드를 작성하는 것을 전략 디자인 패턴이라고 한다.

이렇게 코드를 작성하면 앞서 문제가 되었던 filterApples(List inventory, Color color, int weight, boolean flag) 역시 AppleRedAndHeavyPredicate와 같은 전달할 동작을 구현하여 전달하기만 하면 된다.

하나의 파라미터로 다양한 동작을 할 수 있다는 것 동작 파라미터화의 장점이다.

익명 클래스

익명 클래스는 말 그대로 이름이 없는 클래스로 이를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.

즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

하지만 무작정 만들 수는 없고 틀은 필요하다.

이때 틀이 인터페이스가 되는 것이다.

이 역시 코드로 보면 아래와 같다.

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

위의 코드에서 틀은 ApplePredicate 를 사용하였다.

위와 같이 익명 클래스를 사용하면 장점은 코드의 장황함이 줄어든다.

이 말은 이전 예제와 같이 클래스 선언하는 과정을 줄일 수 있다는 것이다.

이 정도만 해도 많이 줄인 거 같지만 람다 표현 식을 사용하면 이를 더 줄일 수 있는데 이는 다음 장에서 소개한다고 한다.

정리

요구사항은 항상 변경될 수 있고 우리는 이를 대비하여야 한다.

이를 대비하는 방법이 인터페이스(ex ApplePredicate)를 통해 요구사항을 어떻게 수행할지 틀만 정의하고 구체적인 코드(ex AppleColorPredicate)는 파라미터로 전달받는 것이다.

이때 파라미터로 전달받는 방법은 인터페이스를 정의한 클래스, 익명 클래스, 람다 표현 식과 같은 방법이 있다.

0개의 댓글