2023.09 ~ 2024.01 / Backend
Givemeticon
만료가 임박한 기프티콘을 사고파는 중고거래 플랫폼
쿠폰의 재고 정합성과 인기순 조회의 성능을 개선했습니다.
01 / CONSISTENCY · Redisson, MySQL
재고와 중복 발급 규칙을 DB까지 보호
문제동시 요청에서 재고를 초과하거나 같은 사용자에게 쿠폰을 중복 발급할 수 있었습니다.
초과 발급 / 같은 사용자의 중복 발급 가능
변경 행 수 확인 · 실패 시 예외로 롤백
판단 근거
락이 풀려도 재고와 중복 발급 규칙은
DB에서 지켜져야 했습니다.
- 경합 제어: stockId 기준 Redisson 분산락으로 같은 재고의 동시 진입을 제어했습니다.
- 재고 보호: 조회 후 차감 사이의 경쟁을 없애기 위해
remain > 0인 경우에만 DB가 원자적으로 차감하게 했습니다. - 중복 보호:
(user_id, stock_id)UNIQUE와 변경 행 수 확인으로 발급 성공을 판단했습니다.
Redis만으로 충분하지 않다고 본 이유
lease 만료·Redis 장애 등으로 락의 보호가 끊기는 경우에도 DB에는 잘못된 발급 결과가 남아서는 안 됩니다. Redis는 요청 경합을 제어하고, DB의 조건과 제약은 저장할 수 있는 데이터의 범위를 제한하도록 역할을 나눴습니다.
INSERT IGNORE가 중복을 무시했더라도 성공으로 처리하지 않습니다. 반환된 행 수가 1이 아니면 예외를 발생시켜 재고 차감도 함께 롤백되도록 구성했습니다.
핵심 코드
SQL / Java · 핵심 발췌UPDATE coupon_stock
SET remain = remain - 1
WHERE id = #{id} AND remain > 0;int updatedRows = couponStockMapper.decreaseStockIfEnough(stockId);
if (updatedRows != 1) {
throw new NotEnoughCouponStockException();
}
// UNIQUE(user_id, stock_id)를 가진 테이블에 INSERT IGNORE
int insertedRows = couponMapper.saveIfNotIssued(coupon);
if (insertedRows != 1) {
throw new AlreadyIssuedCouponException();
}재고·쿠폰 서비스의 검사 부분을 모았습니다. 락 획득 후 AopForTransaction이 발급 흐름을 REQUIRES_NEW로 감싸며, 예외 시 함께 롤백하는 구조입니다.
검증 결과
로컬 애플리케이션 2대 환경에서 동시 요청 후 MySQL의 최종 발급 수·잔여 재고·중복 건수를 조회한 결과입니다.
02 / READ PERFORMANCE · MySQL, 사전 집계
인기순 조회에서 반복 집계 비용 제거
문제좋아요 100만 건을 실시간 집계하면서 인기순 Top 20 조회가 4.6초까지 느려졌습니다.
데이터 증가 → 반복 집계 비용 증가
읽기 경로에서 실시간 집계 제거
판단 근거
20개를 보여주기 위해 매번 전체를
집계하는 비용에 주목했습니다.
- 시행착오: 좋아요 테이블에 인덱스를 추가했지만,
GROUP BY + COUNT + ORDER BY의 반복 계산은 남았습니다. - 원인 판단:
LIMIT 20이어도 상위 상품을 찾기 위한 집계·정렬 비용이 먼저 발생하는 구조였습니다. - 구조 전환: 쓰기 시점에 메타 테이블의 집계값을 갱신하고, 인기순 커버링 인덱스로 집계값 조회의 I/O를 줄였습니다.
사전 집계와 커버링 인덱스의 역할
사전 집계는 조회 시 좋아요 행을 다시 세는 작업을 제거합니다. 커버링 인덱스는 필요한 정렬 키와 집계값을 인덱스에서 읽게 해 추가 테이블 접근을 줄입니다.
상품 정보는 별도 JOIN으로 조회합니다. 아래 전체 쿼리가 모두 커버링된다는 의미는 아니며, 실제 접근 경로와 정렬 여부는 EXPLAIN으로 확인해야 합니다.
핵심 쿼리
MyBatis · ItemMapper 발췌SELECT i.id,
i.brand_id,
i.name,
i.price,
m.like_count,
m.view_count
FROM item_favorite_meta m
JOIN item i
ON i.id = m.item_id
ORDER BY m.like_count DESC, m.item_id DESC
LIMIT #{limit};좋아요를 실시간 COUNT하지 않고 저장된 like_count로 정렬합니다. 같은 좋아요 수는 item_id로 순서를 결정합니다.
검증 결과 · Top 20 조회 시간
상품 10만 개 · 좋아요 100만 건
실시간 집계 → 메타 테이블 기반 조회
기존 측정 결과 기준. 실행 환경·반복 횟수·캐시 상태의 상세 기록은 별도 보완이 필요합니다.