Tiny Star

프로젝트/쿠폰

[쿠폰] 쿠폰 조회 (Redis 예외처리)

흰둥아 2025. 4. 28. 22:18

couponId에 따른 재고 조회하는 API (자세한 내용 Git Issue 참고)

 


 

 

1. 요구사항

1. 목적

  • Redis에 coupon:{couponId}:stock 형식으로 재고를 등록한다.

 

2. 작업 내용

  • POST /api/admin/coupons
  • 요청 파라미터:
    1. couponId (Long, 필수) : 쿠폰 ID
    2. quantity (int, 필수) : 초기 재고 수량
    3. 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는 들어가지 않게 됐다.

 

 

 

 

 

top