글을 작성하게 된 계기
가맹점마다 정산 모델이 다를 때, 어떻게 분기문을 줄이면서 깔끔하게 정산을 처리할 수 있을지 고민하는 과정에서 글을 작성하게 되었습니다.
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) 을 사용할 수 있습니다.
전략 패턴은 너무 유명하기 때문에 자세한 설명은 생략하겠습니다. 이를 통해 각 가맹점의 정산 모델에 따라 다른 정산 로직을 깔끔하게 처리할 수 있습니다. 정산이라는 변하지 않는 행위 는 동일하지만, 각 가맹점마다 다른 알고리즘 을 적용할 수 있는 것이죠. 변하지 않는 행위는 인터페이스로 정의하고, 각 가맹점마다 다른 알고리즘을 구현하는 것입니다.
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. 정리
별 내용은 없었는데요, 특별한 정산 모델을 처리할 때, 어떻게 하면 분기문을 줄여 처리할 수 있을까 고민하다가 전략 패턴 이 떠올랐습니다. 이를 안다고 해도 정산 로직은 정말 복잡하기 때문에 이제 시작이라고 생각합니다. 다시 코드 레벨로 🚀