모던자바인액션 - 2

이건희·2023년 6월 30일
2

모던자바인액션

목록 보기
2/9

챕터 2는 앞서 챕터 1에서 간단히 소개 되었던 동작 파라미터화에 관한 이야기이다.

동작 파라미터화란?

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

예를 들어 나중에 실행될 메서드의 인수로 코드 블록을 전달 할 수 있다. 챕터 2에서는 이전까지 동작 파라미터화의 문제점을 자바 8에서는 어떻게 해결하였는지 보여준다. 아래 예제들을 보며 이해해보자.


요구사항

만약 사과를 재배하는 농부가 있고, 초록 사과를 필터링 하는 코드를 작성해야 한다고 하자. 그러면 다음과 같이 코드를 작성할 수 있다.

1. 녹색 사과 필터링

enum Color { RED, GREED }
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;
}

이렇게 초록 사과를 필터링 하는 코드를 작성하였다. 하지만 만약 빨간 사과를 필터링하고 싶으면 어떻게 해야 될까? 또는 더 나아가 좀 더 다양한 색(초록색, 빨간색, 노란색 등)을 필터링 하고 싶다면?

  • 아마 대부분 메서드를 복사하여 filterRedApples라는 새로운 메서드를 만들고 조건을 해당 색깔로 바꾸는 방법을 선택할 것이다.

하지만 이는 코드의 가독성을 낮추고 반복적인 코드가 늘어난다.

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화해보자.


2. 색을 파라미터화

public static List<Apple> filterGreenApples(List<Apple> inventory, Color color){
//Color를 파라미터화 하였다.
	List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory){
    	if (apple.getColor().equals(color){
        //색을 파라미터화 하여 인수로 넘겨 받는다.
        	result.add(apple);
            }
        }
        return result;
}

하지만 갑자기 농부가 색 이외에도 사과의 무게도 필터링 하고 싶다면?

  • 위와 같은 방법으로 무게도 파라미터에 추가할 수 있을 것이다.
  • 하지만 이는 조건을 적용하는 부분의 코드가 색 필터링 코드와 중복된다.
  • 이는 소프트웨어 공학의 DRY(Don't Repeat Yourself) 원칙을 위반한다.

그러면 한번 filter라는 메소드에 색과 무게를 합쳐보자.


3. 가능한 모든 속성으로 필터링

public static List<Apple> filterGreenApples(List<Apple> inventory, Color color,
int weight, boolean flag){
//Color, Weight 파라미터화, 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);

여기서 문제점이 무엇일까?

  1. true와 false가 직관적으로 무엇을 의미하는 것인가?
  2. 요구사항이 바뀌었을때 유연하게 대응할 수 있는가?
  3. 만약 사과의 크기, 모양, 출하지 등으로 필터링 하고 싶다면?
  4. 녹색 사과 중 무거운 사과를 필터링 하고 싶다면?

결국 중복된 필터 메서드를 만들거나 모든 것을 처리하는 거대한 필터 메서드를 구현해야 한다.

한번 동작 파라미터화를 활용해 코드를 수정해보자


4. 동작 파라미터화

아래와 같이 선택 조건을 결정하는 인터페이스를 정의해보자

public interface ApplePredicate {
	boolean test (Apple apple);
}

위 인터페이스를 통해 다양한 선택 조건을 구현할 수 있다.

//무거운 사과를 필터링 하는 메서드
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());
    }
}

위와 같이 참 또는 거짓을 반환하는 함수를 프리디케이트(Predicate)라고 한다.

위와 같이 코드를 작성하였을때 어떻게 다양한 동작을 수행 할 수 있을까?

  • filterApples에서 ApplePredicate 객체를 받아 사과의 조건을 검사하도록 메서드 수정
  • 동작 파라미터화, 즉 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행

아래는 수정한 filterApples이다.

public static List<Apple> filterApples(List<Apple> inventory,
									ApplePredicate p) {
//ApplePredicate 객체를 통해 동작을 전달 받는다.

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

우리가 전달한 ApplePredicate 객체에 의해 filterApples의 메서드의 동작이 결정된다 !

  • 탐색 로직과 각 항목에 적용할 동작을 분리하였다.
  • 한 메서드가 다른 동작을 수행하도록 재활용 할 수 있다.

하지만 여기에서도 문제점은 있다.

  1. 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야한다.
  2. 여러개의 필터를 구현하려면 ApplePredicate 인터페이스를 구현해서 인스턴스화 해야한다.

이는 상당히 번거로운 작업이다. 한번 익명 클래스를 사용하여 다시 수정해보자.


5. 익명 클래스 활용

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

익명 클래스를 활용하여 클래스 선언과 인스턴스화를 동시에 하였다.

하지만 이도 단점은 있다.

  1. 익명 클래스도 여전히 많은 공간을 차지한다.
  2. 많은 개발자들이 익명 클래스에 익숙하지 않다.
  3. 결국은 객체를 만들고 메서드를 구현해야 한다.

따라서 Java 8에서 추가된 람다 표현식을 사용하면 이를 해결 할 수 있다.


6. 람다 표현식 사용

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

처음과 비교하였을 떄 훨씬 가독성이 높아지고 코드도 간결해졌다.

또한 리스트 형식으로 추상화하여 다른 형식의 리스트에 필터 메서드를 사용할 수도 있다.

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> redApples =
	filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));

//정수 필터링
List<Integer> evenNumbers = 
	filter(numbers, (Integer i) -> i % 2 == 0);

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

동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달하여 메서드의 동작을 파라미터화 한다.

profile
백엔드 개발자가 되겠어요

0개의 댓글