couponId에 따른 재고 조회하는 API (자세한 내용 Git Issue 참고)
1. 요구사항
1. 목적
- Redis에 coupon:{couponId}:stock 형식으로 재고를 등록한다.
2. 작업 내용
- POST /api/admin/coupons
- 요청 파라미터:
- couponId (Long, 필수) : 쿠폰 ID
- quantity (int, 필수) : 초기 재고 수량
- ttlSeconds (int, 선택) : 재고 만료시간(초 단위), 미지정시 무제한 유지
3. 요청/응답 예시
요청
POST /api/admin/coupon
{
"couponId": 123,
"quantity": 100,
"ttlSeconds": 86400
}
응답
{
"couponId":13,
"stock":100
}
2. 작업 내용
// 초기 구상
public Integer getCouponStock(Long couponId) {
if (couponId == null) {
throw new CustomException(ErrorCode.INVALID_REQUEST);
}
String key = COUPON_STOCK_PREFIX + couponId + ":stock";
Object stockObj = redisTemplate.opsForValue().get(key);
if (stockObj == null) {
throw new CustomException(ErrorCode.COUPON_NOT_FOUND);
}
try {
return (Integer) stockObj;
} catch (ClassCastException e) {
log.error("couponId: {} ERROR!", couponId, e);
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
해당 코드에는 아래 세 가지 에외처리가 들어갔다.
- couponId null 예외처리
- Redis CouponId 예외처리
- Integer 변환 예외처리
바라보다가, 레디스 서버가 재실행돼서 저장되어있지 않을 경우를 대비해서 DB 를 조회해야겠다고 생각했다.
if (stockObj == null) {
// Redis에 없으면 DB 조회
Coupons coupon = couponsRepository.findById(couponId).orElse(null);
redisTemplate.opsForValue().set(key, coupon.getTotalQuantity(), Duration.ofHours(6));
return coupon.getTotalQuantity();
}
DB 조회해서 Key 를 넣는 로직을 추가했다.
여기서 다시 잘못된 CouponId가 발급돼서 수천명의 사용자가 요청하게되면 그만큼 디비의 부하가 있을 것 같았다.
if (stockObj == null) {
// Redis에 없으면 DB 조회
Coupons coupon = couponsRepository.findById(couponId).orElse(null);
if (coupon == null) { // 존재하지 않는 쿠폰은 NULL 로 저장
redisTemplate.opsForValue().set(key, VALUE_NULL, Duration.ofMinutes(1));
throw new CustomException(ErrorCode.COUPON_NOT_FOUND);
}
// DB에 있으면 Redis 적재
redisTemplate.opsForValue().set(key, coupon.getTotalQuantity(), Duration.ofHours(6));
return coupon.getTotalQuantity();
}
if (VALUE_NULL.equals(stockObj)) {
log.error("couponId {} is null", couponId);
throw new CustomException(ErrorCode.COUPON_NOT_FOUND);
}
그래서 쿠폰이 없을 경우에도 NULL로 값을 넣어주고 Redis 에서 처리될 수 있도록 했다.
하단에서 예외처리까지 추가하여 잘못된 접근임을 명시하였다.
public Integer getCouponStock(Long couponId) {
if (couponId == null) {
log.error("couponId is null");
throw new CustomException(ErrorCode.INVALID_REQUEST);
}
String key = COUPON_STOCK_PREFIX + couponId + ":stock";
Object stockObj = redisTemplate.opsForValue().get(key);
if (stockObj == null) {
// Redis에 없으면 DB 조회
Coupons coupon = couponsRepository.findById(couponId).orElse(null);
if (coupon == null) { // 존재하지 않는 쿠폰은 NULL 로 저장
redisTemplate.opsForValue().set(key, VALUE_NULL, Duration.ofMinutes(1));
throw new CustomException(ErrorCode.COUPON_NOT_FOUND);
}
// DB에 있으면 Redis 적재
redisTemplate.opsForValue().set(key, coupon.getTotalQuantity(), Duration.ofHours(6));
return coupon.getTotalQuantity();
}
if (VALUE_NULL.equals(stockObj)) {
log.error("couponId {} is null", couponId);
throw new CustomException(ErrorCode.COUPON_NOT_FOUND);
}
try {
return (Integer) stockObj;
} catch (ClassCastException e) {
log.error("couponId: {} ERROR!", couponId, e);
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
최종코드
쿠폰이 만료되거나 존재하지않을 경우 디비를 조회하고 그 이후 1분동안은 Redis 에서 조회함으로써 DB는 들어가지 않게 됐다.
'프로젝트 > 쿠폰' 카테고리의 다른 글
[쿠폰] 테스트를 위한 k6 + InfluxDB + Grafana 구성 (1) | 2025.04.30 |
---|---|
[쿠폰] 쿠폰 발급 수정 (saveAll, @Transactional) (0) | 2025.04.29 |
[쿠폰] 쿠폰 발급 (Redis Stream, CustomDbWorker) (0) | 2025.04.29 |
[쿠폰] 쿠폰 생성 (@Transactional, CustomException) (0) | 2025.04.28 |
[쿠폰] Redis 선착순 쿠폰 시스템 (1) | 2025.04.28 |