You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
인기 비디오 조회 API 요청이 매번 MySQL에 직접 도달하여 트래픽 집중 시 DB 부하가 급증하고 응답 속도가 저하됨
원인파악
캐싱 레이어 없이 모든 읽기 요청이 MySQL SELECT로 처리되었고, 동일 비디오에 대한 중복 조회가 반복적으로 발생
해결과정
Redis를 캐시 레이어로 도입. @Cacheable을 활용해 비디오·채널 정보를 캐싱하고 TTL을 설정하여 데이터 일관성 유지
결과
DB 조회 쿼리 수 약 85% 감소 / 응답시간 200ms → 15ms (92% 개선)
2. 비디오 조회수 — Redis + Spring Batch 로 고빈도 Write 최적화
항목
내용
문제사항
비디오 조회마다 MySQL의 view_count 컬럼에 UPDATE 쿼리가 발생하여, 인기 비디오에 초당 수백 건의 Write가 집중되는 병목 발생
원인파악
실시간 카운터를 RDB에 직접 적용하는 구조는 행(Row) 단위 잠금으로 인한 경합이 불가피하며, Write 집중이 전체 DB 성능 저하로 이어짐
해결과정
Redis INCR로 조회수를 인메모리에 누적한 뒤, Spring Batch 스케줄러가 주기적으로 집계하여 MySQL에 벌크 UPDATE로 동기화
결과
DB Write 쿼리 수 약 95% 감소 (조회마다 1건 → 배치 주기마다 1건 벌크 UPDATE)
3. 쿠폰 발급 — Redisson 분산 락으로 동시성 문제 해결
항목
내용
문제사항
선착순 한정 쿠폰 발급 시 다수의 동시 요청이 몰려 재고 수량을 초과하는 중복 발급이 발생
원인파악
다중 서버 환경에서 SELECT → 검증 → INSERT 흐름 사이의 Race Condition. JVM 레벨의 synchronized는 분산 환경에서 효력 없음
해결과정
Redisson의 분산 락(RLock)을 쿠폰 발급 유즈케이스에 적용하여, 단일 서버가 임계 구역을 점유하는 동안 나머지 요청은 대기하도록 처리
결과
JMeter 500 동시 요청 테스트 기준 — 초과 발급 건수 0건, 정합성 100% 보장
4. 새로운 비디오 알림 전송 — Kafka 비동기 분리로 트랜잭션 결합 제거
항목
내용
문제사항
비디오 생성 트랜잭션 안에서 구독자 알림을 동기 전송하여, 알림 서비스 지연·장애 발생 시 비디오 생성 API의 응답시간이 함께 증가
원인파악
구독자 수에 비례하여 알림 루프 처리 시간이 선형 증가하고, 외부 장애가 핵심 비즈니스 트랜잭션에 직접 영향을 줌
해결과정
비디오 생성 완료 후 Kafka 토픽에 이벤트를 발행하고, 알림 컨슈머가 별도로 구독자에게 전송하도록 분리. 컨슈머 파티션 수를 늘려 수평 확장 가능한 구조로 설계
결과
비디오 생성 API 응답시간 약 2,500ms → 150ms (94% 개선) / 알림 장애가 비디오 생성 성공 여부에 무영향
5. 댓글 작성 — MongoDB 도입으로 계층 구조 쿼리 개선
항목
내용
문제사항
부모-자식 계층 구조의 댓글(대댓글)을 MySQL에서 처리할 때 재귀 CTE 또는 다단계 JOIN이 필요하여 쿼리 복잡도가 높고 응답이 느림
원인파악
RDB는 계층형 트리 구조를 표현하는 데 적합하지 않으며, depth가 깊어질수록 쿼리 비용이 기하급수적으로 증가
해결과정
댓글 도메인을 MongoDB로 분리. 댓글 문서 안에 자식 댓글을 중첩 배열로 저장하거나 parentId 참조 방식을 활용하여 단일 쿼리로 계층 조회 가능하게 설계
결과
3-depth 댓글 조회 응답시간 약 180ms → 30ms (83% 개선) / 스키마 변경 없이 댓글 메타데이터 필드 확장 가능
6. 도서 검색 — Resilience4j Circuit Breaker로 외부 API 장애 대응
항목
내용
문제사항
Naver 도서 검색 API가 일시적으로 장애 상태일 때 요청이 계속 누적되어 스레드 고갈이 발생하고, 도서 검색 기능 전체가 응답 불가 상태가 됨
원인파악
단일 외부 API 의존 구조에서 Timeout 설정만으로는 장애 전파를 막기 어렵고, 장애 지속 중에도 반복 요청이 발생하여 복구를 방해
해결과정
Resilience4j Circuit Breaker를 Naver API 호출부에 적용. 오류율 50% 초과 시 회로를 OPEN하여 즉시 Kakao API로 Fallback 전환. HALF_OPEN 상태에서 회복 여부를 자동 감지
결과
Naver API 장애 시 Fallback 전환 시간 500ms 이내 / 도서 검색 서비스 가용성 99% 이상 유지
개발 환경
Java
Spring Boot
Gradle
JPA
QueryDSL
MySQL
mongodb
redis
kafka
프로젝트 모듈 구조
헥사고날 아키텍쳐로 구성하기 위해 프로젝트를 멀티 모듈로 구성합니다.
주요 모듈:
video-core: 비즈니스 로직과 도메인 모델을 관리하는 기본 모듈
video-apps: 클라이언트가 호출할 수 있는 REST API 와 배치잡을 모아둔 모듈
video-adapters: 외부 인프라와 통신하기 위한 모듈
video-commons: 공통으로 사용되는 유틸리티를 모아둔 모듈
실행방법
docker compose up
./gradlew bootRun
Architecture
아키텍처 설계 배경
헥사고날 아키텍처 채택 이유
비즈니스 로직(core-service, core-usecase)을 외부 기술(JPA, Redis, Kafka 등)로부터 완전히 분리하기 위해 헥사고날 아키텍처를 채택했습니다.
의존성 역전(DIP): Port 인터페이스를 통해 도메인이 인프라를 직접 참조하지 않으며, 기술 교체 시 Adapter만 수정하면 됩니다.
독립적 테스트: 각 레이어를 Port 목킹 기반으로 단위 테스트할 수 있어, 외부 인프라 없이도 비즈니스 로직 검증이 가능합니다.
컴파일 타임 의존성 강제: 멀티모듈 구조로 레이어 간 잘못된 의존성을 컴파일 타임에 차단합니다.
멀티모듈 구조의 의도
모듈
의도
video-core
순수 Java로 작성하여 외부 라이브러리 의존 없음 → 기술 중립성 보장
video-adapters
JPA, Redis, Kafka, MongoDB 등 모든 외부 기술 의존성을 한 곳에 격리
video-apps
REST API(video-api)와 배치(video-batch) 진입점을 분리하여 배포 단위 독립성 확보
video-commons
공통 유틸리티를 별도 모듈로 분리하여 중복 제거
기술 선택 기준
기술
선택 이유
MySQL
비디오·채널·사용자·구독·쿠폰 등 관계형 데이터의 트랜잭션 무결성 보장이 필요하고, 구독 관계 등 복잡한 JOIN 쿼리가 요구되는 영구 저장소
MongoDB
댓글은 부모-자식 계층 구조와 스키마 유연성이 필요하며, 고빈도 읽기/쓰기에 RDB보다 유리
Redis
①비디오·채널 캐시로 DB 부하 감소, ②조회수 고빈도 Write를 메모리에서 처리 후 배치 동기화, ③Redisson 분산 락으로 쿠폰 발급 동시성 제어, ④사용자 세션 관리
Kafka
새 비디오 업로드 시 구독자 알림을 비동기로 분리하여 알림 장애가 비디오 생성 트랜잭션에 영향 없도록 설계, 컨슈머 수평 확장으로 대량 구독자 처리 가능
QueryDSL
채널별 비디오 조회 등 동적 조건 쿼리를 컴파일 타임에 타입 안전하게 작성하여 런타임 오류 방지
Resilience4j (Circuit Breaker)
도서 검색 외부 API(Naver)의 장애를 감지해 자동으로 Kakao API로 Fallback, 외부 의존성 장애의 내부 전파 방지
Spring Batch
Redis에 누적된 비디오 조회수를 주기적으로 MySQL에 동기화하는 대량 데이터 처리 작업에 적합
Flow
비디오 생성
채널 구독 등록
k6 부하 테스트 스크립트
테스트 대상 기술 스택
시나리오
검증 대상
핵심 지표
video-load
Redis 캐시 (비디오 조회), Redis INCR (조회수)
캐시 히트율 >80%, P95 <100ms
comment-load
MongoDB CRUD, 커서 페이지네이션
P95 <200ms (작성), <150ms (조회)
coupon-load
Redisson 분산 락, Race Condition 방어
초과 발급 0건 (≤500건)
stress-test
전체 시스템 한계점
P99 <2000ms
spike-test
Circuit Breaker (Resilience4j), 급격한 트래픽 대응
P99 <3000ms
사전 준비
2. 시드 데이터 DB 등록
MySQL에 다음 ID로 데이터가 존재해야 합니다:
비디오: video-seed-001 ~ video-seed-005
채널: channel-seed-001 ~ channel-seed-003
MongoDB에 다음 ID로 데이터가 존재해야 합니다:
댓글: comment-seed-001 ~ comment-seed-003
부모 댓글: parent-comment-001 ~ parent-comment-002
3. Redis 세션 등록
다음 토큰이 Redis에 세션으로 등록되어야 합니다 (x-auth-key 헤더 인증):
test-auth-token-001 ~ test-auth-token-005
4. 쿠폰 정책 생성
쿠폰 분산 락 테스트 전 다음 정책이 존재해야 합니다:
ID: coupon-policy-concurrent-001
totalQuantity: 500
실행 방법
기본 실행 (로컬 서버)
# Smoke Test (기본 동작 확인 — 배포 직후 필수)
k6 run k6-scripts/scenarios/smoke/smoke-test.js
# Video Load Test (Redis 캐시 성능)
k6 run k6-scripts/scenarios/load/video-load.js
# Comment Load Test (MongoDB CRUD)
k6 run k6-scripts/scenarios/load/comment-load.js
# Coupon Load Test (Redisson 분산 락) — 핵심
k6 run k6-scripts/scenarios/load/coupon-load.js
# Stress Test (한계점 탐색)
k6 run k6-scripts/scenarios/stress/stress-test.js
# Spike Test (트래픽 폭증)
k6 run k6-scripts/scenarios/spike/spike-test.js
환경변수로 서버 지정
BASE_URL=http://your-server:8080 k6 run k6-scripts/scenarios/smoke/smoke-test.js
결과를 JSON으로 저장
k6 run --out json=result.json k6-scripts/scenarios/load/coupon-load.js
Grafana + InfluxDB 연동 (실시간 모니터링)
k6 run --out influxdb=http://localhost:8086/k6 k6-scripts/scenarios/stress/stress-test.js