[리팩토링] 쿠폰 정책 생성 전략패턴 적용 정리 - 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. 정리 및 전체 흐름도

  1. 애플리케이션이 시작될때 Spring은 @Component가 붙은 모든 CouponPolicyCreator 구현체들을 생성하여 CouponPolicyCreatorFactory에 주입
  2. Controller를 통해 쿠폰 정책 생성 요청이 들어오면 Service는 Factory에 findCreator 호출
  3. findCreator는 주입받은 전략 객체를 순회하며 supports() 메서드 실행 요청 타입과 일치하는 전략 객체 찾아서 반환
  4. 해당 전략 객체의 create 메서드를 호출해서 실제 쿠폰 로직 실행
  • 전략 패턴 흐름 다이어그램
  • 정리 : if-else if-else if …등 분기처리로 하던 코드를 스프링의 기능들을 활용해서 리팩토링 해보는 과정을 해보았습니다.
  • 향후 공부하실때 도움이 됐으면 좋겠습니다