Problem
Problem
타임딜 주문은 짧은 시간에 같은 재고로 요청이 몰리는 고경합 상황이라 정합성, 실패율, 응답 지연을 함께 봐야 했습니다.
Decision
Decision
비관적 락, 낙관적 락, Redis 분산 락을 런타임 설정으로 바꿀 수 있게 만들고, 같은 부하 조건의 결과를 기준으로 전략을 해석했습니다.
Build
Implementation
- 주문 재고 차감 구간을 락 전략별로 분리해 동일 시나리오에서 비교할 수 있게 구성했습니다.
- 반복 상품 조회에는 Caffeine 로컬 캐시를 적용하되 다중 인스턴스 무효화는 한계로 명시했습니다.
- Resilience4j Rate Limiter와 Circuit Breaker, Prometheus/Grafana 메트릭을 붙여 장애와 부하 상황을 관찰할 수 있게 했습니다.
고경합 재고 차감에서는 단순 처리량보다 실패율과 락 대기 비용까지 함께 비교해야 하며, 캐시 개선도 배포 토폴로지의 한계를 같이 말해야 한다는 점을 정리했습니다.
Proof
Verification
docs/perf/PERF_RESULT.md의 k6 결과로 200 VU 주문 스파이크에서 비관적 락 162 RPS, 에러율 0%, 상품 조회 p95 38.8ms에서 11.6ms 개선을 확인했습니다.
200 VU 스파이크 주문에서도 에러율 0%를 기록한 처리량
멀티 상품 시나리오에서 확인한 최대 처리량
38.8ms에서 11.6ms로 낮춘 캐시 기반 개선
Boundaries
Trade-offs & Limitations
Trade-offs
- 비관적 락은 고경합에서 정합성이 안정적이지만 DB 락 대기 시간이 늘 수 있습니다.
- Caffeine 로컬 캐시는 빠르지만 다중 인스턴스에서는 무효화 전략을 별도로 맞춰야 합니다.
Limitations
- 실제 결제·주문 외부 시스템은 포함하지 않은 도메인 검증 프로젝트입니다.
- 장애 복구 측정은 로컬 컨테이너 장애 주입 기준입니다.
아키텍처
전체 아키텍처▼
architecture
전체 아키텍처
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Client
사용자 진입과 실시간 상태 확인
Web / App
Application
API, 도메인 로직, 워커 처리
Controller API / Validation / JWT / Swagger
TDS
Service Business / Transaction / Lock
TDS
Repository JPA / Querydsl
TDS
Data / Messaging
상태 저장, 캐시, 이벤트 전달
Caffeine Cache Item Lookup
TDS
MySQL User / Item / Order / Stock
Storage
Redis JWT Blacklist / Distributed Lock
Storage
Ops
관측성, 배포, 운영 보조
Prometheus
Monitoring
Grafana
Monitoring
핵심 연결 흐름
- 1. Web / App → Controller API / Validation / JWT / Swagger · HTTP/JSON + Bearer JWT
- 2. Controller API / Validation / JWT / Swagger → Service Business / Transaction / Lock · Request
- 3. Service Business / Transaction / Lock → Caffeine Cache Item Lookup · Cache Lookup / Refresh
- 4. Caffeine Cache Item Lookup → Service Business / Transaction / Lock · hit
- 5. Service Business / Transaction / Lock → Repository JPA / Querydsl · Call
- 6. Repository JPA / Querydsl → MySQL User / Item / Order / Stock · CRUD / Pessimistic Lock
- 7. Repository JPA / Querydsl → Redis JWT Blacklist / Distributed Lock · Blacklist / Lock Key
- 8. Controller API / Validation / JWT / Swagger → Prometheus · /actuator/prometheus
- 9. Prometheus → Grafana · Metrics
원본 Mermaid 보기
flowchart LR
Client["Web / App"]
subgraph TDS["Timedeal Service (Spring Boot)"]
Controller["Controller<br/>API / Validation / JWT / Swagger"]
Service["Service<br/>Business / Transaction / Lock"]
Cache[("Caffeine Cache<br/>Item Lookup")]
Repo["Repository<br/>JPA / Querydsl"]
end
subgraph Storage["Data Storage"]
MySQL[("MySQL<br/>User / Item / Order / Stock")]
Redis[("Redis<br/>JWT Blacklist / Distributed Lock")]
end
subgraph Monitoring["Monitoring"]
Prometheus["Prometheus"]
Grafana["Grafana"]
end
Client -->|HTTP/JSON + Bearer JWT| Controller
Controller -->|Request| Service
Service -->|Cache Lookup / Refresh| Cache
Cache -->|hit| Service
Service -->|Call| Repo
Repo -->|CRUD / Pessimistic Lock| MySQL
Repo -->|Blacklist / Lock Key| Redis
Controller -->|/actuator/prometheus| Prometheus
Prometheus -->|Metrics| GrafanaERD
전체 ERD▼
erd
전체 ERD
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
users
- bigint
- id
- PK
- string
- UK
- string
- password
- string
- name
- string
- role
- datetime
- createdAt
+ 1개 필드
items
- bigint
- id
- PK
- string
- name
- decimal
- price
- datetime
- openTime
- datetime
- createdAt
- datetime
- updatedAt
orders
- bigint
- id
- PK
- bigint
- user_id
- FK
- bigint
- item_id
- FK
- string
- status
- int
- quantity
- datetime
- createdAt
+ 1개 필드
stocks
- bigint
- id
- PK
- bigint
- item_id
- FK
- bigint
- version
- int
- quantity
- datetime
- createdAt
- datetime
- updatedAt
관계 요약
- #1 users ||--o{ orders · places
- #2 items ||--o{ orders · ordered
- #3 items ||--|| stocks · tracks
원본 Mermaid 보기
erDiagram
users ||--o{ orders : "places"
items ||--o{ orders : "ordered"
items ||--|| stocks : "tracks"
users {
bigint id PK
string email UK
string password
string name
string role
datetime createdAt
datetime updatedAt
}
items {
bigint id PK
string name
decimal price
datetime openTime
datetime createdAt
datetime updatedAt
}
orders {
bigint id PK
bigint user_id FK
bigint item_id FK
string status
int quantity
datetime createdAt
datetime updatedAt
}
stocks {
bigint id PK
bigint item_id FK
bigint version
int quantity
datetime createdAt
datetime updatedAt
}시퀀스 다이어그램
시퀀스 다이어그램▼
sequence
시퀀스 다이어그램
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → OrderController
POST /api/orders (itemId, quantity)
- 2요청
OrderController → OrderService
createOrder(userId, request)
- 3저장
OrderService → UserService
findById(userId)
- 4처리
UserService → OrderService
User
- 5저장
OrderService → ItemService
findById(itemId)
- 6처리
ItemService → OrderService
Item
- 7저장
OrderService → StockRepository
findByItemIdWithLock(itemId)
- 8저장
StockRepository → MySQL
SELECT ... FOR UPDATE
+ 9개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → OrderController | POST /api/orders (itemId, quantity) | - |
| 2 | OrderController → OrderService | createOrder(userId, request) | - |
| 3 | OrderService → UserService | findById(userId) | - |
| 4 | UserService → OrderService | User | - |
| 5 | OrderService → ItemService | findById(itemId) | - |
| 6 | ItemService → OrderService | Item | - |
| 7 | OrderService → StockRepository | findByItemIdWithLock(itemId) | - |
| 8 | StockRepository → MySQL | SELECT ... FOR UPDATE | - |
| 9 | MySQL → StockRepository | Stock | - |
| 10 | StockRepository → OrderService | Stock | - |
| 11 | OrderService → StockRepository | saveAndFlush(stock) | - |
| 12 | StockRepository → MySQL | UPDATE stocks | - |
| 13 | OrderService → OrderRepository | save(order) | - |
| 14 | OrderRepository → MySQL | INSERT orders | - |
| 15 | OrderRepository → OrderService | Order | - |
| 16 | OrderService → OrderController | OrderResponse | - |
| 17 | OrderController → Client | 201 OrderResponse | - |
원본 Mermaid 보기
sequenceDiagram
actor Client
participant OrderController
participant OrderService
participant UserService
participant ItemService
participant StockRepository
participant OrderRepository
participant MySQL
Client->>OrderController: POST /api/orders (itemId, quantity)
OrderController->>OrderService: createOrder(userId, request)
OrderService->>UserService: findById(userId)
UserService-->>OrderService: User
OrderService->>ItemService: findById(itemId)
ItemService-->>OrderService: Item
OrderService->>StockRepository: findByItemIdWithLock(itemId)
StockRepository->>MySQL: SELECT ... FOR UPDATE
MySQL-->>StockRepository: Stock
StockRepository-->>OrderService: Stock
OrderService->>StockRepository: saveAndFlush(stock)
StockRepository->>MySQL: UPDATE stocks
OrderService->>OrderRepository: save(order)
OrderRepository->>MySQL: INSERT orders
OrderRepository-->>OrderService: Order
OrderService-->>OrderController: OrderResponse
OrderController-->>Client: 201 OrderResponse