전략 패턴을 이용해서 요구사항 대응하기

2024. 6. 1. 22:24프로젝트/[EATceed] 몸무게 증량 어플

728x90

 

프로젝트를 진행하다가 요구사항이 변경되었다.

 

기존 요구사항은 "식사 등록시 한 식사에 대해 인 분(multiple)이란 단위만을 사용하는 것이었다." 



요구사항은 "식사 등록시 각 음식에 대해 인 분(multiple) 또는 g(무게) 단위를 사용하는 것"으로 바뀌었다.

식사를 등록할 때 단위는 얼마든지 바뀌거나 수정될 수 있다.

 

문제는 바뀐 요구사항에의해 음식의 영양소를 분석하는 기존 로직을 수정해야한다는 것이었다.

이는 OSP(Open Closed Principle)에 위배된다.

 

따라서, 전략 패턴을 사용해 새로운 "단위"가 도입되더라도 변경이 최소화되도록 코드를 개선하였다.

 

전략 패턴

 

전략 패턴은 실행 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 해주는 디자인 패턴이다.

 

전략 패턴은 아래 블로그에 잘 설명되어있다.

 

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%84%EB%9E%B5Strategy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

 

💠 전략(Strategy) 패턴 - 완벽 마스터하기

Strategy Pattern 전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수

inpa.tistory.com

 

 

전략 패턴을 적용하기 전 선행되어야할 일은 객체가 해야할 일을 생각해보는 것이다.

 

기존 객체가 하는 일은 하루 동안 식사한 음식 영양소를 분석하는 것이었다. 기존의 단위는 인 분이었지만 이제는 g을 사용할 수도 있다.
(유저 선택)

 

즉, 객체는 하루 동안 식사한 음식 영양소를 분석하되 분석 결과는 여러 단위를 사용할 수 있어야한다.

 

따라서, 객체가 여러 단위를 사용해서 분석하는 것을 측정 전략(MeasureStrategy)으로 추상화하고 해당 인터페이스를 구현해 각 단위를 사용하는 전략들을 구체화하도록 설계하였다.

 

대략적인 설계 방향은 위와 같고, 이제 코드를 개선해보자.

 

기존 코드 

 

public class Meal {

    private List<Food> foods;

    public double getCurrentCalorie() {
        return consumedFoods.stream().mapToDouble(Food::getAdjustedCalorie).sum();
    }

    public double getCurrentCarbohydrate() {
        return consumedFoods.stream().mapToDouble(Food::getAdjustedCarbohydrate).sum();
    }

    public double getCurrentProtein() {
        return consumedFoods.stream().mapToDouble(Food::getAdjustedProtein).sum();
    }

    public double getCurrentFat() {
        return consumedFoods.stream().mapToDouble(Food::getAdjustedFat).sum();
    }
}

 

기존 코드는 Meal 클래스(POJO)에서 먹은 Foods들의 영양성분을 가져와서 반환해주고 있다.

getAdjustedXXX 메서드에서는 multiple을 각 영양소와 단순히 곱하고 있다.

 

class Food {

    private double fat;
    private double multiple
    ...

    public double getAdjustedFat(){
        return this.multiple * fat;
    }
    ...
}

 

개선된 코드

 

Food 클래스는 상황에 따라 g 이란 단위를 사용해 음식의 영양소를 반환해줘야하고, 기존 multiple이라는 단위를 사용해서도 음식의 영양소를 반환해줘야한다.

 

따라서, 식사 전략이라는 인터페이스를 선언하고, 해당 인터페이스를 구현하는 MultipleStrategy와 GStrategy 클래스를 구현하였다.

 

public interface MeasureStrategy {
    double measure(double value, Unit unit);
}

public class MultipleStrategy implements MeasureStrategy {
    @Override
    public double measure(double value, Unit unit) {
        return value * unit.getMultiple();
    }
}

public class GStrategy implements MeasureStrategy {
    @Override
    public double measure(double value, Unit unit) {
        return value * unit.getG();
    }
}

 

각 전략은 상황에 따라 알맞은 알고리즘을 적용된다.

 

상황 : multiple을 사용하여 식사 등록할 시, g을 사용하여 식사 등록할 시

 

그리고, ConsumedFood 클래스를 명명하고 MeasureStrategy 타입인 필드를 갖게 한다.

 

public class ConsumedFood {

    private Food food;
    private Unit unit;
    private MeasureStrategy measureStrategy;

    private MeasureStrategy getStrategy() {
        if (this.unit.getG() == null) {
            return new MultipleStrategy();
        } else {
            return new GStrategy();
        }
    }

    public double getAdjustedCalorie() {
        return getStrategy().measure(this.food.getCalorie(), unit);
    }

  	...
}

 

 

원하는 상황에 맞게 getStrategy() 메서드를 사용해 전략을 구체화해준다.

그리고, 기존 코드와 같이 getAdjustedXXX 메서드를 호출해준다.

 

public class Meal {

    private List<ConsumedFood> consumedFoods;

    public double getCurrentCalorie() {
        return consumedFoods.stream().mapToDouble(ConsumedFood::getAdjustedCalorie).sum();
    }

  	...
}

 

이렇게 함으로써 기존 코드를 크게 수정하지 않고 전략 패턴을 적용하여 요구사항을 반영하였다.

 

아쉬운 점은 ConsumedFood 클래스의 getStrategy 메서드에서 매번 새로운 전략 클래스를 생성하고 있다는 것이다.

 

 

고민한 점

 

현재 기존 코드는 비즈니스 로직을 전부 POJO 클래스를 만들어 위임해둔 상태이다.

POJO 클래스이기때문에 해당 클래스를 빈으로 등록하여 스프링 컨텍스트가 관리하게 하면 POJO로 만든 이유가 없다고 생각하였다.

당연히 클래스가 빈으로 등록되지 않았기 때문에 DI를 적용할 수 없으며 따라서, MeasureStrategy 인터페이스 또한 DI와 다형성을 활용해 Serivce 레이어 내에서 클래스를 갈아끼울 수 없었다.

 

MeasureStrategy 인터페이스에 @FunctionalInterface를 적용해 measure 메서드를 람다로 구현하는 것을 고민하였다.

일반적으로 함수형 인터페이스를 사용해 가독성을 개선할 수 있다.

하지만, 함수형 인터페이스를 사용하지 않았다.

이유는 아래와 같다.

 

현재 로직은 함수형 인터페이스를 사용해 가독성을 개선할만큼의 코드가 아니라고 판단하였다.

 

@FunctionalInterface
public interface MeasureStrategy<T, U, R> {
	R measure(T value, U unit); 
}

 

무엇보다 MeasureStrategy를 함수형 인터페이스를 사용하면, 기존의 Primitive한 타입을 boxing해야해서 비용이 꽤나 들고, 힙에 객체가 쌓이게 되면, GC가 돌아서 stop the world가 더 자주 발생하기 때문에 함수형 인터페이스는 적용하지 않는 것이 좋다고 판단하였다.

728x90