[리팩토링] 쿠폰 정책 생성 전략패턴 적용 정리 - nhnacademy-be10-WannaB/wannab-wiki GitHub Wiki
Strategy-Pattern
1.기존 코드(CouponService.java)
- 기존의 문제점 쿠폰 생성시 새로운 쿠폰 타입이 생길때마다 코드를 바꿔줘야함 → OCP,LSP 원칙 위배
@Transactional
public void createCouponPolicy(CreateCouponPolicyDto request) {
CouponPolicy couponPolicy = CouponPolicy.builder()
.couponPolicyName(request.getName())
.discountType(DiscountType.valueOf(request.getDiscountType()))
.discountValue(request.getDiscountValue())
.maxDiscount(request.getMaxDiscount())
.minPurchase(request.getMinPurchase())
.validDays(request.getValidDays())
.fixedStartDate(request.getStartDate())
.fixedEndDate(request.getEndDate())
.policyStatus(PolicyStatus.ACTIVE).build();
CouponType newCouponType;
if (request.getCouponType().equals("NORMAL")) {
if (request.isBirthday()) {
newCouponType = CouponType.BIRTHDAY;
} else if (request.isWelcome()) {
newCouponType = CouponType.WELCOME;
} else {
newCouponType = CouponType.CUSTOM;
}
couponPolicy.setCouponType(newCouponType);
if (couponPolicyRepository.findByCouponTypeAndPolicyStatus(newCouponType, PolicyStatus.ACTIVE).isPresent()) {
throw new CouponPolicyException(CouponPolicyErrorCode.POLICY_ALREADY_EXISTS);
}
couponPolicyRepository.save(couponPolicy);
} else if (request.getCouponType().equals("BOOK")) {
newCouponType = CouponType.BOOK;
long bookId = request.getTargetBookId();
if (bookId <= 0) {
throw new CouponPolicyException(CouponPolicyErrorCode.INVALID_BOOK_ID);
}
if (policyTargetBookRepository.findByBookId(bookId).isPresent()) {
throw new CouponPolicyException(CouponPolicyErrorCode.BOOK_POLICY_ALREADY_EXISTS);
}
couponPolicy.setCouponType(newCouponType);
couponPolicyRepository.save(couponPolicy);
createPolicyTargetBook(bookId, couponPolicy);
} else {
newCouponType = CouponType.CATEGORY;
long categoryId = request.getTargetCategoryId();
if (categoryId <= 0) {
throw new CouponPolicyException(CouponPolicyErrorCode.INVALID_CATEGORY_ID);
}
if (policyTargetCategoryRepository.findByCategoryId(categoryId).isPresent()) {
throw new CouponPolicyException(CouponPolicyErrorCode.CATEGORY_POLICY_ALREADY_EXISTS);
}
couponPolicy.setCouponType(newCouponType);
couponPolicyRepository.save(couponPolicy);
createPolicyTargetCategory(categoryId, couponPolicy);
}
}
private void createPolicyTargetBook(long bookId, CouponPolicy couponPolicy) {
PolicyTargetBook policyTargetBook = PolicyTargetBook.builder()
.bookId(bookId)
.couponPolicy(couponPolicy).build();
policyTargetBookRepository.save(policyTargetBook);
}
private void createPolicyTargetCategory(long categoryId, CouponPolicy couponPolicy) {
PolicyTargetCategory policyTargetCategory = PolicyTargetCategory.builder()
.categoryId(categoryId)
.couponPolicy(couponPolicy).build();
policyTargetCategoryRepository.save(policyTargetCategory);
}
- 위와 같이 쿠폰 정책 타입별로 if문으로 분기처리해서 관리해야하는 불편함이있고
- 만약 실제 서비스에서 쿠폰 타입을 추가한다면 if문이 점점 늘어나면서 유지보수에 비용이 올라가게됨
- ex) VIP 쿠폰을 추가한다고 가정할때 else if(request.getCouponType.equals(”VIP”)…
2.그래서 해결 방법이 뭔데?
-
해결 방법을 찾아보던중 전략 패턴을 찾았고 해당 패턴으로 리팩토링
-
참고 블로그 : https://victorydntmd.tistory.com/292
-
밑에는 현재 우리의 쿠폰 서비스에 적용한 코드
-
쿠폰 정책 생성 인터페이스 생성
public interface CouponPolicyCreator {
boolean supports(String couponType);
void createCouponPolicy(CreateCouponPolicyDto createCouponPolicyDto);
}
- 인터페이스 구현체 생성(일반 쿠폰 등록)
@Component
@RequiredArgsConstructor
public class NormalCouponPolicyCreator implements CouponPolicyCreator {
private final CouponPolicyRepository couponPolicyRepository;
@Override
public boolean supports(String couponType) {
return "NORMAL".equalsIgnoreCase(couponType);
}
@Override
public void createCouponPolicy(CreateCouponPolicyDto request) {
CouponType newCouponType;
if (request.isBirthday()) {
newCouponType = CouponType.BIRTHDAY;
} else if (request.isWelcome()) {
newCouponType = CouponType.WELCOME;
} else {
newCouponType = CouponType.CUSTOM;
}
if (couponPolicyRepository.findByCouponTypeAndPolicyStatus(newCouponType, PolicyStatus.ACTIVE).isPresent()) {
throw new CouponPolicyException(CouponPolicyErrorCode.POLICY_ALREADY_EXISTS);
}
CouponPolicy couponPolicy = buildBasePolicy(request);
couponPolicy.setCouponType(newCouponType);
couponPolicyRepository.save(couponPolicy);
}
private CouponPolicy buildBasePolicy(CreateCouponPolicyDto request) {
return CouponPolicy.builder()
.couponPolicyName(request.getName())
.discountType(DiscountType.valueOf(request.getDiscountType()))
.discountValue(request.getDiscountValue())
.maxDiscount(request.getMaxDiscount())
.minPurchase(request.getMinPurchase())
.validDays(request.getValidDays())
.fixedStartDate(request.getStartDate())
.fixedEndDate(request.getEndDate())
.policyStatus(PolicyStatus.ACTIVE).build();
}
- 인터페이스 구현체 생성(도서 쿠폰 등록)
@Component
@RequiredArgsConstructor
public class BookCouponPolicyCreator implements CouponPolicyCreator {
private final CouponPolicyRepository couponPolicyRepository;
private final PolicyTargetBookRepository policyTargetBookRepository;
@Override
public boolean supports(String couponType) {
return "BOOK".equalsIgnoreCase(couponType);
}
@Override
public void createCouponPolicy(CreateCouponPolicyDto request) {
long bookId = request.getTargetBookId();
if (bookId <= 0) {
throw new CouponPolicyException(CouponPolicyErrorCode.INVALID_BOOK_ID);
}
if (policyTargetBookRepository.findByBookId(bookId).isPresent()) {
throw new CouponPolicyException(CouponPolicyErrorCode.BOOK_POLICY_ALREADY_EXISTS);
}
CouponPolicy couponPolicy = buildBasePolicy(request);
couponPolicy.setCouponType(CouponType.BOOK);
CouponPolicy savedPolicy = couponPolicyRepository.save(couponPolicy);
PolicyTargetBook policyTargetBook = PolicyTargetBook.builder()
.bookId(bookId)
.couponPolicy(savedPolicy)
.build();
policyTargetBookRepository.save(policyTargetBook);
}
private CouponPolicy buildBasePolicy(CreateCouponPolicyDto request) {
return CouponPolicy.builder()
.couponPolicyName(request.getName())
.discountType(DiscountType.valueOf(request.getDiscountType().toUpperCase()))
.discountValue(request.getDiscountValue())
.maxDiscount(request.getMaxDiscount())
.minPurchase(request.getMinPurchase())
.fixedStartDate(request.getStartDate())
.fixedEndDate(request.getEndDate())
.policyStatus(PolicyStatus.ACTIVE)
.build();
}
-
이런식으로 계속 인터페이스 implements 받아서 구현체만 만들어주고
-
쿠폰 정책 생성 팩토리 추가
@Component
public class CouponPolicyCreatorFactory {
private final List<CouponPolicyCreator> creators;
public CouponPolicyCreatorFactory(List<CouponPolicyCreator> creators) {
this.creators = creators;
}
public CouponPolicyCreator findCreator(String couponType){
return creators.stream()
.filter(creator -> creator.supports(couponType))
.findFirst()
.orElseThrow(() -> new CouponPolicyException(CouponPolicyErrorCode.INVALID_COUPON_TYPE));
}
}
3.활용은 어떻게 하는데?
-
아까 위에서 예시로 들었던 회원등급에 따른 VIP 타입의 쿠폰을 추가한다고 할때
-
먼저 CouponType Enum에 VIP추가
public enum CouponType {
CUSTOM,
BIRTHDAY,
WELCOME,
BOOK,
CATEGORY,
VIP
}
- CouponPolicyCreator 인터페이스 구현체 생성
@Component
@RequiredArgsConstructor
public class VipCouponPolicyCreator implements CouponPolicyCreator {
private final CouponPolicyRepository couponPolicyRepository;
@Override
public boolean supports(String couponType) {
return "VIP".equalsIgnoreCase(couponType);
}
@Override
public void createCouponPolicy(CreateCouponPolicyDto request) {
CouponPolicy couponPolicy = buildBasePolicy(request);
couponPolicy.setCouponType(CouponType.VIP);
CouponPolicy savedPolicy = couponPolicyRepository.save(couponPolicy);
}
private CouponPolicy buildBasePolicy(CreateCouponPolicyDto request) {
return CouponPolicy.builder()
.couponPolicyName(request.getName())
.discountType(DiscountType.valueOf(request.getDiscountType().toUpperCase()))
.discountValue(request.getDiscountValue())
.maxDiscount(request.getMaxDiscount())
.minPurchase(request.getMinPurchase())
.fixedStartDate(request.getStartDate())
.fixedEndDate(request.getEndDate())
.policyStatus(PolicyStatus.ACTIVE)
.build();
}
끘!
4. 정리 및 전체 흐름도
- 애플리케이션이 시작될때 Spring은 @Component가 붙은 모든 CouponPolicyCreator 구현체들을 생성하여 CouponPolicyCreatorFactory에 주입
- Controller를 통해 쿠폰 정책 생성 요청이 들어오면 Service는 Factory에 findCreator 호출
- findCreator는 주입받은 전략 객체를 순회하며 supports() 메서드 실행 요청 타입과 일치하는 전략 객체 찾아서 반환
- 해당 전략 객체의 create 메서드를 호출해서 실제 쿠폰 로직 실행
- 전략 패턴 흐름 다이어그램
- 정리 : if-else if-else if …등 분기처리로 하던 코드를 스프링의 기능들을 활용해서 리팩토링 해보는 과정을 해보았습니다.
- 향후 공부하실때 도움이 됐으면 좋겠습니다