Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CutIng (미용실 예약 관리 플랫폼)

Spring Boot와 Vue.js 기반의 종합 미용실 뷰티 플랫폼입니다. 고객과 스타일리스트(미용사)가 편리하게 예약과 결제를 진행하고 리뷰 및 포트폴리오를 관리할 수 있습니다.

주요 기능

  • 고객용 기능
    • 스타일리스트 위치 기반 검색 및 필터링 기능
    • 미용 서비스 예약 시스템 (원하는 날짜/시간 선택)
    • Toss Payments 기반 간편 결제 및 승인 (모의 결제 테스트 환경)
    • 예약 관리 및 취소 기능
  • 스타일리스트(미용사)용 관리 기능
    • 프로필 편집 (자기소개, 경력, 미용실 위치)
    • 영업시간 및 휴무일 관리
    • 제공 서비스 및 가격 관리 (추가, 수정, 삭제)
    • 포트폴리오(작업 이미지) 관리
    • 고객 예약 확인 및 승인/거절, 완료 시스템 처리

기술 스택

  • Backend: Java 17, Spring Boot 3.2, Spring Security, Spring Data JPA, MySQL
  • Frontend: Vue.js 3, Pinia, Vue Router, Vite
  • 결제 연동: Toss Payments SDK
  • 소셜 로그인: Kakao OAuth2 (지원 예정)

실행 방법

Backend (Spring Boot)

# /src/main/resources/application.yml (또는 환경변수)에서 DB 설정(MySQL) 필요
./gradlew bootRun

Frontend (Vue.js)

cd frontend
npm install
npm run dev

성능 개선 기록

1. [CPU 연산 부하] 실시간 베이지안 점수 계산으로 인한 CPU 자원 고갈

문제 상황

랭킹 API를 호출할 때마다 DB에서 전체 미용사의 예약 수·리뷰 수·평균 별점을 끌어와 베이지안 평균을 런타임에 직접 계산했습니다. 미용사 N명에 대해 COUNT, AVG 집계 쿼리가 N번 추가로 발생하는 구조였고, 부하 테스트에서 로컬 DB CPU 사용률이 100%까지 치솟는 병목을 확인했습니다.

해결 과정

  • 1차 개선 — Fetch Join + 복합 인덱스: 연관 데이터를 한 번에 가져오도록 쿼리를 바꾸고 (district, status) 복합 인덱스를 추가해 N+1 쿼리를 단일 쿼리로 줄였습니다. 하지만 지속 부하 환경에서 COUNT·AVG 집계 자체의 연산 비용은 그대로 남았습니다.
  • 2차 개선 — Redis ZSET 인메모리 캐싱: 연산 책임을 "조회 시점"에서 "예약·리뷰 작성 시점"으로 옮겼습니다. 예약이 완료되면 Redis ZSET 점수를 증분(ZINCRBY)하고, 랭킹 조회는 ZREVRANGE 한 번으로 O(log N) 안에 처리합니다. DB 집계 쿼리가 조회 경로에서 완전히 사라집니다.

측정 결과 (50 VU, 30초, 로컬 환경 / k6 부하 테스트)

지표 1차 개선 (Fetch Join) 2차 개선 (Redis ZSET) 변화
avg latency 44ms 32ms ↓ 27%
p95 latency 99ms 70ms ↓ 29%
RPS 346 376 ↑ 9%
총 요청 수 10,417건 11,334건 -
에러율 0% 0% -
랭킹 조회 시 DB 집계 쿼리 매 요청 발생 0건 제거

2. [스레드 고갈] 무거운 동기 처리로 인한 Tomcat 스레드 블로킹 (Thread Starvation)

문제 상황

예약 저장 → 랭킹 재계산 → WebSocket 알림 전송이 하나의 흐름에 묶여 동기로 실행됐습니다. 알림·랭킹 처리가 느려지면 Tomcat 스레드가 응답을 주지 못한 채 붙잡혀 있었고, 동시 요청이 쌓이면 스레드 풀이 소진되는 구조였습니다. 핵심 비즈니스인 예약이 부가 도메인인 알림의 처리 완료를 기다려야 한다는 것도 문제였습니다.

해결

랭킹·알림 처리를 @Async 스레드풀에 위임해 메인 스레드는 DB 저장 직후 응답을 반환하도록 분리했습니다. 예약 성공·실패 여부가 알림·랭킹 도메인 로직과 완전히 독립됩니다. 다만 이 구조에서 DB 커넥션 경합 문제가 새롭게 드러났습니다.


3. [DB 커넥션 고갈] 분산락 I/O로 인한 HikariCP 커넥션 마름 현상

문제 상황

@Async 도입 후 비동기 스레드와 메인 스레드가 HikariCP 커넥션 풀(10개)을 함께 소비했습니다. 더 큰 문제는 Redis 분산락 획득 코드가 @Transactional 내부에 있어서 Redis 네트워크 I/O 대기 시간 동안 DB 커넥션을 계속 점유하고 있었다는 점입니다. 50건이 동시에 들어오면 커넥션이 바닥나 타임아웃이 발생했고, 인메모리 큐 용량 초과 시 랭킹·알림 이벤트가 유실되는 위험도 있었습니다.

해결

  1. 트랜잭션 범위 축소: 분산락 획득(Redis I/O)을 @Transactional 밖으로 꺼내고 TransactionTemplate으로 실제 DB 작업 구간에만 커넥션을 사용하도록 변경해, 커넥션 점유 시간을 최소화했습니다.
  2. Redis Streams 도입: 인메모리 큐를 Redis Streams 브로커로 교체해 서버가 재시작되어도 이벤트가 보존됩니다. Consumer가 후처리(랭킹 갱신·알림 전송)에 성공한 경우에만 ACK를 전송하므로 이벤트 유실 없이 정합성을 보장합니다.

측정 결과 (50 VU, 60초, 로컬 환경 / k6 부하 테스트)

지표 Before (동기 처리) After (트랜잭션 분리 + Redis Streams)
avg latency 29ms 25ms
p95 latency 41ms 69ms†
총 요청 수 5,700건 401건
에러율 0.02% 0%
중복 예약 레이스 컨디션 미검증 0건 (분산락 차단 50건)
이벤트 유실 큐 포화 시 발생 가능 0건

† After p95(69ms)는 동시 중복 예약 시나리오 측정값으로 분산락 대기 시간이 포함됩니다. Before는 VU별 고유 시간대를 사용한 순차 처리 시나리오이므로 latency 수치를 직접 비교하기보다 중복 예약 0건, 이벤트 유실 0건을 핵심 지표로 봐야 합니다.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages