프로젝트 목록으로 돌아가기
Recruiter Briefborrow-me

BorrowMe

11인 팀 물건 대여 플랫폼에서 예약 정합성, 상품 목록 조회, 알림 흐름을 맡아 동시 예약 성공/실패 경로를 검증했습니다.

Role
백엔드 담당
Period
2025
Team
11인 팀
Theme
Spring/JPA
Java 17Spring Boot 3Spring SecurityJWTSpring Data JPAMySQLAWS S3Swagger

30초 요약

문제
팀 대여 플랫폼에서 상품 조회 N+1과 동시 예약 재고 차감 문제가 있었습니다.
해결
JOIN FETCH, PESSIMISTIC_WRITE, detach 후 FOR UPDATE 재조회로 조회와 예약 경계를 정리했습니다.
검증
재고 50개에 100명 동시 예약 시 성공 50건, 실패 50건, 최종 재고 0을 확인했습니다.

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명 예약 경로를 확인했습니다.

JOIN FETCH
Verified
상품 목록 쿼리

user/hashtags 접근을 통합 테스트로 확인

30 VU
Target
상품 목록 부하

상품 목록 k6 p95 <500ms 목표 기준

50/50
Verified
동시 예약 결과

100 VU, 재고 50개 테스트에서 성공 50건, 실패 50건

Target

JOIN FETCH

상품 목록

Scenario
상품 목록에서 사용자와 해시태그를 함께 조회
Method
JOIN FETCH 적용 후 통합 테스트와 k6 상품 목록 시나리오로 재현
Result
상품 user/hashtags 접근은 통합 테스트 범위에 포함했습니다. 상품 목록 p95 <500ms는 k6 스크립트의 목표 기준으로만 명시했습니다.
Verified

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. 1. React Client → JwtAuthenticationFilter
  2. 2. JwtAuthenticationFilter → Controller
  3. 3. Controller → Service
  4. 4. Service → Repository
  5. 5. Repository → MySQL
  6. 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 --> S3

ERD

전체 ERD

erd

전체 ERD

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

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

+ 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. #1 USER ||--o{ PRODUCT · 소유
  2. #2 USER ||--o{ RESERVATION · 신청
  3. #3 USER ||--o{ COMMENT · 작성
  4. #4 USER ||--o{ LIKES · 좋아요
  5. #5 USER ||--o{ FOLLOW · 팔로우
  6. #6 USER ||--o{ NOTIFICATION · 수신
  7. #7 USER ||--o{ REPLY · 작성
  8. #8 PRODUCT ||--o{ RESERVATION · 예약
  9. #9 PRODUCT ||--o{ COMMENT · 댓글
  10. #10 PRODUCT ||--o{ LIKES · 좋아요
  11. #11 PRODUCT ||--o{ NOTIFICATION · 알림
  12. #12 PRODUCT ||--o{ PRODUCT_HASHTAGS · 태깅
  13. #13 HASHTAG ||--o{ PRODUCT_HASHTAGS · 태깅
  14. #14 COMMENT ||--o{ REPLY · 답글
  15. #15 COMMENT ||--o{ NOTIFICATION · 알림
  16. #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
ClientJwtAuthenticationFilterJwtTokenProviderSecurityContextController
  1. 1요청

    Client → JwtAuthenticationFilter

    HTTP Request (Bearer token)

  2. 2검증

    JwtAuthenticationFilter → JwtTokenProvider

    validateToken(token)

  3. 3alt 유효한 토큰

    control

    alt 유효한 토큰

    조건: alt 유효한 토큰

  4. 4alt 유효한 토큰

    JwtTokenProvider → JwtAuthenticationFilter

    true

    조건: alt 유효한 토큰

  5. 5검증

    JwtAuthenticationFilter → SecurityContext

    setAuthentication(auth)

    조건: alt 유효한 토큰

  6. 6alt 유효한 토큰

    JwtAuthenticationFilter → Controller

    요청 전달

    조건: alt 유효한 토큰

  7. 7alt 유효한 토큰

    Controller → Client

    200 OK

    조건: alt 유효한 토큰

  8. 8else 만료 또는 무효 토큰

    control

    else 만료 또는 무효 토큰

    조건: else 만료 또는 무효 토큰

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

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → JwtAuthenticationFilterHTTP Request (Bearer token)-
2JwtAuthenticationFilter → JwtTokenProvidervalidateToken(token)-
3controlalt 유효한 토큰alt 유효한 토큰
4JwtTokenProvider → JwtAuthenticationFiltertruealt 유효한 토큰
5JwtAuthenticationFilter → SecurityContextsetAuthentication(auth)alt 유효한 토큰
6JwtAuthenticationFilter → Controller요청 전달alt 유효한 토큰
7Controller → Client200 OKalt 유효한 토큰
8controlelse 만료 또는 무효 토큰else 만료 또는 무효 토큰
9JwtTokenProvider → JwtAuthenticationFilterfalseelse 만료 또는 무효 토큰
10JwtAuthenticationFilter → Client401 Unauthorizedelse 만료 또는 무효 토큰
원본 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
ClientProductControllerReservationServiceEntityManagerProductRepositoryMySQL
  1. 1요청

    Client → ProductController

    POST /api/products/{id}/reserve

  2. 2처리

    ProductController → ReservationService

    reserve(product, user, quantity)

  3. 3처리

    ReservationService → EntityManager

    detach(product)

  4. 4저장

    ReservationService → ProductRepository

    findByIdForUpdate(productId)

  5. 5저장

    ProductRepository → MySQL

    SELECT ... FOR UPDATE

  6. 6처리

    MySQL → ProductRepository

    locked product

  7. 7alt 재고 충분

    control

    alt 재고 충분

    조건: alt 재고 충분

  8. 8alt 재고 충분

    ReservationService → MySQL

    availableQuantity 감소

    조건: alt 재고 충분

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

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → ProductControllerPOST /api/products/{id}/reserve-
2ProductController → ReservationServicereserve(product, user, quantity)-
3ReservationService → EntityManagerdetach(product)-
4ReservationService → ProductRepositoryfindByIdForUpdate(productId)-
5ProductRepository → MySQLSELECT ... FOR UPDATE-
6MySQL → ProductRepositorylocked product-
7controlalt 재고 충분alt 재고 충분
8ReservationService → MySQLavailableQuantity 감소alt 재고 충분
9ReservationService → MySQLreservation 저장alt 재고 충분
10ReservationService → ProductController예약 성공alt 재고 충분
11controlelse 재고 부족else 재고 부족
12ReservationService → 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