Home 가맹점마다 정산 모델이 다를 때 어떻게 처리해야 할까?
Post
Cancel

가맹점마다 정산 모델이 다를 때 어떻게 처리해야 할까?

글을 작성하게 된 계기


가맹점마다 정산 모델이 다를 때, 어떻게 분기문을 줄이면서 깔끔하게 정산을 처리할 수 있을지 고민하는 과정에서 글을 작성하게 되었습니다.





1. 문제 상황


수 만 개의 가맹점이 존재할 때, 일부 가맹점들 은 정산 모델이 다를 수 있습니다. 한 두 개의 정산 모델만 다른 경우 분기문 을 사용해 정산할 수 있지만, 애매하게 10개, 20개 정도가 다르다면 분기문이 너무 많아져 가독성이 떨어지고 유지보수가 어려워집니다. 대략 이런 코드죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class SettlementService {

    @Transactional
    public void settle(final Merchant merchant) {
        if (merchant.getSettlementModel() == SettlementModel.SAMSUNG) {
            // 삼성 가맹점에 대한 정산 로직
        } else if (merchant.getSettlementModel() == SettlementModel.HYUNDAI) {
            // 현대 가맹점에 대한 정산 로직
        } else if (merchant.getSettlementModel() == SettlementModel.LOTTE) {
            // 롯데 가맹점에 대한 정산 로직
        } else {
            // 다른 일반 가맹점에 대한 정산 로직
        }
    }
}





정산 로직에서는 다양한 조건에서 분기문이 정말 많이 등장하는데요, 이런 분기문을 줄이지 않으면 가독성 저하유지보수 어려움 을 겪게 됩니다. 잘 변하진 않지만 한 번 변하면 지옥을 맛보거든요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Service
public class SettlementService {

    @Transactional
    public void settle(final Merchant merchant) {
        if (merchant.getSettlementModel() == SettlementModel.SAMSUNG) {
            if (merchant.getType() == Type.DIRECT) {
                if (merchant.isOverseas()) {
                    // 삼성 + 직접판매 + 해외
                } else {
                    // 삼성 + 직접판매 + 국내
                }
            } else if (merchant.getType() == Type.AGENCY) {
                if (merchant.hasSpecialAgreement()) {
                    // 삼성 + 대리점 + 특약 있음
                } else {
                    // 삼성 + 대리점 + 특약 없음
                }
            }
        } else if (merchant.getSettlementModel() == SettlementModel.HYUNDAI) {
            if (merchant.getRegion() == Region.SEOUL) {
                // 현대 + 서울
            } else if (merchant.getRegion() == Region.BUSAN) {
                // 현대 + 부산
            } else {
                // 현대 + 기타 지역
            }
        } else if (merchant.getSettlementModel() == SettlementModel.LOTTE) {
            if (merchant.isPromotionActive()) {
                if (merchant.getPromotionType() == PromotionType.PERCENTAGE) {
                    // 롯데 + 프로모션 + 퍼센트
                } else {
                    // 롯데 + 프로모션 + 정액
                }
            } else {
                // 롯데 + 프로모션 없음
            }
        } else {
            // 기본 정산
            if (merchant.isBlacklisted()) {
                // 블랙리스트 예외 정산
            } else {
                // 일반 정산
            }
        }
    }
}





일부 변하는 것도 어려운데, 새로운 특수 정산 모델이 추가 되어 기존 코드를 크게 수정해야 한다면 어디서 사이드 이펙트를 발생시킬 지 감을 잡을 수 없게 됩니다. 위에 나온 예제 코드는 사실 귀여운 수준인데요, 실제로는 훨씬 더 복잡한 분기문이 존재합니다. 거기다 중간에 등장하는 한 방 쿼리(💩)들은 정신을 놓게 만듭니다.

현재 팀에서는 특별한 가맹점의 정산 모델을 점별 정산 이라고 부르며 특수하게 취급하고 있습니다.







2. 해결 방안


이런 문제를 해결하기 위해 전략 패턴(Strategy Pattern) 을 사용할 수 있습니다.

Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.



전략 패턴은 너무 유명하기 때문에 자세한 설명은 생략하겠습니다. 이를 통해 각 가맹점의 정산 모델에 따라 다른 정산 로직을 깔끔하게 처리할 수 있습니다. 정산이라는 변하지 않는 행위 는 동일하지만, 각 가맹점마다 다른 알고리즘 을 적용할 수 있는 것이죠. 변하지 않는 행위는 인터페이스로 정의하고, 각 가맹점마다 다른 알고리즘을 구현하는 것입니다.

1
2
3
public interface SettlementStrategy {
    void settle(Merchant merchant);
}




예를 들어, 대부분의 가맹점에 적용되는 일반 정산 모델별도의 정산 모델 을 가지고 있는 삼성, 현대, 롯데 정산 모델이 있다면 다음과 같이 구현할 수 있죠.

1
2
3
4
5
6
7
8
9
@Service
public class DefaultSettlementStrategy implements SettlementStrategy {
    
    @Override
    @Transactional
    public void settle(final Merchant merchant) {
        // 기본 정산 로직
    }
}
1
2
3
4
5
6
7
8
9
@Service
public class SamsungSettlementStrategy implements SettlementStrategy {
    
    @Override
    @Transactional
    public void settle(final Merchant merchant) {
        // 삼성 정산 모델 로직
    }
}
1
2
3
4
5
6
7
8
9
@Service
public class HyundaiSettlementStrategy implements SettlementStrategy {
    
    @Override
    @Transactional
    public void settle(final Merchant merchant) {
        // 현대 정산 모델 로직
    }
}
1
2
3
4
5
6
7
8
9
@Service
public class LotteSettlementStrategy implements SettlementStrategy {
    
    @Override
    @Transactional
    public void settle(final Merchant merchant) {
        // 롯데 정산 모델 로직
    }
}




정산 모델은 팩토리(Factory) 를 통해 적절한 전략을 선택하여 사용할 수 있습니다. 이러면 복잡한 분기문이 사라지고 각 정산 모델에 맞는 적절한 전략을 선택 하여 사용할 수 있죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class SettlementStrategyFactory {

    private final Map<String, SettlementStrategy> strategyMap;

    public SettlementStrategyFactory(final List<SettlementStrategy> strategies) {
        this.strategyMap = new HashMap<>();
        for (final SettlementStrategy strategy : strategies) {
            strategyMap.put(strategy.getClass().getSimpleName(), strategy);
        }
    }

    public SettlementStrategy getStrategy(final String strategyKey) {
        final SettlementStrategy strategy = strategyMap.get(strategyKey);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown settlement strategy: " + strategyKey);
        }
        return strategy;
    }
}







3. 정리


별 내용은 없었는데요, 특별한 정산 모델을 처리할 때, 어떻게 하면 분기문을 줄여 처리할 수 있을까 고민하다가 전략 패턴 이 떠올랐습니다. 이를 안다고 해도 정산 로직은 정말 복잡하기 때문에 이제 시작이라고 생각합니다. 다시 코드 레벨로 🚀


This post is licensed under CC BY 4.0 by the author.

로컬에서 Github Action 실행하기

스케줄링 시스템의 분류와 설계 관점 살펴보기