| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- Java
- 정렬
- 프로그래머스
- 조합
- 정수 삼각형
- 백준
- GIT
- Lv2
- 자바
- Summer/Winter Coding(~2018)
- 알고리즘
- 그래프탐색
- dfs
- DP
- Python
- 다익스트라
- 15686
- BFS
- 1932
- 프로그래멋
- 완전탐색
- 구현
- 월간 코드 챌린지 시즌1
- 그래프
- 분할정복
- 이코테
- 토마토
- 알고리즘고득점Kit
- 깃허브 프로필
- 깃허브
- Today
- Total
갱스터하우스
[쿠.파.프] 만료된 쿠폰 개선기 | EP2. Redis Key Notifications을 통해 TTL이 만료된 쿠폰 상태 업데이트하기 본문
[쿠.파.프] 만료된 쿠폰 개선기 | EP2. Redis Key Notifications을 통해 TTL이 만료된 쿠폰 상태 업데이트하기
승갱 2025. 3. 8. 21:30이 글은 제가 학습하고 경험한 것을 바탕으로 작성하여,
일부 부족하거나 잘못된 점이 있을 수 있습니다.
부족한 부분이나 잘못된 내용에 대한 피드백을 주시면 감사히 배우겠습니다!
1. 사용하는 쿠폰 Redis에 저장하기
Q. 사용하는 쿠폰을 왜 Redis에 저장해?
본 프로젝트의 쿠폰 정책은 유저가 사용하는 시각을 기준으로 24시간이었다.
닉네임 변경과 같이 사용하는 순간, 완료 처리가 되는 쿠폰도 있지만
대부분의 쿠폰은 24시간 동안 그 효력을 발휘했다.
따라서, 유저가 서비스를 이용할 때마다 쿠폰의 사용 여부를 검증해야 했고
이를 위해 매번 DB를 조회해야 한다는 문제와
쿠폰의 상태를 쿠폰 이력 테이블에서만 관리하고 있어
ORDER BY created_at DESC LIMIT 1 이런 쿼리를 매번 날려야 하는 문제가 있었다.
➡️ 그렇다면, 구현하자!
String key = "COUPON_USER_" + useCouponRequest.getUserId();
redisUtil.setObjectExpire(key, useCouponRequest.getCouponId(), TIMER); // 2분으로 설정
빠른 테스트를 위해 TTL은 2분으로 설정했다



key 조회 시, 사용한 쿠폰이 저장된 것을 확인할 수 있다.

2분이 지나고 다시 key를 조회했을 때, TTL 만료로 해당 키가 expired 된 것을 확인할 수 있다
2. 문제점 발생
TTL은 만료되었지만 문제가 발생했다. (= 쿠.파.프의 진행 동기)
바로 쿠폰의 상태는 변하지 않았다는 것이다.
TTL 만료 = 쿠폰 사용 완료 = 쿠폰의 상태 "APPLIED"
이지만, 만료 후 다시 쿠폰 테이블을 조회했을 때

여전히 쿠폰의 상태는 IN_USE, 사용 중이었다.
본 프로젝트에서는 쿠폰 테이블의 비정규화와 스케줄러를 이용해서 해결하기로 했지만,
이 과정에서 알게 된 Redis Key Notifications를 알고만 넘어가기는 아쉬워 적용해 보기로 했다.
Q. Redis Key Notifications 란?
간단하게 말하자면 Redis Key Notification은 Redis에서 등록된 키에 대한 event가 발생할 경우, 이를 알려주는 기능이다.
https://redis.io/docs/latest/develop/use/keyspace-notifications
내 경우에는 Key가 TTL이 만료될 때 이를 이용하는 것이다.
3. Redis Key Notifications
redis.conf 설정
먼저 비활성화되어 있는 Redis Key Notifications를 활성화해주어야 한다.
왜냐하면 Redis Key Notifications는 CPU를 소모하기 때문에 기본적으로는 비활성화되어 있기 때문이다.

먼저 Redis 서버를 중지한다.


그 후, redis.conig에서 notify-keyspace-events 영역에 "Ex"를 추가한다.
Redis Key Notifications는 Redis data 공간에 영향을 주는 두 가지 유형의 이벤트를 보내는 방식으로 실행된다.
바로, Key-space notification와 Key-event notification 두 가지가 있다.
key-space notification : 이벤트 이름을 메시지로 수신받음
key-event notification : key값을 메시지로 수신받음
나의 경우에는 만료이벤트가 발생할 때마다 해당 key값을 가지고 상태를 업데이트해야 하기 때문에 후자를 택했다.

config 설정 후 다시 서버 시작
(1) 1차 구현
RedisConfig에 추가
@Configuration
@EnableRedisRepositories
public class RedisConfig {
......
/*
Redis에서 발생하는 이벤트를 감지하고, 등록된 리스너(listener)를 실행
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 만료된 Key 이벤트 감지
container.addMessageListener(listenerAdapter, new PatternTopic("__keyevent@0__:expired"));
return container;
}
/*
Redis 이벤트가 발생했을 때,
이를 실제로 처리할 리스너(RedisExpirationListener)로 전달
*/
@Bean
public MessageListenerAdapter listenerAdapter(RedisExpirationListener listener) {
return new MessageListenerAdapter(listener, "onMessage");
}
}
RedisMessegeListenerContainer는 redis에서 발생하는 이벤트를 감지하는 역할이다.
해당 코드에서는 " __keyevent@0__:expired", 즉 키가 만료되는 이벤트를 감지하면 listenerAdapter가 실행된다.
그리고 listenerAdapter에서는 RedisExpirationListener의 onMessage 메서드를 실행하도록 한다.
위의 과정을 거쳐 onMessage 메서드에서 수신받은 key값(만료된)을 가져오고 이를 service단에 넘겨 쿠폰 상태를 업데이트한다.
그럼 테스트를 해보자!



현재 coupon_id가 1인 쿠폰이 사용 중임을 알 수 있다.(status = IN_USE)

Redis에도 잘 등록된 것을 확인할 수 있다.

TTL이 만료된 후, 다시 조회하니 Key가 사라진 걸 확인할 수 있다.
그럼 이제 쿠폰 테이블을 확인하면!

여전히 IN_USE 상태이다!!!
'왜 그러지?' 하고 로그를 확인해 보면

만료된 Key를 찾을 수 없다고 한다.
그렇다면 TTL이 만료된 이벤트를 감지해서 실행한 것까지는 맞다는 것이고,
오류에서 수신받은 키에 대한 데이터를 찾을 수 없다 하니 다시 그 부분을 확인해 보자.
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisExpirationListener implements MessageListener {
private final RedisUtil redisUtil;
private final CouponService couponService;
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString(); // 만료된 Key 값 가져오기
log.info("Redis Key 만료 감지: {}", expiredKey);
try{
String[] parts = expiredKey.split("_");
Long couponId = Long.parseLong(parts[2]); // 만료된 key에서 쿠폰 ID 가져오기 (USE_COUPON_쿠폰번호)
log.info("만료된 쿠폰 ID: {}", couponId);
couponService.updateCouponStatusToExpired(couponId);
}catch(Exception e){
log.error("Redis TTL 만료 처리 중 오류 발생 : ", e.getMessage(), e);
}
}
}
사실 현재 위의 코드는 최종 완성본의 코드이므로 위에서는 오류가 발생하는 부분이 없다.
초기 코드에서는 만료된 key로 다시 조회해서 value 값
즉, coupon_id값을 가져오는 것이 내 계획이었다.
key-value로 저장되는 redis이기 때문에, 만료 시 수신받는 메시지도 key-value일 것이고
만료된 key로 조회한다면, value값을 가져올 수 있다고 생각했기 때문이다.
하지만, 공식문서에도 나와있듯이 수신받는 message에는 key값만이 존재한다.
거기다 TTL이 만료되며 key-value도 삭제되기 때문에 아무리 조회해도 value값을 찾을 수 없었다.
그렇다고 value 값만 저장하는 db를 만들 수도 없었다.(또 이렇게 되면 현재 저장되는 key 형식으로는 pk가 중복될 가능성 발생)
빠르게 해결하기 위해 key값에서 coouponId를 가져오기로 결정하고
저장되는 key-value 형식을 바꾸기로 했다.
(2) 2차 구현
String key = "USE_COUPON_" + couponValidationequest.getCouponId();
redisUtil.setObjectExpire(key, couponValidationequest.getUserId(), TIMER); // 2분으로 설정
log.info("[쿠폰 REDIS 저장] key: {}, value: {}, TTL: {}초",
key,
couponValidationequest.getUserId(),
TIMER);
key값에서 couopnId를 가져오기 위해 위와 같이 구조를 바꾸었다.
그리고 테스트를 다시 해보자!



coupon_id가 1인 쿠폰의 status가 IN_USE로 변경된 걸 확인할 수 있다.

변경된 형식으로 사용 중인 쿠폰의 key가 등록된 걸 확인할 수 있다.

TTL 만료되었으니 다시 테이블을 확인하러 가보자

"APPLIED"
만료된 것을 확인할 수 있다
야호!

로그를 확인하니 해당 쿠폰이 만료되었다는 것을 확인할 수 있다.

다른 쿠폰으로 테스트했을 때도, 이벤트가 잘 발생하고 있다는 것을 확인할 수 있다
이렇게 Redis Key Notifications을 이용해서 TTL이 만료된 쿠폰의 상태 변경을 해결할 수 있었다.
다음에는 이번 프로젝트의 마지막 여정인
Scheduler를 이용해 사용하지 않고 유효기간이 지난 쿠폰의 만료 처리를 해결해 보겠다
'Project' 카테고리의 다른 글
| [쿠.파.프] 만료된 쿠폰 개선기 | EP3. Scheduler를 이용해 유효기간이 만료된 쿠폰 상태 업데이트 하기 (1) | 2025.03.08 |
|---|---|
| [쿠.파.프] 만료된 쿠폰 개선기 | EP1. 쿠폰 사용 API 개발 (0) | 2025.03.01 |
| [쿠.파.프] 만료된 쿠폰 개선기 | EP. 0 설계 (0) | 2025.02.24 |