버터떡 매장 정보를 조회하고, 주문 기반 랭킹과 선착순 쿠폰 발급을 실험한 개인 프로젝트입니다.
단순 CRUD보다 Redis 캐시, 로컬 캐시, Lua Script, SSE, k6 부하 테스트를 직접 적용해 읽기 성능과 동시성 처리 흐름을 검증하는 데 초점을 두었습니다.
- 매장 목록 조회에서 DB 접근을 줄이기 위한 다층 캐시 구조 설계
- 선착순 쿠폰 발급에서 중복 참여와 초과 발급을 Redis Lua Script로 제어
- 주문 발생 시 Redis Sorted Set 기반 실시간 매장 랭킹 갱신
- SSE(Server-Sent Events)를 이용한 랭킹 변경 실시간 전달
- k6를 이용한 고부하 요청 시나리오 작성
| 영역 | 기술 |
|---|---|
| Backend | Java 17, Spring Boot 3.5, Spring Web, Spring Data JPA |
| Database | MySQL |
| Cache / Queue | Redis, Spring Cache, Caffeine |
| Realtime | SSE, Redis Pub/Sub |
| Monitoring | Spring Boot Actuator, Micrometer, Prometheus |
| Test | JUnit 5, k6 |
| Frontend | Vite, Axios |
매장 목록 조회는 DB를 바로 조회하지 않고 다음 순서로 처리합니다.
Local Cache(Caffeine)
-> Redis List + Redis Hash
-> MySQL
storeList로컬 캐시에 매장 목록을 저장해 짧은 시간 동안 반복 조회 비용을 줄였습니다.- Redis에는
store:ids목록과store:{id}Hash를 분리 저장했습니다. - Redis Hash는 pipeline으로 한 번에 조회해 N번 round-trip을 줄였습니다.
- 캐시 일부가 누락되면 DB에서 전체 데이터를 다시 읽어 Redis와 local cache를 재동기화합니다.
쿠폰 발급은 Redis Lua Script로 처리합니다.
쿠폰 요청
-> Lua Script로 중복 참여 / 수량 초과 검사
-> 성공 사용자를 Redis Queue에 적재
-> Redis Pub/Sub 알림
-> CouponWorker가 Queue를 소비해 DB 저장
- 중복 참여 방지
- 최대 발급 수량 제한
- 쿠폰 발급 요청과 DB 저장 분리
- 순간 트래픽에서 DB write 부하 완화
주문이 생성되면 Redis Sorted Set의 점수를 증가시켜 매장 랭킹을 갱신합니다.
POST /butter/orders
-> 주문 저장
-> store:ranking:daily score 증가
-> ranking:change:events publish
-> SSE로 랭킹 브로드캐스트
랭킹 조회는 Redis ZSet에서 상위 N개 매장을 읽고, 매장명은 DB에서 보강합니다.
클라이언트는 /butter/stream으로 SSE 연결을 맺고, 랭킹 변경 이벤트를 받습니다.
- 연결 직후 현재 랭킹을 즉시 전송
- Redis Pub/Sub 이벤트 발생 시 전체 SSE client에 브로드캐스트
- 연결 종료, timeout, error 발생 시 emitter 정리
| Method | Path | 설명 |
|---|---|---|
| GET | /butter/stores |
캐시 기반 매장 목록 조회 |
| GET | /butter/storesdb |
DB 직접 매장 목록 조회 |
| GET | /butter/store/{id} |
매장 단건 조회 |
| POST | /butter/stores/suggest |
매장 제안 등록 |
| POST | /butter/coupons/butter-first-come/claim |
선착순 쿠폰 발급 요청 |
| GET | /butter/stream |
실시간 랭킹 SSE 구독 |
| POST | /butter/orders |
주문 생성 및 랭킹 갱신 |
| GET | /butter/local |
로컬 캐시 조회 테스트 |
| POST | /butter/local |
로컬 캐시 저장 테스트 |
butter_map
├── butter # Spring Boot backend
│ ├── src/main/java
│ │ └── com/example/butter/domain
│ │ ├── config # Redis, Cache, Web 설정
│ │ ├── controller # REST / SSE API
│ │ ├── dto
│ │ ├── entity
│ │ ├── repository
│ │ └── service # 캐시, 쿠폰, 주문, 랭킹 로직
│ └── src/main/resources
│ └── coupon.lua # 선착순 쿠폰 Lua Script
├── butter_front # Vite frontend
└── k6 # 부하 테스트 스크립트
cd butter
./gradlew bootRuncd butter_front
npm install
npm run devk6 run k6/test.js현재 k6 스크립트는 /butter/storesdb에 순간 요청을 몰아 DB 직접 조회 경로를 확인하는 용도입니다.
ButterMap은 작은 도메인 안에서 캐시와 이벤트 기반 처리의 차이를 직접 비교하기 위한 프로젝트입니다.
특히 매장 목록 조회, 선착순 쿠폰, 실시간 랭킹처럼 트래픽이 몰리기 쉬운 기능을 Redis 중심으로 분리해 보면서 읽기 성능과 동시성 제어를 실험했습니다.