Skip to content

[CHAPTER 2] - 동작 파라미터화 코드 전달하기 #18

@Irisation23

Description

@Irisation23

Discussed in https://github.com/orgs/Study-2-Modern-Java-In-Action/discussions/12

Originally posted by bunsung92 June 11, 2023

0. 개요

클라이언트의 요구사항은 언제나 바뀐다.
바뀔 요구사항을 대비해 미리 만들 수 없다면, 필요한 시점에 효과적으로 대응 할 수 있는 방법이 필요하다.

이를 위해 동작 파라미터화를 이용한다.


1. 변화하는 요구사항에 대응

image

총 7단계로 요구사항을 구성하여 동작 파라미터화의 개념을 잡아보자.

해당 코드를 보는 시각은 농부가 자동화 된 농작물 필터링 기계의 로직을 만들어 달라고 요구해 온 상황이라 생각해보자.

1.0 Domain

Apple 클래스
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@AllArgsConstructor
@Getter
@ToString
public class Apple {

    private Color color;
    private int weight;
}
Color 클래스
public enum Color {
    RED,
    GREEN
}
Inventory 클래스

import java.util.ArrayList;
import java.util.List;

public class InventoryFactory {

private InventoryFactory() {}

public static List<Apple> makeInventory() {
    List<Apple> inventory = new ArrayList<>();

    inventory.add(new Apple(Color.GREEN, 200));
    inventory.add(new Apple(Color.RED, 50));

    return inventory;
}

}

1.1 녹색 사과 필터링

package com.hello.modern.chapter1.requirement.ex1;

    import com.hello.modern.chapter1.domain.Apple;
    import com.hello.modern.chapter1.domain.Color;
    import java.util.ArrayList;
    import java.util.List;

// 요구사항 1: 녹색 사과 필터링
public class Requirements1 {

    public static void main(String[] args) {

    }

    public static List<Apple> filterGreenApples(List<Apple> inventory) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (Color.GREEN.equals(apple.getColor())) {
                result.add(apple);
            }
        }

        return result;
    }
}

농장 주인은 최초에 요구사항으로 녹색 사과에 대한 필터링만을 요청했다.
하지만 금새 마음을 바꿨다.

빨간 사과도 필터링도 추가해 주세요

1.2 색을 파라미터화 (하지만 이내 무게 요청을 추가함)

package com.hello.modern.chapter1.requirement.ex2;

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;
import com.hello.modern.chapter1.domain.InventoryFactory;
import java.util.ArrayList;
import java.util.List;

// 요구사항 2: 색, 무게를 파라미터화
public class Requirements2 {


    public static void main(String[] args) {
        List<Apple> inventory = InventoryFactory.makeInventory();

        // start --- 색깔을 구별하는 로직을 만들어주세요. ---

        List<Apple> haveColorApples = filterAppleByColor(inventory, Color.GREEN);
//        List<Apple> haveColorApples = filterAppleByColor(inventory, Color.RED);

        for (Apple apple : haveColorApples) {
            System.out.println(apple);
        }

        // end --- 색깔을 구별하는 로직을 만들어주세요. ---

        // start --- 무게로 구별하는 로직을 만들어주세요. ---
        List<Apple> weightApple = filterAppleByWeight(inventory, 150);
        // end --- 무게로 구별하는 로직을 만들어주세요. ---
    }

     // 처음 요청한 요구사항
    public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (apple.getColor().equals(color)) {
                result.add(apple);
            }
        }

        return result;
    }

    // 추가된 요구사항
    public static List<Apple> filterAppleByWeight(List<Apple> inventory, int weight) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (apple.getWeight() > weight) {
                result.add(apple);
            }
        }

        return result;
    }
}

하지만 위의 구조는 대부분의 코드가 중복된다.
그리고 해당 중복 코드를 엮어 색, 무게를 동시에 처리할 방법에 대해 강구 해 볼 수있다.


1.3 가능한 모든 속성으로 필터링 (flag가 의미하는 바는 색과 무게 중 어떤 것을 기준으로 필터링할지 가리키는 요소이다.)

package com.hello.modern.chapter1.requirement.ex3;

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;
import com.hello.modern.chapter1.domain.InventoryFactory;
import java.util.ArrayList;
import java.util.List;

// 요구사항 3: 가능한 모든 속성으로 필터링 (해당 코드에서는 사과의 색깔과 무게로 필터링)
public class Requirements3 {


    public static void main(String[] args) {
        List<Apple> inventory = InventoryFactory.makeInventory();

        //What does means true false????!!!
        List<Apple> greenApples = filterApples(inventory, Color.GREEN, 0, true);
        List<Apple> heavyApples = filterApples(inventory, Color.RED, 150, false);

    }

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

해당 코드를 보자마자 true, false에 대해 무슨 행위를 하는지 분석하는데 골머리를 썩게된다.
기준이 명확하지 못해, 코드 가독성이 떨어지게 되고 이는 개발 속도를 저하시킨다.

이제 해당 사안을 안고 동작 파라미터화에 대해 본격적으로 알아보자.


2. 동작 파라미터화

ApplePredicate 인터페이스

import com.hello.modern.chapter1.domain.Apple;

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

AppleGreenColorPredicate 클래스
import com.hello.modern.chapter1.domain.Apple; import com.hello.modern.chapter1.domain.Color;

public class AppleGreenColorPredicate implements ApplePredicate {

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

}

Foo

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;

public class AppleGreenColorPredicate implements ApplePredicate {

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

}

2.1 요구사항 4: 추상적 조건으로 필터링

package com.hello.modern.chapter1.requirement.ex4;

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.InventoryFactory;
import java.util.ArrayList;
import java.util.List;

// 동작 파라미터화 (behavior parameterization)
public class Requirements4 {

    public static void main(String[] args) {

        List<Apple> inventory = InventoryFactory.makeInventory();

        List<Apple> apples = filterApples(inventory, new AppleGreenColorPredicate());
        System.out.println("apples = " + apples);
        List<Apple> apples1 = filterApples(inventory, new AppleHeavyWeightPredicate());
        System.out.println("apples1 = " + apples1);
    }

    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }

        return result;
    }
}
  • 전략 패턴을 이용한 알고리즘(전략)을 캡슐화 하는 패턴을 이용해 요구사항에 알맞는 클래스를 정의할 수 있도록 한다.
  • 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

보다 변화에 다양하게 대응할 수 있도록 코드를 추가할 수 있었다.

하지만 해당 구조는 꽤 복잡한 구조를 가짐으로써 언젠간 해당 코드를 이어받아 코드를 작성하게 될 다음 개발자를 배려하지못했다.

  1. 사과의 색이 추가된다면?
  2. 사과의 길이가 추가된다면?
  3. 사과의 무게가 보다 더 정밀(소수점을 사용)해야 한다면?

각 변경 사안에 맞는 수많은 구현 클래스를 구축할 것인가?

극단적인 예시일 수 있지만, 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스 기법을 제공하고 있다.

2.2 요구사항 5: 익명 클래스 사용

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;
import com.hello.modern.chapter1.domain.InventoryFactory;
import com.hello.modern.chapter1.requirement.ex4.ApplePredicate;
import java.util.ArrayList;
import java.util.List;

// 요구사항 5: 익명 클래스 사용
public class Requirements5 {

    public static void main(String[] args) {
        List<Apple> redApples = filterApples(InventoryFactory.makeInventory(), new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return Color.RED.equals(apple.getColor());
            }
        });
    }

    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }

        return result;
    }
}

익명 클래스에 대한 자세한 설명은 링크 를 참조하기 바란다.

2.3 요구사항 6: 람다 표현식 사용

image

인텔리제이는 사실 익명클래스의 사용보다는 람다 표현식을 권유한다.

package com.hello.modern.chapter1.requirement.ex6;

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;
import com.hello.modern.chapter1.domain.InventoryFactory;
import com.hello.modern.chapter1.requirement.ex4.ApplePredicate;
import java.util.ArrayList;
import java.util.List;

// 요구사항 6: 람다 표현식 사용
public class Requirements6 {

    public static void main(String[] args) {
        List<Apple> result = filterApples(InventoryFactory.makeInventory(),
            (Apple apple) -> Color.RED.equals((apple.getColor())));
    }

    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }

        return result;
    }
}

하는 역할은 같지만 보다 깔끔한 코드와 복잡성을 해결하는 읽좋코 가 된것이다.
마지막 추상화를 만나보자.

추상화의 관점은 아래와 같다.

  1. 현재 filterXXX 메서드는 List인 inventory를 이용한다.
  2. XXXPredicate를 파라미터로 받는다.

2.4 요구사항 7: 리스트 형식으로 추상화

package com.hello.modern.chapter1.requirement.ex7;

public interface Predicate<T> {
    boolean test(T t);
}
package com.hello.modern.chapter1.requirement.ex7;

import com.hello.modern.chapter1.domain.Apple;
import com.hello.modern.chapter1.domain.Color;
import com.hello.modern.chapter1.domain.InventoryFactory;
import java.util.ArrayList;
import java.util.List;

// 요구사항 7: 리스트 형식으로 추상화 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 사용 가능!
public class Requirements7 {

    public static void main(String[] args) {
        List<Apple> redApples = filter(InventoryFactory.makeInventory(),
            (Apple apple) -> Color.RED.equals(apple.getColor()));
        System.out.println("redApples = " + redApples);

        List<Integer> evenNumbers = filter(List.of(1, 2, 3, 4, 5, 6, 7), (Integer i) -> i % 2 == 0);
        System.out.println("evenNumbers = " + evenNumbers);
    }

    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;
    }
}
  • 추상화는 행위의 반복에 기반하기도 하며, 역할에 기반하기도 한다.

3. 정리 및 회고 📚

  • 동작 파라미터화는 변화에 유연하다.
  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수있도록 코드를 메서드 Argument로 전달한다.
  • 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.

2023-06-11 일

모던 자바 인 액션의 시작은 동작 파라미터화 코드 전달하기였다.
Behvior Parameterization 변화에 유연하기 위해 자바가 선택한 기법을 학습 해 볼 수 있었고, 과거의 코드 역사를 기반으로 해당 학습을 이어가니
체화 시키기에 좋았다.
하지만 개인적으로 글로 정리하기에는 호흡이 길었다는 느낌을 받았고, 1차적으로 정리를 마치면 정리 역시도 리팩터링을 이어나가 보다 중요한 부분에 포인트를 싣는게 좋을것 같다 생각했다.

시작이 반이라 생각하며 조금씩 나아 가 보자. (추신: 자바 20은 2023-03-23에 나옴)



참조 자료
익명클래스 - https://kadosholy.tistory.com/103
Clean Architecture - https://wikidocs.net/167372

Metadata

Metadata

Assignees

Labels

Chapter 2Chapter 2 정리를 위한 라벨입니다.ding_cook훈민님을 위한 라벨입니다.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions