동작 파라미터화 - 모던자바인액션

HUSII·2023년 7월 12일
0

자바 공부

목록 보기
4/8

우리가 어떤 상황에서 일을 하든, 소비자의 요구사항은 항상 바뀐다.
동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.


요구사항

사과를 재배하는 농부가 사과를 구별하고 싶어서,
내가 사과를 분류해주는 프로그램을 구현한다고 하자.

처음에 농부는 빨간사과를 분류하고 싶다고 했다.

그래서 내가

List<Apple> filterRedApples(List<Apple> apples) {
	List<Apple> result = new ArrayList<>();
	for(Apple apple: apples){
    	if(RED.equals(apple.getColor())){
        	result.add(apple);
        }
    }
    
    return result;
}

모든사과를 입력하면 빨간사과만 리턴하는 함수를 짰는데,
나중에 농부가 150g을 넘는 사과를 분류하고 싶다고 했다.

그래서 나는 filterApplesByWeight 라는 메서드를 추가로 생성했다.

List<Apple> filterApplesByWeight(List<Apple> apples) {
	List<Apple> result = new ArrayList<>();
	for(Apple apple: apples){
    	if(apple.getWeight() > 150){
        	result.add(apple);
        }
    }
    
    return result;
}

기존 코드의 문제점

여기서 filterRedApples 메서드와 filterApplesByWeight 메서드를 비교했을때 겹치는 코드가 많다.

List<Apple> filterApples(List<Apple> apples) {
	List<Apple> result = new ArrayList<>();
	for(Apple apple: apples){
    	if(/* 사과를 분류하는 기준 */){
        	result.add(apple);
        }
    }
    
    return result;
}

위처럼 사과를 분류하는 기준을 제외한 다른 코드는 모두 같다.
여기서 농부가 또 다른 분류기준을 주면 어떻게될까?
그럼 또 새로운 메서드를 추가할 것인가?


여기서 우리는 메서드를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법인 동작 파라미터화를 이용할 것이다.


문제 해결 과정

결국 농부의 요구사항은 사과를 어떤 속성에 기초해서(빨간색인가?, 150g을 넘는가?) 분류할 것인지, 아닌지를 판단만 하면 되므로, 참 또는 거짓을 반환하는 함수를 만든다.

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 AppleRedColorPredicate implements ApplePredicate {

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

  }

이 다음은 filterApples에서 ApplePredicate 객체를 받아 애플의 조건을 검사하도록 메서드를 고친다.

List<Apple> filterApples(List<Apple> apples, ApplePredicate p) {
	List<Apple> result = new ArrayList<>();
	for(Apple apple: apples){
    	if(p.test(apple)){ // 깔끔
        	result.add(apple);
        }
    }
    
    return result;
}

만약 빨간 사과를 분류하고 싶다면
filterApples(apples, new AppleColorPredicate())처럼
AppleColorPredicate 객체를 선언해서 filterApples 메서드를 호출해주면 되고,
150g을 넘는 사과를 분류하고 싶다면,
filterApples(apples, new AppleWeightPredicate()) 처럼 선언해주면 된다.


그림으로 정리

위의 과정을 그림으로 보자

(다른 메서드)
기존 코드의 문제점은 분류기준이 바뀔때마다 새로운 메서드를 생성해야 한다는 것이다.

그래서 동작 파라미터화를 적용하면,

(같은 메서드)
ApplePredicate라는 인터페이스가 filterApples메서드의 인자로 입력되면서,
사과를 분류하는 기준(동작)을 메서드의 파라미터로 받아서, 좀더 유연하게 사과를 분류할 수 있게 되었다.

만약 여기서 농부가 다른 분류 기준을 제시한다면 어떻게 될까?
(가벼운 사과만 분류, weight < 100)

public class AppleLightWeightPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
      return apple.getWeight() < 100;
    }

  }

라는 클래스를 정의하고
filterApples(apples, new AppleLightWeightPredicate) 이렇게 선언해주면 새로운 분류기준에도 유연하게 대처할 수 있다.


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

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


부족한 점

하지만 여기서도 약간 부족한 점이 보인다.
현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴화해야 한다. 이는 상당히 번거로운 작업이며 시간 낭비이다.

초록색 사과를 분류하는 메소드를 호출할때,

 public class AppleGreenColorPredicate implements ApplePredicate {

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

  }

fileterApples(apples, new AppleRedColorPredicate());

위 코드에서 필요한 코드는 apple.getColor() == Color.GREEN 이다.
그런데 로직과 관련 없는 코드가 많이 추가되었다.

자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(anonymous class)라는 기법을 제공한다. 익명 클래스를 이용하면 코드의 양을 줄일 수 있다.
그리고 자바 8에서 등장한 람다 표현식으로 더 가독성 있는 코드를 구현할 수 있다.


익명 클래스

익명 클래스는 이름이 없는 클래스로, 자바의 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념이다.
익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. -> 즉석에서 필요한 구현을 만들어서 사용할 수 있다.


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

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

위의 예제를 통해, 익명 클래스를 이용해 코드의 꽤많이 줄일 수 있었다.

안드로이드에서 익명 클래스를 이용하는걸 많이 볼 수 있다.

하지만 조금더 줄이고싶다.
역시 위에서도 return RED.equals(apple.getColor()) 이 코드만 필요한데, 추가로 작업하는 코드가 존재한다.


람다 표현식

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

List<Apple> redApples = filterApples(apples, (Apple apple) -> RED.equals(apple.getcolor()));

코드가 훨씬 간결해졌다.

(람다의 자세한 설명은 다음 장에서 이어집니다..)


동작 파라미터화 정리

  1. 동작 파라미터화에서는 매서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
  2. 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
  3. 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 하지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
profile
공부하다가 생긴 궁금한 것들을 정리하는 공간

0개의 댓글