프로젝트 목록으로 돌아가기
Recruiter Brieftimedeal-service

타임딜 서비스

200 VU 주문 스파이크에서 락 전략 3가지를 비교해 재고 정합성을 검증했고, 비관적 락으로 162 RPS와 에러율 0%를 확인했습니다.

Role
백엔드 설계 및 구현
Period
2026
Team
개인 프로젝트
Theme
Performance
Java 21Spring Boot 3Spring Data JPAQuerydslMySQLRedisJWTCaffeine

30초 요약

문제
한정 수량 타임딜에서 주문 스파이크가 재고 정합성과 응답 지연을 동시에 흔들 수 있었습니다.
해결
비관적·낙관적·Redis 락 전략을 같은 주문 시나리오에서 비교하고 캐시/관측성을 구성했습니다.
검증
200 VU 주문 스파이크와 상품 조회 캐시 결과를 docs/perf/PERF_RESULT.md에 기록했습니다.

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 개선을 확인했습니다.

162 RPS
Measured
비관적 락

200 VU 스파이크 주문에서도 에러율 0%를 기록한 처리량

247 RPS
Measured
분산 부하

멀티 상품 시나리오에서 확인한 최대 처리량

38.8ms → 11.6ms
Measured
상품 조회 p95

38.8ms에서 11.6ms로 낮춘 캐시 기반 개선

Measured

162 RPS

비관적 락

Scenario
200 VU 주문 스파이크
Method
비관적·낙관적·Redis 분산 락을 동일 부하로 비교
Result
비관적 락에서 p95 935ms, 에러율 0%를 확인했습니다.
Measured

38.8ms → 11.6ms

상품 조회

Scenario
반복 상품 목록 조회
Method
Caffeine 캐시 도입 전후 p95 비교
Result
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. 1. Web / App → Controller API / Validation / JWT / Swagger · HTTP/JSON + Bearer JWT
  2. 2. Controller API / Validation / JWT / Swagger → Service Business / Transaction / Lock · Request
  3. 3. Service Business / Transaction / Lock → Caffeine Cache Item Lookup · Cache Lookup / Refresh
  4. 4. Caffeine Cache Item Lookup → Service Business / Transaction / Lock · hit
  5. 5. Service Business / Transaction / Lock → Repository JPA / Querydsl · Call
  6. 6. Repository JPA / Querydsl → MySQL User / Item / Order / Stock · CRUD / Pessimistic Lock
  7. 7. Repository JPA / Querydsl → Redis JWT Blacklist / Distributed Lock · Blacklist / Lock Key
  8. 8. Controller API / Validation / JWT / Swagger → Prometheus · /actuator/prometheus
  9. 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| Grafana

ERD

전체 ERD

erd

전체 ERD

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

users
bigint
id
PK
string
email
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. #1 users ||--o{ orders · places
  2. #2 items ||--o{ orders · ordered
  3. #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
ClientOrderControllerOrderServiceUserServiceItemServiceStockRepositoryOrderRepositoryMySQL
  1. 1요청

    Client → OrderController

    POST /api/orders (itemId, quantity)

  2. 2요청

    OrderController → OrderService

    createOrder(userId, request)

  3. 3저장

    OrderService → UserService

    findById(userId)

  4. 4처리

    UserService → OrderService

    User

  5. 5저장

    OrderService → ItemService

    findById(itemId)

  6. 6처리

    ItemService → OrderService

    Item

  7. 7저장

    OrderService → StockRepository

    findByItemIdWithLock(itemId)

  8. 8저장

    StockRepository → MySQL

    SELECT ... FOR UPDATE

+ 9개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → OrderControllerPOST /api/orders (itemId, quantity)-
2OrderController → OrderServicecreateOrder(userId, request)-
3OrderService → UserServicefindById(userId)-
4UserService → OrderServiceUser-
5OrderService → ItemServicefindById(itemId)-
6ItemService → OrderServiceItem-
7OrderService → StockRepositoryfindByItemIdWithLock(itemId)-
8StockRepository → MySQLSELECT ... FOR UPDATE-
9MySQL → StockRepositoryStock-
10StockRepository → OrderServiceStock-
11OrderService → StockRepositorysaveAndFlush(stock)-
12StockRepository → MySQLUPDATE stocks-
13OrderService → OrderRepositorysave(order)-
14OrderRepository → MySQLINSERT orders-
15OrderRepository → OrderServiceOrder-
16OrderService → OrderControllerOrderResponse-
17OrderController → Client201 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