cd ..

BorrowMe

11인 팀 물건 대여 플랫폼에서 예약 정합성, 상품 목록 N+1, 알림 흐름을 맡아 상품 100개 조회를 201→3쿼리로 줄이고 동시 예약 정합성 100%를 검증했습니다.

201→3
상품 목록 쿼리

상품 100개 조회 시 JOIN FETCH + 배치 로딩으로 N+1 제거

1,010→23ms
p95 응답

목록 조회 p95를 44배 줄인 성능 개선

50/50
동시 예약 결과

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

Java 17Spring Boot 3Spring SecurityJWTSpring Data JPAMySQLAWS S3SwaggerTestcontainersk6
Key Result

상품 목록 조회: 상품 100개 기준 201회 → 3회 쿼리, p95 1,010ms → 23ms, 처리량 30 req/s → 253 req/s로 개선했습니다.

개요

11인 팀 해커톤 프로젝트였지만, 제가 맡은 범위는 예약 정합성, 상품 목록 최적화, 알림 시스템처럼 백엔드 핵심 흐름이었습니다. 상품 목록 조회에서 User, Hashtag, Follow를 건건이 읽던 구조를 JOIN FETCH + 배치 쿼리로 바꿔 p95를 1,010ms에서 23ms로 줄였고, 예약 API에는 비관적 락과 캐시 우회 처리를 넣어 초과 예약 없이 동시성을 검증했습니다. 해커톤 산출물에서 끝내지 않고 민감정보 노출 정리와 k6 재측정까지 이어간 팀 프로젝트입니다.

팀 프로젝트와 맡은 범위

  • 11인 팀에서 예약 시스템, 상품 목록 성능 최적화, 알림 시스템을 담당
  • 예약 API에 Pessimistic Lock 기반 재고 차감과 취소 흐름 구현
  • 댓글·답글·팔로우 알림 및 읽음 처리 API 구성
  • 해커톤 종료 후 정합성, 보안, 성능 지표를 다시 정리해 포트폴리오 수준으로 보강

성능 최적화

  • 상품 100개 기준 쿼리 201회 → 3회, p95 1,010ms → 23ms, 처리량 30 req/s → 253 req/s
  • JOIN FETCH로 Product-User-Hashtag를 한 번에 읽고, 팔로우 상태는 배치 쿼리로 사전 로딩
  • 해시태그 업서트는 findByName/save 반복 대신 findByNameIn + saveAll로 최대 2회 쿼리로 고정
  • 검색 API는 30 VU, p95 72ms, 223 req/s, 에러율 0%를 기록

예약 정합성

  • 100 VU, 재고 50개 동시 예약 테스트에서 성공 50건, 실패 50건으로 정합성 확인
  • Product 행에 @Lock(PESSIMISTIC_WRITE)를 적용해 중복 차감을 방지
  • OSIV + Hibernate L1 캐시 때문에 FOR UPDATE가 무시되던 문제를 entityManager.detach()로 해결
  • 최종 재고 0 유지, 초과 예약 없이 정합성 100% 확인

보안 & 운영

  • @JsonIgnore로 passwordHash, verificationToken 같은 민감정보를 API 응답에서 제거
  • JWT 인증, BCrypt 비밀번호 암호화, 환경변수 기반 비밀키 분리
  • AWS S3 이미지 업로드와 Swagger/OpenAPI 문서 구성
  • git filter-repo와 GitHub Push Protection으로 노출된 비밀값 이력 정리

아키텍처

전체 아키텍처

ERD

전체 ERD

시퀀스 다이어그램

JWT 인증 흐름
예약 락 처리

문제 원인

  1. 01상품 목록 조회 시 User, Hashtag, Follow를 건건이 읽어 상품 100개 기준 201회 쿼리와 p95 1,010ms가 발생했습니다.
  2. 02같은 상품을 여러 사용자가 동시에 예약하면 재고가 음수로 내려가는 Race Condition이 발생했습니다.
  3. 03OSIV 환경에서 Hibernate L1 캐시가 findByIdForUpdate()를 우회해 Pessimistic Lock이 실제로 걸리지 않는 문제가 있었습니다.
  4. 04연관 엔티티 직렬화 과정에서 passwordHash, verificationToken 같은 민감정보가 응답에 노출될 수 있었습니다.

해결 과정

  1. 01JOIN FETCH로 Product-User-Hashtag를 한 번에 조회하고, 팔로우 상태는 배치 쿼리로 사전 로딩해 목록 조회를 3회 쿼리로 정리했습니다.
  2. 02ProductRepository.findByIdForUpdate()에 @Lock(PESSIMISTIC_WRITE)를 적용해 재고 차감 구간을 직렬화했습니다.
  3. 03entityManager.detach()로 캐시된 Product를 분리한 뒤 SELECT ... FOR UPDATE를 다시 실행해 실제 락이 걸리도록 수정했습니다.
  4. 04@JsonIgnore, 환경변수 분리, git filter-repo로 민감정보 노출 경로를 정리했습니다.

결과

  1. 01상품 목록 조회: 상품 100개 기준 201회 → 3회 쿼리, p95 1,010ms → 23ms, 처리량 30 req/s → 253 req/s로 개선했습니다.
  2. 02동시 예약 테스트(100 VU, 재고 50개): 성공 50건, 실패 50건, 최종 재고 0으로 정합성 100%를 확인했습니다.
  3. 03검색 API 부하테스트(30 VU, 30초): p95 72ms, 223 req/s, 에러율 0%를 기록했습니다.
  4. 04API 응답에서 민감정보를 제거했고, 비밀키는 환경변수로 분리해 Push Protection과 함께 관리하게 만들었습니다.