개요
Part 1 에서는 쿠폰 발급 로직을 전략 패턴을 통해 분리하고 서비스 레이어를 명확히 나누어 유지보수성과 확장성을 향상시킨 내용을 다루었다.
이번 포스팅에서는 쿠폰 발급의 빈도(IssuanceFrequency)와 제한 조건을 전략 패턴을 이용해 보다 유연하고 효율적으로 관리하는 방법을 소개하려고한다.
쿠폰 시스템의 특성을 자세히 생각해본다면 "얼마나, 누구한테, 언제, 쿠폰을 발급할 것인가?" 이다.
따라서 발급 빈도와 제한 조건을 명확히 정의해야지, 이벤트 쿠폰이나 회원가입 축하 쿠폰 등 다양한 조건과 상황에 유연하게 대응이 가능해진다.
예를 들어, 최근 클라이언트가 요청한 특정 기간에만 발급되는 신규 가입 이벤트 쿠폰의 경우를 살펴보자.
클라이언트 요청은 특정 기간(예: 3월 23일~3월 31일)에만 기존의 회원가입 축하 쿠폰 대신 새로운 쿠폰을 발급하고 싶어 했다. 또한, 이벤트 기간이 종료된 후에는 기존의 쿠폰 발급 전략으로 다시 복구를 요청 하기도 하였다.
이전 포스팅에서는 이러한 요구사항을 효과적으로 관리하기 위해, 쿠폰 발급 전략을 별도의 정책(Policy) 엔티티로 정의하여 데이터베이스에 저장하고 관리하였다.
-> 하지만 이때마다 매번 정책을 삭제하고 재생성하는 방식은 기존 쿠폰 발급 이력 관리나 통계 조회 시 혼란을 야기할 수 있다.
따라서 정책에 대한 soft delete 방식을 활용하면 기존 데이터를 안전하게 보존하면서도 효율적으로 정책을 활성화 및 비활성화할 수 있다.
또한, 해당 방식을 활용하게 된다면, 관리자 패널에서 한 가지 타입의 쿠폰 발급 전략을 동시에 단 하나만 활성화할 수 있도록 제한 조건을 추가를 통해서 위에 우리가 고민했던 상황에 유연하게 대처가 편해질 것이다.
즉, 관리자 패널에서 정책 유형별로 쿠폰 발급 전략 목록을 제공하고, 그 중 딱 하나만 활성화 상태로 관리함으로써 발급 전략의 명확성과 유지보수 편의성을 높일 수 있다.
이처럼 쿠폰 발급 빈도와 제한 조건을 명확히 설정하고 전략 패턴으로 구현하면 응집성 있게 다양한 비즈니스 요구사항에 효율적으로 대응할 수 있게 된다.
그렇다면, 내가 쿠폰 발급 제한 조건을 나눈 기준
그렇다면, 쿠폰 발급 제한 조건을 나누는 기준은 어떻게 설계하면 좋을까?
쿠폰 발급의 빈도와 제한 조건을 명확히 관리하기 위해서는 먼저 쿠폰이 어떤 상황에서, 얼마나 자주 발급될지를 정확히 분석해야 한다.
1. 사용자 단위 제한 (ONCE_PER_USER)
가장 흔하게 사용되는 제한 조건으로, 한 명의 사용자가 특정 쿠폰을 단 한 번만 받을 수 있도록 관리하는 방식이다.
예를 들어, 회원 가입 축하 쿠폰이나 특정 프로모션 참여에 대한 보상으로 발급되는 쿠폰이 여기에 해당한다. 이러한 제한은 사용자 단위로 쿠폰 중복 발급을 방지하고 공정성을 유지하는 데 유용하다.
2. 연 단위 제한 (ONCE_PER_YEAR)
매년 반복적으로 진행되는 이벤트나 프로모션에서 활용되는 방식으로, 특정 쿠폰을 사용자당 연간 1회만 받을 수 있게 제한한다.
주로 생일 쿠폰, 연말 감사 쿠폰 등 정기적으로 반복 발급되는 쿠폰에 적합한 제한 방식이다.
배치 프로세스를 통해 매년 지정된 날짜에 자동으로 발급 및 관리가 가능하도록 구현하는 것이 바람직하다.
3. 이벤트 단위 제한 (ONCE_PER_EVENT)
특정 이벤트나 프로모션 기간 동안 사용자가 단 한 번만 쿠폰을 발급받을 수 있도록 설정하는 방식이다.
이벤트 기간 내에 과도한 중복 참여나 악용 사례를 방지하는 데 효과적이다. 이 제한 방식은 특히 사용자가 몰리는 대규모 이벤트에서 동시성을 효과적으로 관리하기 위해, 쿠폰 재고 관리와 낙관적 락(Optimistic Lock)과 같은 기술적 전략이 필수적으로 요구된다.
위의 제한 조건들은 개별 쿠폰 정책(Policy)으로 관리하고, 이를 전략 패턴(Strategy Pattern)을 활용해 동적으로 적용함으로써 각 정책이 쿠폰 발급 프로세스에 쉽게 추가 및 교체될 수 있게 한다.
이를 통해 정책 변경이나 추가 요청이 들어올 때마다 기존 시스템의 코드를 수정하지 않고도 유연하게 대응할 수 있는 환경이 구축되는 것이다.
정리
사용자 단위 제한 | 한 사용자가 한 번만 받을 수 있는 쿠폰 (ONCE_PER_USER) |
연 단위 제한 | 연 1회만 받을 수 있는 쿠폰 (ONCE_PER_YEAR) |
이벤트 단위 제한 | 특정 이벤트 기간 동안 한 번만 받을 수 있는 쿠폰 (ONCE_PER_EVENT) |
IssueFrequency 의 도입과 빈도 기반의 전략 패턴 적용
쿠폰 정책을 설계할 때 단순히 쿠폰 발급 시점을 결정하는 것만으로는 부족하다.
발급 빈도를 설정하고 이를 관리할 수 있는 명확한 전략이 있어야 시스템이 보다 유연해지고 관리가 수월해진다.
이를 위해 도입한 것이 바로 IssuanceFrequency라는 개념이다.
IssuanceFrequency는 다음과 같이 정의할 수 있다
package net.dayner.api.domain.coupon.entity;
public enum IssuanceFrequency {
ONCE_PER_USER, // 최초 1회
ONCE_PER_YEAR, // 연 1회
ONCE_PER_EVENT, // 이벤트 기간 내 1회
ONCE_PER_MONTH, // 월 1회
ONCE_PER_WEEK, // 주 1회
DAILY_LIMITED, // 하루 1회 (예: 출석 체크 쿠폰)
CUSTOM // 커스텀 정책 (별도 로직 필요)
}
쿠폰 정책을 생성할 때 발급 빈도(IssuanceFrequency)를 설정하면, 발급 시점에서 사용자 및 정책의 조건에 따라 적절한 검증 로직을 수행해야 한다.
하지만 이 발급 제한 로직을 모두 단일 클래스나 메서드에서 처리할 경우, 코드가 복잡해지고 유지보수가 어렵다.
이러한 문제를 해결하기 위해 전략 패턴(Strategy Pattern) 을 도입한다.
전략 패턴을 활용하면 IssuanceFrequency 타입별로 서로 다른 발급 제한 로직을 별도의 클래스에서 관리할 수 있으며, 런타임에 동적으로 선택하여 실행할 수 있다.
예를 들어, 사용자가 한 번만 받을 수 있는 쿠폰(ONCE_PER_USER)은 사용자의 발급 이력을 확인해 이미 발급된 적이 있는지 여부를 체크하는 로직이 필요하다.
반면, 연 단위 제한(ONCE_PER_YEAR) 쿠폰은 발급된 시점을 기준으로 특정 연도에만 한 번 허용하고, 이벤트 단위 제한(ONCE_PER_EVENT) 쿠폰은 특정 이벤트 기간을 기준으로 중복 발급 여부를 체크해야 한다.
결국, 전략 패턴을 통해 쿠폰 발급 조건을 동적으로 적용함으로써, 새로운 빈도 조건이 추가되거나 기존 발급 제한 조건이 변경되더라도 코드 수정 없이 전략 클래스를 추가하거나 교체하는 것만으로 쉽게 대응할 수 있게 된다.
EligibilityStrategyFactory를 통한 동적 전략 주입
쿠폰의 발급 가능 여부를 판단하는 로직은 쿠폰 정책의 발급 빈도(IssuanceFrequency)에 따라 달라지며, 이 로직을 직접 쿠폰 발급 프로세스 내부에서 처리하면 코드가 복잡해지고 변경이 어려워진다.
이를 효율적으로 해결하기 위해 EligibilityStrategyFactory를 도입하여 발급 제한 로직을 동적으로 관리한다.
구체적으로 EligibilityStrategyFactory는 정책의 IssuanceFrequency 값을 바탕으로 적절한 발급 제한 전략을 매핑하여 제공하는 역할을 담당한다.
아래처럼 구현해두었다.
@Component
public class EligibilityStrategyFactory {
private final Map<IssuanceFrequency, EligibilityStrategy> strategyMap = new HashMap<>();
public EligibilityStrategyFactory(CouponHistoryRepository couponHistoryRepository) {
strategyMap.put(ONCE_PER_USER, new OncePerUserEligibilityStrategy(couponHistoryRepository));
strategyMap.put(ONCE_PER_YEAR, new OncePerYearEligibilityStrategy(couponHistoryRepository));
strategyMap.put(ONCE_PER_EVENT, new OncePerEventEligibilityStrategy(couponHistoryRepository));
}
public EligibilityStrategy getStrategy(IssuanceFrequency frequency) {
return strategyMap.get(frequency);
}
}
전략패턴을 도입하면 뭐가 좋을까?
OCP(개방-폐쇄 원칙) 준수
새로운 발급 제한 정책을 추가하거나 기존 정책을 변경할 때 CouponProcessor 같은 핵심 로직을 수정하지 않고도 새로운 전략 클래스만 추가하면 되므로 확장성이 높아진다.
유지보수 용이
전략별 클래스에서 로직이 명확히 분리되어 있으므로 특정 조건 변경이나 신규 로직 추가 시 해당 전략만 변경하거나 추가하면 된다.
발급 주기별 전략 구현
쿠폰 정책을 생성할 때, IssuanceFrequency를 설정하면 해당 쿠폰이 어떤 빈도로 발급될지를 결정할 수 있다.
(1) OncePerUserEligibilityStrategy: 1회 한정 발급
public class OncePerUserEligibilityStrategy implements EligibilityStrategy {
private final CouponHistoryRepository couponHistoryRepository;
public OncePerUserEligibilityStrategy(CouponHistoryRepository couponHistoryRepository) {
this.couponHistoryRepository = couponHistoryRepository;
}
@Override
public boolean isEligible(Long userId, Long policyId) {
return !couponHistoryRepository.existsByUserIdAndPolicyId(userId, policyId);
}
}
✔ 사용자가 해당 쿠폰을 한 번이라도 받은 적이 있으면 발급 불가
(2) OncePerYearEligibilityStrategy: 연 1회 발급 제한
public class OncePerYearEligibilityStrategy implements EligibilityStrategy {
private final CouponHistoryRepository couponHistoryRepository;
public OncePerYearEligibilityStrategy(CouponHistoryRepository couponHistoryRepository) {
this.couponHistoryRepository = couponHistoryRepository;
}
@Override
public boolean isEligible(Long userId, Long policyId) {
int currentYear = LocalDate.now().getYear();
LocalDateTime startOfYear = LocalDate.of(currentYear, 1, 1).atStartOfDay();
LocalDateTime endOfYear = LocalDate.of(currentYear, 12, 31).atTime(23, 59, 59);
return !couponHistoryRepository.existsByUserIdAndPolicyIdAndIssuedAtBetween(
userId, policyId, startOfYear, endOfYear);
}
}
✔ 쿠폰이 발급된 연도를 기준으로 발급 가능 여부 체크
✔ 현재 연도에 한 번도 발급받지 않은 경우에만 쿠폰 지급
(3) OncePerEventEligibilityStrategy: 이벤트 단위 발급 제한
public class OncePerEventEligibilityStrategy implements EligibilityStrategy {
private final CouponHistoryRepository couponHistoryRepository;
public OncePerEventEligibilityStrategy(CouponHistoryRepository couponHistoryRepository) {
this.couponHistoryRepository = couponHistoryRepository;
}
@Override
public boolean isEligible(Long userId, Long policyId) {
return !couponHistoryRepository.existsByUserIdAndPolicyId(userId, policyId);
}
}
✔ 이벤트 단위 발급은 기본적으로 OncePerUserEligibilityStrategy와 유사한 로직을 사용
✔ 단, 특정 이벤트 기간을 DB에서 조회해 체크할 수도 있음
Part 1. CouponProcessor 에서의 전략 적용
Dayner에서 이벤트성, 일회성 쿠폰을 발급하고 관리하는 방법 [1]
개요사실 블로깅할 소재가 쌓여있다.. 인턴하면서 국제화 리팩터링 한 건도 그렇고 알림/메일링 서버도,,, 사업보안인증을 위해서 메인서버 취약점을 고쳤던 소재도 남아있는데,, 클라이언트
nstgic3.tistory.com
생일 축하 쿠폰을 발급하는 과정을 예시로 들어보자
쿠폰 발급 전략을 보다 실질적으로 이해하기 위해, 매달 자동으로 발급되는 생일 축하 쿠폰 배치 작업의 예시를 살펴보자.
아래는 Spring의 @Scheduled 어노테이션을 활용하여 매달 정기적으로 생일 쿠폰을 발급하는 작업을 구현한 예시 코드이다.
@Component
@RequiredArgsConstructor
public class ScheduledTasks {
private final UserFacade userFacade;
private final CouponProcessor couponProcessor;
private final CacheManager cacheManager;
// 매달 1일 새벽 1시에 생일 쿠폰 발급 배치 실행
@Scheduled(cron = "0 0 1 1 * *")
public void issueMonthlyBirthdayCoupon() {
userFacade.issueBirthdayCoupon();
}
}
// UserFacade 내부 메서드
public void issueBirthdayCoupon() {
int currentMonth = LocalDate.now().getMonthValue();
// 생일이 해당 월에 속한 사용자 조회 및 발급 처리
List<Long> userIdsWithBirthday = userRepository.findUserIdsByBirthdayMonth(currentMonth);
userIdsWithBirthday.forEach(userId ->
couponProcessor.processCouponIssuance(userId, PolicyType.BIRTHDAY_COUPON, 3)
);
}
위의 코드와 같이 매달 지정된 날짜와 시간에 배치 작업을 실행하여 지금까지 구현하였던 Processor에 BIRTHDAY 타입과 함께 생일 쿠폰을 자동 발급하는 로직을 간단히 구현할 수 있다.
이 방식을 활용하면 시스템 관리자는 복잡한 수동 작업 없이 정해진 기간에 정확히 쿠폰을 발급할 수 있으며, 동시에 시스템의 유지보수성과 효율성 또한 향상된다.
또한, 주기적인 작업은 배치로 처리함으로써, 사용자 경험을 저하시키지 않고, 트래픽을 효율적으로 관리할 수 있는 장점이 있다.
마무리
이번 글에서는 쿠폰 발급의 빈도와 제한 조건을 명확히 관리하기 위해 IssuanceFrequency와 EligibilityStrategy를 도입하여 전략 패턴을 활용한 구조 개선을 다뤘다. 또한, 쿠폰 정책의 명확한 분류와 관리를 위해 PolicyType을 활용했고, 실질적인 예로 생일 쿠폰의 배치 작업을 살펴보았다.
이를 통해 얻은 주요 개선점은 다음과 같다.
명확한 책임 분리
- 쿠폰 정책(PolicyType)과 발급 빈도(IssuanceFrequency), 그리고 전략(EligibilityStrategy)의 책임을 명확히 구분함.
OCP(개방-폐쇄 원칙) 준수
- 새로운 쿠폰 발급 제한 조건이 추가되더라도 기존 코드를 수정하지 않고 전략 클래스만 추가하면 확장 가능.
관리 편의성 향상
- 정책과 발급 전략을 명확히 나눠 관리함으로써, 유지보수와 운영 효율성을 높임.
- 실제로 해당 클라이언트의 요청으로 부터 변경사항 적용-배포까지의 과정이 10분이 채 걸리지 않았다
이외에도 이벤트로 인한 트래픽 몰림 현상을 대비해서 버저닝을 통해 낙관락을 적용했다거나 동시성 관련 구현도 되어있는데
참고 문헌
데이터 중심 애플리케이션 설계 - 마틴 클레프만
자바/스프링 개발자를 위한 실용주의 프로그래밍 - 김우근
내 머리속 내공냠냠
'Dayner 프로젝트' 카테고리의 다른 글
Dayner에서 이벤트성, 일회성 쿠폰을 발급하고 관리하는 방법 [1] (0) | 2025.03.07 |
---|---|
Dayner에서 구매 이력을 관리하는 방법 [1] (feat: 개인정보보호법, 원장 데이터) (4) | 2024.08.31 |
영업시간 디자인 변경에 대응한 리팩토링 일지 [2] (feat: 버전별 API 캐시 전략) (0) | 2024.08.25 |
영업시간 디자인 변경에 대응한 리팩토링 일지 [1] (feat: 연결된 일정 구현하기) (0) | 2024.08.22 |
반년 간의 EC2 Spring 배포 Thread Stravation 장애 해결기 (0) | 2024.08.10 |