갱스터하우스

[쿠.파.프] 만료된 쿠폰 개선기 | EP3. Scheduler를 이용해 유효기간이 만료된 쿠폰 상태 업데이트 하기 본문

Project

[쿠.파.프] 만료된 쿠폰 개선기 | EP3. Scheduler를 이용해 유효기간이 만료된 쿠폰 상태 업데이트 하기

승갱 2025. 3. 8. 21:40

이 글은 제가 학습하고 경험한 것을 바탕으로 작성하여,

일부 부족하거나 잘못된 점이 있을 수 있습니다.
부족한 부분이나 잘못된 내용에 대한 피드백을 주시면 감사히 배우겠습니다!

 

 

 

 

 

1. 문제점 - 유효기간이 지나도 만료되지 않는 쿠폰의 상태

쿠폰 테이블

 

쿠폰 테이블을 조회했을 때,

쿠폰의 expiredAt이 지나도 여전히 쿠폰의 상태는 "AVAILABLE", 

즉, 사용가능한 status로 남아있었다.

 

 

Q. 어떻게 하면 좋을까?

처음에는 Redis를 이용한 방법을 생각했다.

1. 쿠폰 발급 시, 해당 쿠폰 아이디를 Key로 Redis에 저장

2. TTL 만료 시, Redis Keyspace Notifications 이용해 status 상태를 "EXPIRED"로 업데이트

 

하지만 곧바로 의문이 들었다.

1. 쿠폰을 발급받을 때마다 모든 쿠폰Redis저장하는 것이 효율적인 것인가?

2. 같은 쿠폰에 대하여, 발급 시 받은 쿠폰의 TTL 만료와 사용 중인 쿠폰의 TTL 만료를 어떻게 처리할 것인가?

 

 

1 ➡️

  • 발급받은 쿠폰의 양이 증가한다면, 메모리 사용량 증가하기 때문에 성능 부담이 간다(+ 비용도 ↑)
  • 자주 조회되는 데이터를 처리하기 위해 Redis를 사용하는 것이 아니라, 만료된 쿠폰을 "일괄처리" 하려고 하는 것이라 적합하지 않다

 

2 ➡️

[기존 로직]

유저가 쿠폰을 사용할 때 해당 쿠폰에 대한 정보를 Redis에 등록하며

해당 키의 TTL이 만료되면 쿠폰의 상태를 "APPLIED"로 변경한다. 

 

여기서 사용하는 쿠폰뿐만 아니라 쿠폰을 발급받을 때에도 Redis에 저장한다면,

(유저가 쿠폰을 사용할 때 발급받은 쿠폰의 TTL + 사용 중인 쿠폰의 TTL) 두 가지를 같이 처리해야 하는 문제가 발생한다!

 

그리고 쿠폰의 최종 상태는 두 가지로 나뉜다.

(1) 유저가 쿠폰을 사용해서 만료된 경우 -> "APPLIED"

(2) 사용하지 않고 쿠폰의 유효기간이 만료된 경우 -> "EXPIRED"

 

하나의 쿠폰에 대해 최종적으로 두 가지 상태가 나오는 데이터 불일치를 방지하기 위해,

사용중인 쿠폰의 Key가 만료될 때 발급 당시 등록된 쿠폰의 Key도 같이 만료해야 한다.

이 과정에서 발급 당시 등록된 쿠폰의 Key 가 만료되는 이벤트도 처리해야 하는데 아무리 생각해도 이 방법은 너무 비효율적이었다. 

 

만약 쿠폰의 유효기간을 날짜를 기준으로 설정했다면 메세지큐를 이용해 처리해보고 싶었지만
우리의 쿠폰 정책은 발급받은 시각을 기준으로 하기 때문에 넘어갔다...

고민 끝에 현재로서 최선의 방법이라 생각해, 스케줄러를 돌려 처리하기로 했다.

목표는 매일 오전 12시에 현재 날짜를 기준으로 expiredAt이 지난 쿠폰 처리하기!

 

 

2. 해결하기 - 스케줄러를 이용하자

CouponScheduler.java

@Component
@RequiredArgsConstructor
@Slf4j
public class CouponScheduler {

    private final CouponService couponService;

    @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
    public void expireCouponScheduler() {
        int expiredCount = couponService.expireCoupons();
        log.info("[스케줄러 실행] 만료된 쿠폰 개수: {}", expiredCount);
    }
}

복잡했던 고민 과정과 달리 스케줄러 구현은 단순하다.

매일 오전 12시에 스케줄러를 반복하기 때문에

이처럼 cron을 설정한다.

 

 

CouponService.java

public int expireCoupons() {
     return couponRepository.expireCoupons(LocalDateTime.now());
}

쿠폰의 만료 처리는 service단에서 처리하도록 설계했다.

이때, 날짜 기준으로 만료처리를 하기 때문에 현재시각을 넘겼다.

(근데 넘기는 시간을 now()로 할지 아니면 localdate+12:000로 넘겨야 할지 고민됐다.

우선은 스케줄러가 돌아간다는 것에 의미 두어 now()를 택했다.)

 

 

CouponRepository.java

@Modifying(clearAutomatically = true)
@Transactional
@Query(
       "UPDATE Coupon c SET c.status = 'EXPIRED' 
       WHERE c.expiredAt <= :currentDateTime AND c.status = 'AVAILABLE'")
int expireCoupons(@Param("currentDateTime") LocalDateTime currentDateTime);

 

쿠폰테이블에서 expiredAt이 currentDateTime 보다 이전인 쿠폰 중,

상태가 'AVAILABLE'인 쿠폰을 'EXPIRED'로 UPDATE 하는 쿼리를 작성했다.

 

 

 

3. 테스트하기

테스트를 할 때는 3분 후 테스트를 진행할 수 있도록 cron을 수정했다.

테스트를 실행하면,

만료된 쿠폰의 개수가 로그로 찍히고

 

만료된 쿠폰의 상태

쿠폰테이블 조회 시, 유효기간이 지난 쿠폰들의 status가 "EXPIRED"로 변경된 것을 확인할 수 있다.

 

 

 

 

 

이렇게 Scheduler를 이용한 유효기간이 지난 쿠폰의 상태 업데이트까지 완료하며

쿠폰 파생 프로젝트를 마쳤다!