한정 수량 굿즈의 안정적인 선착순 구매와 결제를 위한 타임세일 서비스
한정 판매 서비스에서는 판매 시작과 동시에 다수의 구매 요청이 집중될 수 있다.
이 과정에서 다음과 같은 문제를 고려했다.
- 여러 사용자가 동시에 같은 재고를 변경하면서 발생할 수 있는 초과 판매
- 연속 클릭이나 요청 재전송으로 인한 중복 구매
- Redis 재고 차감 이후 DB 저장 실패로 인한 상태 불일치
- 외부 결제 시스템과 내부 구매·결제 상태 간 정합성 문제
- 결제 승인, 구매 취소, Timeout이 동시에 발생하는 상황의 동시성 문제
이를 해결하기 위해 Redis 기반 재고 처리와 Toss Payments 결제를 중심으로 구매 흐름을 구성하고, 동시성 및 부하 테스트를 통해 주요 정책을 검증했다.
2026.08.07 ~ 2026.08.25
| 팀원 | 역할 |
|---|---|
| 신창석 | 팀장 / 인증·인가 / 공통 예외 처리 / Swagger / k6 테스트 |
| 박예은 | 구매 / 결제 / Frontend / 결제 동시성 테스트 / 발표 자료 |
| 정진협 | 상품 / 판매 / 재고 / 재고 동시성 테스트 / 발표 |
- Kakao OAuth2 로그인
- JWT 기반 인증·인가
- 판매 중인 굿즈 조회
- 선착순 상품 구매
- Toss Payments 결제
- 구매 내역 조회
- 구매 및 결제 취소
- 미결제 구매 Timeout 처리
- 상품 등록 및 관리
- 판매 등록 및 관리
- 판매 기간 및 초기 재고 설정
- 1인 최대 구매 수량 설정
- 판매 상태 관리
- Redis 기반 실시간 재고 관리
- Lua Script를 이용한 재고 차감 및 구매 제한 원자 처리
- 판매 시작 전 Redis Warm-up
- 판매 종료 후 Redis 재고를 RDB에 동기화
- 구매·결제 상태 변경 시 비관적 락 적용
- Java 25
- Spring Boot 4.1.0
- Spring MVC
- Spring Data JPA
- Spring Security
- OAuth2 Client
- Thymeleaf
- PostgreSQL
- Redis
- Redis Lua Script
- Kakao OAuth2
- JWT
- Toss Payments
- JUnit
- k6
- Docker
- Docker Compose
- Swagger / Springdoc OpenAPI
일반 사용자와 관리자는 Spring Boot 애플리케이션을 통해 서비스에 접근한다.
상품·판매·구매·결제 데이터는 PostgreSQL에 저장하고, 판매 중 실시간 재고와 사용자별 구매 수량은 Redis에서 관리한다.
외부 서비스로는 인증에 Kakao OAuth2, 결제에 Toss Payments를 연동했다.
판매 시작 시 다수의 요청이 하나의 재고에 집중되는 상황을 고려해 Redis를 실시간 재고 처리에 사용했다.
구매 요청 시 Lua Script 내부에서 다음 조건을 확인한다.
- 판매 정보 존재 여부
- 판매 상태
- 판매 기간
- 사용자별 최대 구매 수량
- 현재 재고
모든 조건을 만족한 경우 재고 차감과 사용자별 구매 수량 증가를 하나의 Lua Script에서 원자적으로 처리한다.
조건을 만족하지 못한 요청은 DB에 접근하기 전에 Redis 단계에서 실패 처리한다.
Redis에서 재고를 먼저 차감한 뒤 구매 및 결제 정보를 RDB에 저장한다.
Redis 차감 이후 DB 트랜잭션이 실패하면 재고만 감소한 상태가 남을 수 있으므로,
DB 트랜잭션 결과에 따라 Redis 재고와 사용자별 구매 수량을 복구하는 보상 처리를 적용했다.
외부 결제 시스템과 내부 DB는 하나의 트랜잭션으로 처리할 수 없기 때문에 결제 결과에 따라 상태를 보정하도록 구성했다.
- 주문번호 기반 멱등성 키를 통한 중복 승인 방지
- 결제 결과가 불확실한 경우 Toss 결제 상태 재조회
- Toss 승인 이후 내부 처리 실패 시 결제 보상 취소
- Toss Webhook을 통한 최종 결제 상태 보정
동일한 구매에 대해 결제 승인, 사용자 취소, Timeout 처리가 동시에 실행될 수 있다.
이를 방지하기 위해 구매와 결제 처리 과정에 비관적 락을 적용하고,
락 획득 이후 현재 상태를 다시 검증한 뒤 실제 처리 가능한 요청만 상태를 변경하도록 구성했다.
k6를 활용해 동시 요청 상황에서 재고·구매·결제의 정합성을 검증하고, 부하 테스트를 통해 병목 지점을 확인했다.
| 시나리오 | 실행 조건 | 결과 |
|---|---|---|
| 재고 초과 구매 | 재고 100 / 1,000 VU | 성공 100건 / 정상 거절 900건 / 초과 판매 0건 |
| 단일 판매 집중 | 재고 1,000 / 1,000 VU | 1,000건 성공 / 최종 재고 0 |
| 분산 판매 | 판매 100개 / 1,000 VU | 모든 판매 재고 0 |
| 중복 구매 | 동일 사용자 / 50 VU | 성공 1건 / 정상 거절 49건 |
| 구매 취소 | 동일 구매 / 50 VU | 취소 1회 / 재고 1회 복구 |
| 결제 승인 | 동일 결제 / 50 VU | 승인 1회 / 최종 상태 일치 |
| 승인·취소 경쟁 | 승인 1 VU + 취소 1 VU | DB·Redis 최종 상태 일치 |
개선 전·후 각각 3회씩 총 42개 시나리오를 실행했으며, 모든 업무 검증을 통과했다.
- 예상 밖 업무 응답: 0건
- Connection Refused: 0건
status=0/ Timeout: 0건- 초과 판매·중복 처리·상태 불일치: 0건
부하 테스트 과정에서 판매 목록 조회 시 Sale.goods의 LAZY 로딩으로 인해
판매 106개 조회에 총 107회의 SQL이 실행되는 N+1 문제를 확인했다.
@EntityGraph(attributePaths = "goods")를 적용해 Sale과 Goods를 함께 조회하도록 개선했다.
| 지표 | 개선 전 | 개선 후 | 변화 |
|---|---|---|---|
| 판매 목록 SQL | 107회 | 1회 | N+1 제거 |
| Baseline 목록 p95 | 64.45ms | 34.34ms | 46.7% 감소 |
| Stress 목록 p95 | 3.49초 | 2.77초 | 20.8% 감소 |
| Stress Dropped | 4,413 | 1,418 | 67.9% 감소 |
| App 평균 CPU | 143.00% | 90.67% | 36.6% 감소 |
| DB 평균 CPU | 70.63% | 8.20% | 88.4% 감소 |
- Stress 목록 p95가 목표 2초를 초과한 2.77초
- Spike 목록 p95가 목표 3초를 초과한 3.37초
- Stress·Spike 구간에서
dropped_iterations가 남아 있어 500 RPS 안정 처리는 확인하지 못함 - 판매 목록 전체 조회와 판매별 Redis 재고 개별 조회가 이후 병목 후보로 남음
3차 프로젝트에서는 운영 환경과 성능 테스트 환경을 개선하고 안정 처리량을 추가로 검증할 예정이다.
- Redis와 Lua Script를 활용해 재고 차감과 사용자별 구매 제한을 원자적으로 처리
- Redis 재고 선점 이후 DB 실패 상황에 대한 보상 처리 구현
- Toss Payments 승인·취소 및 Webhook 기반 상태 보정 구현
- 결제·취소·Timeout 경합에 대한 동시성 제어
- 판매 목록 N+1 문제를 발견하고 EntityGraph를 적용해 SQL 호출을 107회에서 1회로 줄였으며, 응답 시간과 CPU 사용량 개선을 확인
- k6를 활용한 동시성 및 부하 테스트 환경 구축
2차 프로젝트에서는 주요 실패 상황에 대한 보상 처리를 구현했지만, Redis·RDB·외부 결제 시스템을 하나의 트랜잭션으로 묶을 수 없는 한계가 남아 있다.
3차 프로젝트에서는 2차에서 구축한 구매·결제 구조를 기반으로 장애 상황에서도 이벤트를 유실하지 않고
탐지·재처리·복구할 수 있는 운영 구조로 확장할 예정이다.
- Message Queue 도입
- Transactional Outbox Pattern 적용
- Inbox Pattern을 통한 중복 이벤트 처리 방지
- Redis·DB 간 상태 불일치 재처리 및 복구
- Prometheus / Grafana 기반 모니터링
- 주요 장애 및 스케줄러 결과 Slack 알림
- 실제 서비스 배포 및 운영 환경 확장
main: 최종 배포 및 완료 버전dev: 개발 통합 브랜치feat/*: 기능 개발fix/*: 버그 수정 및 기능 개선test/*: 테스트 및 성능 검증
