[Java] 동작 파라미터화

🏃‍♀️·2023년 8월 24일

Java [이론]

목록 보기
7/14

프로그램을 개발하면서 변화하는 요구사항에 대처해야한다.
유동적으로 변화하는 요구사항에 어떻게 대처해야할까?
엔지니어링적인 비용이 가장 최소화되면서 쉽게 구현이 가능하고 유지보수가 쉬워야한다.
동작 파라미터화를 이용하면 변화하는 요구사항에 효과적으로 대응할 수 있다.


동작 파라미터화

아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 말한다. 코드 블록의 실행이 나중으로 미뤄진다.
예를 들어 메소드의 인수로 코드 블럭을 전달할 수 있다. 코드 블럭에 따라 메소드의 동작이 파라미터화되므로 다양한 동작을 수행할 수 있다.

그러나 동작 파라미터화를 사용하게 되면 쓸데없는 코드가 늘어나게 된다. 이것은 익명 객체와 람다식을 사용하여 보완한다.


실습 - 연필 필터링하기

연필 클래스 생성

public class Pencil {
    private COLOR color;
    private int length;

    public Pencil(COLOR color, int length){
        this.color = color;
        this.length = length;
    }

    public COLOR getColor(){
        return color;
    }

    public int getLength(){
        return length;
    }
}

COLOR는 enum으로 {GREEN, YELLOW, RED} 값을 가지고 있습니다.


값을 통한 필터링

사용자 요구사항 1

노란색 연필만 필터링하는 기능을 추가해주세요.

노란색 연필을 필터링하는 메소드
public static List<Pencil> filterYellowPencil(List<Pencil> inventory){
    List<Pencil> result = new ArrayList<Pencil>();
    for(Pencil pencil : inventory){
        if(COLOR.YELLOW.equals(pencil.getColor())){
            result.add(pencil);
        }
    }
    return result;
}

인자로 연필 List 객체를 전달받아 for문을 통해 객체의 color 필드가 YELLOW일 경우 결과 반환 리스트에 해당 객체를 추가하는 코드이다.

main
public static void main(String[] args) {
    List<Pencil> pencils = Arrays.asList(new Pencil(COLOR.YELLOW, 20),
                                         new Pencil(COLOR.GREEN, 30),
                                         new Pencil(COLOR.YELLOW, 30));

    List<Pencil> result = filterYellowPencil(pencils);
    for(Pencil pencil : result){
        System.out.println(pencil.getColor() + ", " + pencil.getLength());
    }
}

실행 결과
YELLOW, 20
YELLOW, 30

사용자가 요구한 노란색 연필만 필터링됐다.
이 때, 사용자의 요구사항이 추가된다고 가정하자.

사용자 요구사항 2

초록색 연필만 필터링하는 기능을 추가해주세요.

사용자 요구사항 1과 색상만 다른 요구사항이 추가되었다.
이 때 코드를 복사 붙여넣기해서 색상만 바꾸는 방법도 있지만 이 방법은 적절하지 못하다.

중복되는 코드가 많고 전혀 유연하지 못하기 때문이다.
그렇다면 중복되는 코드를 추상화해보자.

변하는 값인 색상을 파라미터로 받는 메소드를 작성한다면 조금 더 유연하게 동작할 것이다.

색상을 인자로 받는 필터링 메소드
public static List<Pencil> filterPencilsByColor(List<Pencil> inventory, COLOR color){
    List<Pencil> result = new ArrayList<>();
    for(Pencil pencil : inventory){
        if(color == pencil.getColor()){
            result.add(pencil);
        }
    }
    return result;
}
호출
List<Pencil> result = filterPencilsByColor(pencils, COLOR.YELLOW);
List<Pencil> result = filterPencilsByColor(pencils, COLOR.GREEN);

이제 특정 색상이 아닌 사용자가 입력한 색상에 따라 필터링 기능을 제공한다.


사용자 요구사항 3

길이가 25cm 이상인 연필만 필터링해주세요.

사용자가 색상이 아닌 길이라는 조건을 내걸었다. 이럴 때는 어떻게 해야할까?
이때까지 작성한 코드에서 인자 값과 if문 조건만 바꿔주면 된다.

그러나 이 경우, 대부분의 코드가 중복되므로 적절한 대응이 아니다.
또한 추후 유지보수할 때 중복된 코드에 대해 수정 사항이 생기면 전체를 수정해야하기 때문에 비용과 노력이 많이 든다.

그렇다면 모든 속성을 인자로 받아 필터링하는 건 어떤가?

public static List<Pencil> filterPencils(List<Pencil> inventory, COLOR color, int length, boolean flag){
    List<Pencil> result = new ArrayList<>();
    for(Pencil pencil : inventory){
        if((flag && color.equals(pencil.getColor())) || (!flag && length < pencil.getLength())){
            result.add(pencil);
        }
    }
    return result;
}
호출
List<Pencil> result = filterPencils(pencils, COLOR.YELLOW, 0, true);
List<Pencil> result = filterPencils(pencils, null, 25, false);

문제점이 많다.

우선 flag가 무엇을 하는 인자인지 한 번에 인지할 수 없다.
또한 색상과 길이가 아닌 또 다른 필터링 기준이 나타났을 때 유연하게 대응하지 못하기 때문에 부적절하다.

그렇다면 어떤 코드를 작성해야 다양한 요구사항에 대비하면서 직관적인 코드를 작성할 수 있을까?
그 답은 동작 파라미터화이다.



동작 파라미터화를 통한 필터링

지금까지 작성한 필터링 코드의 조건은 모두 is~?형이다.
즉 속성에 대해 참 또는 거짓으로 결과를 반환받아 필터링할 수 있다.

여기서 활용할 것은 프리디케이트, 참 또는 거짓을 반환하는 함수이다.
선택 조건을 결정하는 인터페이스 역할을 수행한다.

Predicate 구현하기

interface 생성

public interface PencilPredicate {
    boolean test(Pencil pencil);
}

interface를 상속받는 class 생성

public static class PencilYellowColorPredicate implements PencilPredicate{
    @Override
    public boolean test(Pencil pencil) {
        return COLOR.YELLOW.equals(pencil.getColor());
    }
}
public static class PencilLengthPredicate implements PencilPredicate{
    @Override
    public boolean test(Pencil pencil) {
        return pencil.getLength() > 20;
    }
}

PencilPredicate를 상속받는 클래스들은 각 클래스에서 사용될 test()메소드를 구현해야한다.
위에서는 노란색 연필을 필터링하는 클래스와 연필 길이가 20cm를 초과하는 필터링을 구현했다.

이제 값을 인수로 넘기는 것이 아닌 interface 객체를 넘긴다.


public static List<Pencil> filterPenclis(List<Pencil> inventory, PencilPredicate p){
    List<Pencil> result = new ArrayList<>();
    for(Pencil pencil : inventory){
        if(p.test(pencil)){
            result.add(pencil);
        }
    }
    return result;
}
호출
List<Pencil> result = filterPenclis(pencils, new PencilYellowColorPredicate());
List<Pencil> result = filterPenclis(pencils, new PencilLengthPredicate());

객체에 따라 다양한 로직이 수행이 되며 이전보다 더 유연하고 가독성이 좋고 사용자가 사용하기 편리한 코드가 만들어졌다.
전달한 객체에 따라 동작이 결정된다. 즉, 동작을 파라미터화한 것이다.

동작의 파라미터화 장점

  • 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다.
  • 한 메소드가 다른 동작을 수행하도록 재활용할 수 있다. 유연한 api를 만들 때 동작 파라미터화가 중요한 역할을 한다.

하지만 사용하기 복잡한 기능이나 개념을 사용하고 싶은 사람은 없다.

위 코드를 작성하면서 기능 하나가 추가될 때마다 (필터 조건이 추가될때마다) class를 생성하고 인스턴스화하여 사용하는 것이 맞나..? 하는 의문이 들었다.
이 방법은 상당히 번거로우며 시간을 낭비한다.

이러한 문제는 익명 클래스 또는 람다식을 사용해서 코드를 조금 더 간결하여 해결할 수 있다.


간소화

익명 클래스 이용하기

동작 파라미터화를 작성할 때 인터페이스를 상속받는 클래스를 작성하고 그 안에 메소드를 구현하였다.
그러나 익명 클래스를 사용한다면 클래스를 작성하고 호출부에서 해당 클래스를 인스턴스화하는 일이 간소화된다.

List<Pencil> result = filterPenclis(pencils, new PencilPredicate(){
	public boolean test(Pencil pencil){
    	return COLOR.YELLOW.equals(pencil.getColor());
    }
});

람다식 이용하기

람다식을 이용하면 익명 클래스로 작성한 코드보다 훨씬 간결한 코드를 작성할 수 있다.

List<Pencil> result = filterPenclis(pencils, (Pencil pencil) -> COLOR.YELLOW.equals(pencil.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 item: list){
    	if(p.test(item)) result.add(item);
    }
    return result;
}

이제 다양한 타입의 리스트에서 필터 메소드를 사용할 수 있게 되었다.

0개의 댓글