Enum 처리를 위한 끝 없는 switch문에 관하여… (전략 패턴 사용기)

Choi Wontak·2025년 4월 14일

아이쿠MSA

목록 보기
2/12

난이도 ⭐️⭐️
정리 날짜 2025.01.08

고민 내용

우리 프로젝트의 포인트는 트랜잭션이 보장되어야 하는 중요한 비즈니스 파트였다. 그러나 아이쿠의 서버는 MSA를 사용 중이었기 때문에 SAGA 패턴을 통한 보상 트랜잭션이 필수적이었다.

포인트의 변화 유형은 카프카 메시지 속 Enum 타입인 PointChangeReason 으로 관리 중이었는데, 각 변화 유형에 따라 실패 시 롤백되는 방식이 다르다 보니 당연하게도 switch 문을 떠올리게 되었으나..

switch (pointChangeReason) {
            case SCHEDULE_ENTER:
                makeScheduleEnterRollBack(member, pointChangeType, pointAmount, pointChangeReason, reasonId);
                break;

            case SCHEDULE_EXIT:
                makeScheduleExitRollBack(member, pointChangeType, pointAmount, pointChangeReason, reasonId);
                break;

            case SCHEDULE_REWARD:
                makeScheduleRewardRollBack(member, pointChangeType, pointAmount, pointChangeReason, reasonId);
                break;

            case BETTING:
                makeBettingRollBack(member, pointChangeType, pointAmount, pointChangeReason, reasonId);
                break;
                
                // 이후 생략...
}

모든 Case를 관리해주어야 하며 한눈에 들어오지도 않는다..!
Case가 추가되기라도 한다면…

🤔
Switch 문으로 Enum 분기를 하는 건 유지보수에 별로일 것 같은데?!


찾아보기

이러한 문제상황을 해결하기 위해 디자인 패턴을 찾아보던 중 ‘전략 패턴(Strategy Pattern)’을 발견하게 되었다.

객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여,

객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말합니다.

간단히 말해서 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴입니다.

출처 : https://victorydntmd.tistory.com/292

전략패턴이미지

그러니까, 이 패턴을 이용하면 클라이언트는 전략 제공자(Context)에만 의존하고, 구체적인 전략 내용은 알 필요가 없다.

⇒ 다양한 전략이 필요하지만, 캡슐화가 필요한 지금 가장 어울리는 패턴이라는 생각을 하게 되었다.

다음과 같이 변경해보았다.

public interface RollbackStrategy {

    void execute(MemberValue member, PointChangeType pointChangeType, int pointAmount, PointChangeReason pointChangeReason, Long reasonId);
}

전략마다 실행해야 하는 메소드를 하나로 뽑아 인터페이스로 추상화하였고,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PointChangeReasonMapping {

    PointChangeReason[] value();
}

어노테이션을 새로 생성하여 각 전략 구현체마다 달아주었다.
이 어노테이션에는 Enum인 PointChangeReason 을 담을 수 있다.

@PointChangeReasonMapping(PointChangeReason.SCHEDULE_ENTER)
@Component
public class ScheduleEnterRollbackStrategy implements RollbackStrategy {

    @Override
    public void execute(MemberValue member, PointChangeType pointChangeType, int pointAmount, PointChangeReason pointChangeReason, Long reasonId) {
        System.out.println("Processing SCHEDULE_ENTER rollback");
    }
}

각 전략 구현체는 실행할 내용을 적어주었고, 어노테이션과 함께 컴포넌트로 등록해주었다.

대망의 Processor (Context)

@Component
public class RollbackProcessor {

    private final Map<PointChangeReason, RollbackStrategy> strategies;

    public RollbackProcessor(List<RollbackStrategy> strategyList) {
        strategies = new HashMap<>();

        for (RollbackStrategy strategy : strategyList) {
            PointChangeReasonMapping mapping = strategy.getClass().getAnnotation(PointChangeReasonMapping.class);
            if (mapping != null) {
                for (PointChangeReason reason : mapping.value()) {
                    strategies.put(reason, strategy);
                }
            }
        }
    }

    public void process(MemberValue member, PointChangeType pointChangeType, int pointAmount, PointChangeReason pointChangeReason, Long reasonId) {
        RollbackStrategy strategy = strategies.get(pointChangeReason);
        if (strategy != null) {
            strategy.execute(member, pointChangeType, pointAmount, pointChangeReason, reasonId);
        } else {
            throw new IllegalArgumentException("Unsupported PointChangeReason: " + pointChangeReason);
        }
    }

}

스프링은 빈을 등록할 때 생성자에 있는 매개변수를 자동으로 주입한다.

이 경우에는 인터페이스인 RollbackStrategy 의 리스트를 자동으로 주입하여 주는데, 이 인터페이스의 구현체들을 자동으로 주입하여 준다..! 이를 위해 각 구현체의 Component 등록이 필요했던 것이다.

이를 하나씩 어노테이션 속 PointChangeReason 과 함께 Map에 등록하여주는 과정이 RollbackProcessor 의 등록 과정에서 일어난다.

이제 클라이언트는 RollbackProcessor 의 process 메소드를 통해 자동으로 전략을 실행할 수 있다.
( PointChangeReason 의 종류나 strategy 의 내용을 전혀 알 필요가 없다!)


결론

내가 고민하는 것들의 대부분은 이미 다른 선배 개발자들이 고민했던 것들인 것 같다.

디자인 패턴에 이미 정답이 나와있고 그걸 어노테이션 기반의 프레임워크로 매우매우 아름답게 해결할 수 있다는 것이 놀라웠다.

앞으로도 아름다운 구조에 대해 열심히 공부할 필요를 느꼈다!

GoF 당신들은 대체..!

profile
백엔드 주니어 주니어 개발자

0개의 댓글