Problem
Problem
11인 팀 프로젝트 안에서 맡은 핵심 범위는 상품 목록 조회, 예약 재고 정합성, 알림 API처럼 서비스 사용 흐름에 직접 닿는 백엔드 기능이었습니다.
Decision
Decision
해커톤 산출물을 그대로 두지 않고, 상품 목록 N+1 위험과 예약 Race Condition을 테스트와 k6 시나리오로 재현 가능한 형태로 보강했습니다.
Build
Implementation
- 상품 목록은 user와 hashtags 접근을 통합 테스트로 확인하고, fetch join 기반 조회 경로를 검증했습니다.
- 예약은 Product 행에 PESSIMISTIC_WRITE를 적용하고, OSIV 환경의 L1 캐시 우회를 위해 detach 후 FOR UPDATE 재조회 흐름을 사용했습니다.
- 민감정보 직렬화 위험은 @JsonIgnore, 환경변수 분리, 이력 정리 작업으로 관리했습니다.
팀 프로젝트 경험은 기능 구현보다 맡은 범위를 근거와 재현 경로로 설명할 때 신뢰도가 높아진다는 점을 보여주는 사례로 정리했습니다.
Proof
Verification
ProductQueryTest와 상품 목록 k6 스크립트, ReservationConcurrencyTest와 동시 예약 k6 시나리오로 상품 목록 접근과 재고 50개/100명 예약 경로를 확인했습니다.
user/hashtags 접근을 통합 테스트로 확인
상품 목록 k6 p95 <500ms 목표 기준
100 VU, 재고 50개 테스트에서 성공 50건, 실패 50건
JOIN FETCH
상품 목록
- Scenario
- 상품 목록에서 사용자와 해시태그를 함께 조회
- Method
- JOIN FETCH 적용 후 통합 테스트와 k6 상품 목록 시나리오로 재현
- Result
- 상품 user/hashtags 접근은 통합 테스트 범위에 포함했습니다. 상품 목록 p95 <500ms는 k6 스크립트의 목표 기준으로만 명시했습니다.
50/50
예약 정합성
- Scenario
- 재고 50개 상품에 100 VU 동시 예약
- Method
- PESSIMISTIC_WRITE와 detach 후 FOR UPDATE 재조회 적용
- Result
- 성공 50건, 실패 50건, 최종 재고 0을 확인했습니다.
Boundaries
Trade-offs & Limitations
Trade-offs
- Pessimistic Lock은 예약 정합성을 단순하게 지키지만 인기 상품에서는 대기 시간이 늘어날 수 있습니다.
- JOIN FETCH는 목록 조회 쿼리를 줄이지만 페이지네이션과 컬렉션 fetch 조합은 조심해서 제한해야 합니다.
Limitations
- 해커톤 프로젝트를 사후 보강한 사례라 장기 트래픽 데이터는 없습니다.
- 예약 결제까지 이어지는 실제 정산 흐름은 포함하지 않았습니다.
아키텍처
전체 아키텍처▼
architecture
전체 아키텍처
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Client
사용자 진입과 실시간 상태 확인
React Client
Application
API, 도메인 로직, 워커 처리
JwtAuthenticationFilter
Security
Controller
App
Service
App
Repository
App
Data / Messaging
상태 저장, 캐시, 이벤트 전달
MySQL
AWS S3
핵심 연결 흐름
- 1. React Client → JwtAuthenticationFilter
- 2. JwtAuthenticationFilter → Controller
- 3. Controller → Service
- 4. Service → Repository
- 5. Repository → MySQL
- 6. Service → AWS S3
원본 Mermaid 보기
flowchart LR
Client[React Client]
subgraph Security[Security]
JWT[JwtAuthenticationFilter]
end
subgraph App[Spring Boot Application]
C[Controller]
S[Service]
R[Repository]
end
MySQL[(MySQL)]
S3[AWS S3]
Client --> JWT --> C --> S --> R --> MySQL
S --> S3ERD
전체 ERD▼
erd
전체 ERD
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
USER
- bigint
- id
- PK
- varchar
- username
- UK
- varchar
- UK
- varchar
- password_hash
- boolean
- email_verified
- datetime
- created_at
PRODUCT
- bigint
- id
- PK
- bigint
- user_id
- FK
- varchar
- title
- varchar
- image_url
- int
- total_quantity
- int
- available_quantity
+ 2개 필드
RESERVATION
- bigint
- id
- PK
- bigint
- product_id
- FK
- bigint
- user_id
- FK
- int
- quantity
- varchar
- status
- datetime
- rental_start_date
+ 1개 필드
COMMENT
- bigint
- id
- PK
- bigint
- product_id
- FK
- bigint
- user_id
- FK
- text
- content
- int
- like_count
- datetime
- created_at
REPLY
- bigint
- id
- PK
- bigint
- comment_id
- FK
- bigint
- user_id
- FK
- varchar
- content
- datetime
- created_at
HASHTAG
- bigint
- id
- PK
- varchar
- name
- UK
NOTIFICATION
- bigint
- id
- PK
- bigint
- user_id
- FK
- bigint
- product_id
- FK
- bigint
- comment_id
- FK
- bigint
- reply_id
- FK
- varchar
- type
+ 2개 필드
관계 요약
- #1 USER ||--o{ PRODUCT · 소유
- #2 USER ||--o{ RESERVATION · 신청
- #3 USER ||--o{ COMMENT · 작성
- #4 USER ||--o{ LIKES · 좋아요
- #5 USER ||--o{ FOLLOW · 팔로우
- #6 USER ||--o{ NOTIFICATION · 수신
- #7 USER ||--o{ REPLY · 작성
- #8 PRODUCT ||--o{ RESERVATION · 예약
- #9 PRODUCT ||--o{ COMMENT · 댓글
- #10 PRODUCT ||--o{ LIKES · 좋아요
- #11 PRODUCT ||--o{ NOTIFICATION · 알림
- #12 PRODUCT ||--o{ PRODUCT_HASHTAGS · 태깅
- #13 HASHTAG ||--o{ PRODUCT_HASHTAGS · 태깅
- #14 COMMENT ||--o{ REPLY · 답글
- #15 COMMENT ||--o{ NOTIFICATION · 알림
- #16 REPLY ||--o{ NOTIFICATION · 알림
원본 Mermaid 보기
erDiagram
USER ||--o{ PRODUCT : "소유"
USER ||--o{ RESERVATION : "신청"
USER ||--o{ COMMENT : "작성"
USER ||--o{ LIKES : "좋아요"
USER ||--o{ FOLLOW : "팔로우"
USER ||--o{ NOTIFICATION : "수신"
USER ||--o{ REPLY : "작성"
PRODUCT ||--o{ RESERVATION : "예약"
PRODUCT ||--o{ COMMENT : "댓글"
PRODUCT ||--o{ LIKES : "좋아요"
PRODUCT ||--o{ NOTIFICATION : "알림"
PRODUCT ||--o{ PRODUCT_HASHTAGS : "태깅"
HASHTAG ||--o{ PRODUCT_HASHTAGS : "태깅"
COMMENT ||--o{ REPLY : "답글"
COMMENT ||--o{ NOTIFICATION : "알림"
REPLY ||--o{ NOTIFICATION : "알림"
USER {
bigint id PK
varchar username UK
varchar email UK
varchar password_hash
boolean email_verified
datetime created_at
}
PRODUCT {
bigint id PK
bigint user_id FK
varchar title
varchar image_url
int total_quantity
int available_quantity
varchar reservation_status
datetime created_at
}
RESERVATION {
bigint id PK
bigint product_id FK
bigint user_id FK
int quantity
varchar status
datetime rental_start_date
datetime rental_end_date
}
COMMENT {
bigint id PK
bigint product_id FK
bigint user_id FK
text content
int like_count
datetime created_at
}
REPLY {
bigint id PK
bigint comment_id FK
bigint user_id FK
varchar content
datetime created_at
}
HASHTAG {
bigint id PK
varchar name UK
}
NOTIFICATION {
bigint id PK
bigint user_id FK
bigint product_id FK
bigint comment_id FK
bigint reply_id FK
varchar type
boolean is_read
datetime created_at
}시퀀스 다이어그램
JWT 인증 흐름▼
sequence
JWT 인증 흐름
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → JwtAuthenticationFilter
HTTP Request (Bearer token)
- 2검증
JwtAuthenticationFilter → JwtTokenProvider
validateToken(token)
- 3alt 유효한 토큰
control
alt 유효한 토큰
조건: alt 유효한 토큰
- 4alt 유효한 토큰
JwtTokenProvider → JwtAuthenticationFilter
true
조건: alt 유효한 토큰
- 5검증
JwtAuthenticationFilter → SecurityContext
setAuthentication(auth)
조건: alt 유효한 토큰
- 6alt 유효한 토큰
JwtAuthenticationFilter → Controller
요청 전달
조건: alt 유효한 토큰
- 7alt 유효한 토큰
Controller → Client
200 OK
조건: alt 유효한 토큰
- 8else 만료 또는 무효 토큰
control
else 만료 또는 무효 토큰
조건: else 만료 또는 무효 토큰
+ 2개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → JwtAuthenticationFilter | HTTP Request (Bearer token) | - |
| 2 | JwtAuthenticationFilter → JwtTokenProvider | validateToken(token) | - |
| 3 | control | alt 유효한 토큰 | alt 유효한 토큰 |
| 4 | JwtTokenProvider → JwtAuthenticationFilter | true | alt 유효한 토큰 |
| 5 | JwtAuthenticationFilter → SecurityContext | setAuthentication(auth) | alt 유효한 토큰 |
| 6 | JwtAuthenticationFilter → Controller | 요청 전달 | alt 유효한 토큰 |
| 7 | Controller → Client | 200 OK | alt 유효한 토큰 |
| 8 | control | else 만료 또는 무효 토큰 | else 만료 또는 무효 토큰 |
| 9 | JwtTokenProvider → JwtAuthenticationFilter | false | else 만료 또는 무효 토큰 |
| 10 | JwtAuthenticationFilter → Client | 401 Unauthorized | else 만료 또는 무효 토큰 |
원본 Mermaid 보기
sequenceDiagram
actor Client
participant Filter as JwtAuthenticationFilter
participant Provider as JwtTokenProvider
participant Context as SecurityContext
participant Controller
Client->>Filter: HTTP Request (Bearer token)
Filter->>Provider: validateToken(token)
alt 유효한 토큰
Provider-->>Filter: true
Filter->>Context: setAuthentication(auth)
Filter->>Controller: 요청 전달
Controller-->>Client: 200 OK
else 만료 또는 무효 토큰
Provider-->>Filter: false
Filter-->>Client: 401 Unauthorized
end예약 락 처리▼
sequence
예약 락 처리
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → ProductController
POST /api/products/{id}/reserve
- 2처리
ProductController → ReservationService
reserve(product, user, quantity)
- 3처리
ReservationService → EntityManager
detach(product)
- 4저장
ReservationService → ProductRepository
findByIdForUpdate(productId)
- 5저장
ProductRepository → MySQL
SELECT ... FOR UPDATE
- 6처리
MySQL → ProductRepository
locked product
- 7alt 재고 충분
control
alt 재고 충분
조건: alt 재고 충분
- 8alt 재고 충분
ReservationService → MySQL
availableQuantity 감소
조건: alt 재고 충분
+ 4개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → ProductController | POST /api/products/{id}/reserve | - |
| 2 | ProductController → ReservationService | reserve(product, user, quantity) | - |
| 3 | ReservationService → EntityManager | detach(product) | - |
| 4 | ReservationService → ProductRepository | findByIdForUpdate(productId) | - |
| 5 | ProductRepository → MySQL | SELECT ... FOR UPDATE | - |
| 6 | MySQL → ProductRepository | locked product | - |
| 7 | control | alt 재고 충분 | alt 재고 충분 |
| 8 | ReservationService → MySQL | availableQuantity 감소 | alt 재고 충분 |
| 9 | ReservationService → MySQL | reservation 저장 | alt 재고 충분 |
| 10 | ReservationService → ProductController | 예약 성공 | alt 재고 충분 |
| 11 | control | else 재고 부족 | else 재고 부족 |
| 12 | ReservationService → ProductController | 예외 반환 | else 재고 부족 |
원본 Mermaid 보기
sequenceDiagram
participant C as Client
participant PC as ProductController
participant RS as ReservationService
participant EM as EntityManager
participant PR as ProductRepository
participant DB as MySQL
C->>PC: POST /api/products/{id}/reserve
PC->>RS: reserve(product, user, quantity)
RS->>EM: detach(product)
RS->>PR: findByIdForUpdate(productId)
PR->>DB: SELECT ... FOR UPDATE
DB-->>PR: locked product
alt 재고 충분
RS->>DB: availableQuantity 감소
RS->>DB: reservation 저장
RS-->>PC: 예약 성공
else 재고 부족
RS-->>PC: 예외 반환
end