diff --git a/.gitignore b/.gitignore
index 2979364..d4f1498 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,9 @@ src/main/resource/application-local.yml
/**/application-local.yml
### Redis ###
-/redis/redis.conf
\ No newline at end of file
+/redis/redis.conf
+
+### SSL cert
+src/main/resources/cert/
+*.pem
+*.p12
\ No newline at end of file
diff --git a/docs/test/load_test_guide.md b/docs/test/load_test_guide.md
new file mode 100644
index 0000000..071a5a1
--- /dev/null
+++ b/docs/test/load_test_guide.md
@@ -0,0 +1,446 @@
+# 부하 테스트 실행 가이드
+
+## 1. 환경 설정
+
+### 1.1 필수 도구 설치
+
+```bash
+# k6 설치 (macOS)
+brew install k6
+
+# Redis CLI (macOS — 이미 redis가 설치되어 있다면 포함됨)
+brew install redis
+
+# PostgreSQL CLI
+brew install postgresql
+```
+
+### 1.2 로컬 인프라 확인
+
+```bash
+# PostgreSQL 상태 확인
+pg_isready -h localhost -p 5432
+
+# Redis 상태 확인
+redis-cli ping # → PONG
+
+# 서버 (SSL)
+curl -sk https://localhost:8080
+```
+
+### 1.3 테스트 데이터 준비
+
+```bash
+# 기존 데이터 초기화 (선택)
+psql -h localhost -U seohyun -d fisa -f scripts/reset-test-data.sql
+
+# 대용량 테스트 데이터 삽입
+psql -h localhost -U seohyun -d fisa -f scripts/insert-test-data.sql
+```
+
+| 테이블 | 데이터 수 | 설명 |
+|--------------|--------|-----------|
+| Users | 100 | 테스트 사용자 |
+| Organization | 3 | 테스트 조직 |
+| Member | ~150 | 조직당 50명 |
+| Video | 1,500 | 조직당 500개 |
+| History | 7,500+ | 멤버당 약 50개 |
+| Scrap | 1,000 | 스크랩 데이터 |
+
+```bash
+# 데이터 확인
+psql -h localhost -U seohyun -d fisa -c "
+SELECT 'Users' as t, count(*) FROM users
+UNION ALL SELECT 'Videos', count(*) FROM video WHERE upload_status = 'COMPLETE'
+UNION ALL SELECT 'History', count(*) FROM history;"
+```
+
+---
+
+## 2. 프로젝트 구조
+
+```
+k6-tests/
+├── shared/
+│ ├── config.js # 공통 설정 (BASE_URL, 테스트 데이터)
+│ └── auth.js # JWT 토큰 인증 헬퍼
+├── results/ # 테스트 결과 저장 (자동 생성)
+│ ├── scenario1-indexing/ # 인덱스 시나리오 결과
+│ ├── scenario2-cache/ # 캐시 시나리오 결과
+│ └── scenario3-pool/ # 커넥션풀 시나리오 결과
+├── home-api-test.js # 홈 조회 API 테스트
+├── history-api-test.js # 시청 기록 조회 API 테스트
+├── video-join-api-test.js # 영상 시청 세션 시작 API 테스트
+└── run-scenario.sh # 시나리오 오케스트레이터 (메인 실행 스크립트)
+
+scripts/
+├── add-indexes.sql # 인덱스 생성 스크립트
+├── drop-indexes.sql # 인덱스 롤백 스크립트
+├── insert-test-data.sql # 대용량 데이터 삽입
+└── reset-test-data.sql # 데이터 초기화
+
+src/main/resources/
+├── application-local.yml # 로컬 환경 설정
+└── application-nocache.yml # 캐시 비활성화 프로필
+```
+
+---
+
+## 3. 테스트 실행 — 권장 순서
+
+> 시나리오는 **1 → 2 → 3** 순서로 진행하세요.
+> 각 시나리오는 독립적이므로 개별 실행도 가능합니다.
+
+### 한 줄 요약
+
+```bash
+cd k6-tests
+./run-scenario.sh 1 # 인덱스 (완전 자동)
+./run-scenario.sh 2 # 캐시 (서버 재시작 필요)
+./run-scenario.sh 3 # 커넥션풀 (서버 재시작 필요)
+./run-scenario.sh all # 전체 순차 실행
+```
+
+---
+
+## 4. 시나리오 1: 인덱스 Before/After (완전 자동)
+
+### 목적
+
+인덱스 추가 전후의 쿼리 성능 차이 측정
+
+### 전제 조건
+
+- 서버가 `local` 프로필로 실행 중
+- PostgreSQL 접속 가능 (PGPASSWORD=1234)
+
+### 실행
+
+```bash
+cd k6-tests
+./run-scenario.sh 1
+```
+
+### 자동 실행 흐름
+
+```
+① drop-indexes.sql 실행 (인덱스 제거)
+② Redis 캐시 초기화 (home:*, video:*:info)
+③ Before 테스트: 3개 API × k6 실행
+④ add-indexes.sql 실행 (인덱스 적용)
+⑤ Redis 캐시 초기화
+⑥ After 테스트: 3개 API × k6 실행
+⑦ drop-indexes.sql 실행 (롤백 — 원래 상태 복원)
+```
+
+### 결과 확인
+
+```
+results/scenario1-indexing/
+├── before-index-home-api-2026-03-03T14-30-00.html # Before HTML 리포트
+├── before-index-home-api-2026-03-03T14-30-00-summary.json # Before JSON 원시 데이터
+├── after-index-home-api-2026-03-03T14-35-00.html # After HTML 리포트
+├── after-index-home-api-2026-03-03T14-35-00-summary.json
+├── before-index-history-api-*.html
+├── after-index-history-api-*.html
+├── before-index-video-join-api-*.html
+└── after-index-video-join-api-*.html
+```
+
+### 검증
+
+```bash
+# 롤백 확인 — 커스텀 인덱스가 0이면 정상
+psql -h localhost -U seohyun -d fisa -c \
+ "SELECT count(*) FROM pg_indexes WHERE indexname LIKE 'idx_%';"
+```
+
+---
+
+## 5. 시나리오 2: 캐시 Before/After (반자동)
+
+### 목적
+
+Redis 캐시 활성화 전후의 응답 시간 차이 측정
+
+### 전제 조건
+
+- 인덱스가 적용된 상태에서 테스트하려면 먼저 `add-indexes.sql` 실행
+- 터미널 2개 필요 (서버용 + 테스트 실행용)
+
+### 실행
+
+```bash
+cd k6-tests
+./run-scenario.sh 2
+```
+
+### 반자동 흐름
+
+```
+① 스크립트가 "nocache 프로필로 서버 재시작" 안내 표시
+ → 서버 터미널에서:
+ SPRING_PROFILES_ACTIVE=local,nocache ./gradlew bootRun
+ → 서버 시작 후 Enter
+
+② Redis 캐시 초기화
+③ Before 테스트 (캐시 꺼진 상태)
+
+④ 스크립트가 "local 프로필로 서버 재시작" 안내 표시
+ → 서버 터미널에서:
+ SPRING_PROFILES_ACTIVE=local ./gradlew bootRun
+ → 서버 시작 후 Enter
+
+⑤ Redis 캐시 초기화
+⑥ After 테스트 (캐시 켜진 상태)
+```
+
+### 캐시 토글 원리
+
+`application-nocache.yml` 프로필을 추가하면 `app.cache.enabled=false`가 적용됩니다.
+`HomeService`와 `VideoService`에서 이 값에 따라 Redis 캐시 읽기/쓰기를 건너뜁니다.
+
+```yaml
+# application-nocache.yml
+app:
+ cache:
+ enabled: false
+```
+
+### 결과 확인
+
+```
+results/scenario2-cache/
+├── before-cache-home-api-*.html
+├── after-cache-home-api-*.html
+├── before-cache-history-api-*.html
+├── after-cache-history-api-*.html
+├── before-cache-video-join-api-*.html
+└── after-cache-video-join-api-*.html
+```
+
+### 검증
+
+```bash
+# nocache 상태에서 캐시 키가 생성되지 않는지 확인
+redis-cli KEYS "home:*" # → (empty)
+redis-cli KEYS "video:*:info" # → (empty)
+
+# cache 활성 상태에서 After 테스트 후 키 존재 확인
+redis-cli KEYS "home:*" # → home:1:RECENT 등
+```
+
+---
+
+## 6. 시나리오 3: Connection Pool 크기 비교 (반자동)
+
+### 목적
+
+HikariCP `maximum-pool-size` 값(10, 50, 100)에 따른 동시 처리량 변화 측정
+
+### 전제 조건
+
+- `application-local.yml`에 `HIKARI_MAX_POOL_SIZE` 환경변수가 파라미터화되어 있어야 함 (이미 설정됨)
+
+### 실행
+
+```bash
+cd k6-tests
+./run-scenario.sh 3
+```
+
+### 반자동 흐름
+
+```
+for pool_size in 10 50 100:
+ ① 스크립트가 "HIKARI_MAX_POOL_SIZE=${pool_size}로 서버 재시작" 안내
+ → 서버 터미널에서:
+ HIKARI_MAX_POOL_SIZE=10 SPRING_PROFILES_ACTIVE=local ./gradlew bootRun
+ → 서버 시작 후 Enter
+
+ ② Redis 캐시 초기화
+ ③ 3개 API 테스트 실행
+```
+
+### 결과 확인
+
+```
+results/scenario3-pool/
+├── pool-10-home-api-*.html
+├── pool-10-history-api-*.html
+├── pool-10-video-join-api-*.html
+├── pool-50-home-api-*.html
+├── pool-50-history-api-*.html
+├── pool-50-video-join-api-*.html
+├── pool-100-home-api-*.html
+├── pool-100-history-api-*.html
+└── pool-100-video-join-api-*.html
+```
+
+### 핵심 비교 지표
+
+| 지표 | pool=10 | pool=50 | pool=100 |
+|--------------|---------|---------|----------|
+| p95 응답 시간 | ? | ? | ? |
+| 503/504 에러 수 | ? | ? | ? |
+| 최대 TPS | ? | ? | ? |
+
+---
+
+## 7. 결과 분석 방법
+
+### 7.1 HTML 리포트 열기
+
+```bash
+# macOS에서 리포트 열기
+open k6-tests/results/scenario1-indexing/after-index-home-api-*.html
+
+# 또는 파일 탐색기에서 .html 파일 더블클릭
+```
+
+HTML 리포트에는 다음이 포함됩니다:
+
+- 요청 수, 에러율, 응답 시간 분포 차트
+- p50 / p90 / p95 / p99 백분위 테이블
+- 커스텀 메트릭 (home_api_duration 등)
+
+### 7.2 JSON에서 핵심 지표 추출
+
+```bash
+# p95, p99, avg 추출
+cat results/scenario1-indexing/before-index-home-api-*-summary.json | jq '{
+ avg: .metrics.http_req_duration.values.avg,
+ p95: .metrics.http_req_duration.values["p(95)"],
+ p99: .metrics.http_req_duration.values["p(99)"],
+ total_requests: .metrics.http_reqs.values.count,
+ error_rate: .metrics.http_req_failed.values.rate
+}'
+```
+
+### 7.3 Before/After 비교 예시
+
+```bash
+echo "=== Before Index ===" && \
+cat results/scenario1-indexing/before-index-home-api-*-summary.json | \
+ jq '.metrics.http_req_duration.values | {avg, med, "p(95)", "p(99)"}'
+
+echo "=== After Index ===" && \
+cat results/scenario1-indexing/after-index-home-api-*-summary.json | \
+ jq '.metrics.http_req_duration.values | {avg, med, "p(95)", "p(99)"}'
+```
+
+---
+
+## 8. 환경변수 레퍼런스
+
+### run-scenario.sh 환경변수
+
+| 변수 | 기본값 | 설명 |
+|--------------|------------------------|----------------|
+| `DB_HOST` | localhost | PostgreSQL 호스트 |
+| `DB_PORT` | 5432 | PostgreSQL 포트 |
+| `DB_NAME` | privideo | DB 이름 |
+| `DB_USER` | postgres | DB 사용자 |
+| `PGPASSWORD` | 1234 | DB 비밀번호 |
+| `REDIS_HOST` | localhost | Redis 호스트 |
+| `REDIS_PORT` | 6379 | Redis 포트 |
+| `BASE_URL` | https://localhost:8080 | API 서버 URL |
+
+### k6 테스트 환경변수
+
+| 변수 | 기본값 | 설명 |
+|-----------------|------------------|------------|
+| `EMAIL` | test@example.com | 로그인 이메일 |
+| `PASSWORD` | password123 | 로그인 비밀번호 |
+| `ORG_ID` | 1 | 테스트 조직 ID |
+| `MEMBER_ID` | 1 | 테스트 멤버 ID |
+| `VIDEO_ID` | 1 | 테스트 비디오 ID |
+| `RESULT_DIR` | results | 결과 저장 디렉토리 |
+| `RESULT_PREFIX` | (테스트별 자동) | 결과 파일 접두사 |
+
+### 서버 재시작용 환경변수
+
+| 변수 | 용도 |
+|----------------------------------------|------------------|
+| `SPRING_PROFILES_ACTIVE=local` | 기본 로컬 실행 (캐시 ON) |
+| `SPRING_PROFILES_ACTIVE=local,nocache` | 캐시 비활성화 |
+| `HIKARI_MAX_POOL_SIZE=10\|50\|100` | 커넥션풀 크기 변경 |
+
+---
+
+## 9. 모니터링 (테스트 중 병행)
+
+### PostgreSQL
+
+```sql
+-- 현재 활성 연결 수
+SELECT count(*)
+FROM pg_stat_activity
+WHERE state = 'active';
+
+-- 대기 중인 쿼리
+SELECT pid, state, wait_event_type, query
+FROM pg_stat_activity
+WHERE state != 'idle';
+```
+
+### Redis
+
+```bash
+# 실시간 명령어 모니터링
+redis-cli monitor
+
+# 캐시 키 목록 확인
+redis-cli KEYS "home:*"
+redis-cli KEYS "video:*:info"
+```
+
+### HikariCP (Actuator가 활성화된 경우)
+
+```bash
+curl -sk https://localhost:8080/actuator/metrics/hikaricp.connections.active
+curl -sk https://localhost:8080/actuator/metrics/hikaricp.connections.pending
+```
+
+---
+
+## 10. 문제 해결
+
+### SSL 인증서 오류
+
+k6 테스트 시 `--insecure-skip-tls-verify`가 `run-scenario.sh`에 이미 포함되어 있습니다.
+개별 실행 시에는 직접 추가하세요:
+
+```bash
+k6 run --insecure-skip-tls-verify k6-tests/home-api-test.js
+```
+
+### 로그인 실패 (401)
+
+```bash
+# 수동으로 로그인 테스트
+curl -sk -X POST https://localhost:8080/user/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"test@example.com","password":"password123"}'
+```
+
+### PostgreSQL 연결 실패
+
+```bash
+# PGPASSWORD 확인
+export PGPASSWORD=1234
+psql -h localhost -U postgres -d privideo -c "SELECT 1;"
+
+# 사용자 환경에 맞게 DB_USER 등 조정
+DB_USER=seohyun DB_NAME=fisa ./run-scenario.sh 1
+```
+
+### Connection Pool 고갈 (503/504)
+
+이 에러는 시나리오 3에서 **의도적으로 발생**시키는 것입니다.
+pool_size=10일 때 503이 나오고, 50/100에서 줄어드는 것이 정상적인 결과입니다.
+
+### k6-reporter 로드 실패
+
+`handleSummary`에서 사용하는 `benc-uk/k6-reporter`는 URL import 방식입니다.
+첫 실행 시 인터넷 연결이 필요하며, 이후 캐시됩니다.
diff --git a/docs/test/load_test_scenario.md b/docs/test/load_test_scenario.md
new file mode 100644
index 0000000..d8027ff
--- /dev/null
+++ b/docs/test/load_test_scenario.md
@@ -0,0 +1,377 @@
+# 부하 테스트 시나리오
+
+## 1. 테스트 배경
+
+### 1.1 프로젝트 개요
+
+Privideo는 조직 내 비디오 학습 플랫폼으로, 다음과 같은 핵심 기능을 제공합니다:
+
+- 조직별 비디오 관리 및 스트리밍
+- 멤버별 시청 기록 관리
+- 멤버 그룹 기반 접근 권한 제어
+- AI 기반 비디오 요약/피드백/퀴즈 생성
+
+### 1.2 성능 이슈 발생 가능성
+
+ERD 분석 결과, 다음과 같은 성능 병목이 예상됩니다:
+
+| 테이블 | 예상 데이터량 | 병목 원인 |
+|----------------------------|--------------|-----------------------|
+| Video | 조직당 수백~수천 개 | 다중 조인, 필터링, 정렬 |
+| History | 사용자당 수십~수백 개 | 시청 기록 증가에 따른 조회 성능 저하 |
+| Video_Member_Group_Mapping | 비디오당 다수 | 접근 권한 확인을 위한 서브쿼리 |
+| Member_Group_Mapping | 멤버당 다수 | 그룹 기반 필터링 |
+
+### 1.3 테스트 목적
+
+1. **성능 병목 지점 식별**: 실제 부하 상황에서 응답 시간이 느려지는 API 확인
+2. **최적화 효과 검증**: 인덱싱, 캐싱, Connection Pool 최적화의 실제 효과 측정
+3. **시스템 한계 파악**: 동시 사용자 수 증가에 따른 시스템 한계점 확인
+
+---
+
+## 2. 테스트 대상 API
+
+### 2.1 API 1: 홈 조회 API
+
+**엔드포인트**: `GET /{orgId}/home?filter={filter}`
+
+**선정 이유**:
+
+- 서비스의 메인 진입점으로 가장 빈번하게 호출되는 API
+- Video, VideoMemberGroupMapping, MemberGroupMapping 등 다중 테이블 조인
+- 필터(RECENT, POPULAR, RECOMMEND)에 따른 동적 정렬
+- 카테고리 조회를 위한 추가 쿼리 발생
+
+**쿼리 복잡도 분석**:
+
+```
+VideoRepositoryImpl.findHomeVideos()
+├── Video 테이블 조회
+├── LEFT JOIN VideoMemberGroupMapping (접근 권한)
+├── LEFT JOIN MemberGroupMapping (멤버 그룹)
+├── WHERE 조건: organization_id, upload_status, creator.status
+├── GROUP BY: video.id, title, thumbnailKey, ...
+└── ORDER BY: filter에 따라 동적 (created_at / watch_cnt)
+
+VideoRepositoryImpl.findCategoriesForHomeVideos()
+├── Video 테이블 조회
+├── JOIN VideoCategoryMapping
+├── JOIN Category
+├── LEFT JOIN VideoMemberGroupMapping
+└── LEFT JOIN MemberGroupMapping
+```
+
+**예상 병목**:
+
+- 인덱스 부재 시 Full Table Scan
+- 다중 LEFT JOIN으로 인한 카테시안 곱 가능성
+- 서브쿼리(EXISTS)로 인한 추가 오버헤드
+
+---
+
+### 2.2 API 2: 시청 기록 조회 API
+
+**엔드포인트**: `GET /{orgId}/history`
+
+**선정 이유**:
+
+- 사용자별 개인화 데이터 조회
+- History와 Video 조인 + Scrap 서브쿼리
+- `lastWatchedAt` 기준 정렬로 인한 인덱스 필요성
+
+**쿼리 복잡도 분석**:
+
+```
+HistoryRepositoryImpl.findByMemberId()
+├── History 테이블 조회
+├── JOIN Video (비디오 정보)
+├── WHERE: member_id, join_status, upload_status
+├── EXISTS 서브쿼리: Scrap 테이블 (스크랩 여부)
+└── ORDER BY: last_watched_at DESC
+```
+
+**예상 병목**:
+
+- 시청 기록이 많은 사용자의 경우 조회 성능 저하
+- `last_watched_at` 컬럼 인덱스 부재 시 정렬 비용 증가
+- EXISTS 서브쿼리의 반복 실행
+
+---
+
+### 2.3 API 3: 영상 시청 세션 시작 API
+
+**엔드포인트**: `POST /{orgId}/video/{videoId}/join`
+
+**선정 이유**:
+
+- 비디오 시청의 핵심 진입점
+- 다수의 DB 조회 작업이 한 트랜잭션에서 발생
+- Redis 세션 관리와 DB 조회가 결합
+
+**쿼리 복잡도 분석**:
+
+```
+VideoService.prepareJoinVideoSession()
+├── Member 조회: findByIdAndOrganizationIdAndStatus()
+├── Video 조회: findById()
+├── Redis: existsWatchSession() - 세션 중복 확인
+├── MemberGroup: isAccessibleToVideo() - 접근 권한 확인 (복잡한 서브쿼리)
+├── Scrap 조회: existsByMemberIdAndVideoId()
+├── Category 조회: findAllByVideoId()
+├── Quiz 조회: findAllByVideoId() (AI 기능 사용 시)
+└── History 조회/생성: findByMemberIdAndVideoId()
+
+VideoService.openWatchSession()
+├── Video 조회 (중복)
+├── History 조회/생성
+├── Redis: createWatchSession()
+└── LogService: incOrgViewBucket()
+```
+
+**예상 병목**:
+
+- 한 요청에서 다수의 DB 조회 발생 (N+1 문제 가능성)
+- Connection Pool 고갈 위험 (동시 요청 증가 시)
+- Redis와 DB 간 일관성 문제
+
+---
+
+## 3. 테스트 시나리오
+
+### 3.1 시나리오 1: DB 인덱싱 적용 전후 비교
+
+**목적**: 인덱스 추가로 인한 쿼리 성능 개선 효과 측정
+
+**테스트 흐름**:
+
+```
+[인덱스 적용 전]
+ │
+ ├── 홈 조회 API 부하 테스트 (50 VUs, 60초)
+ │ └── 결과 저장: before-indexes-home.json
+ │
+ ├── 시청 기록 조회 API 부하 테스트 (50 VUs, 60초)
+ │ └── 결과 저장: before-indexes-history.json
+ │
+ └── EXPLAIN ANALYZE로 쿼리 실행 계획 저장
+
+[인덱스 추가]
+ │
+ └── scripts/add-indexes.sql 실행
+
+[인덱스 적용 후]
+ │
+ ├── 홈 조회 API 부하 테스트 (동일 조건)
+ │ └── 결과 저장: after-indexes-home.json
+ │
+ ├── 시청 기록 조회 API 부하 테스트 (동일 조건)
+ │ └── 결과 저장: after-indexes-history.json
+ │
+ └── EXPLAIN ANALYZE로 쿼리 실행 계획 비교
+```
+
+**추가 대상 인덱스**:
+
+- History: `(member_id, last_watched_at DESC)`, `(member_id, video_id)`
+- Video: `(organization_id, upload_status, created_at DESC)`
+- VideoMemberGroupMapping: `(video_id)`, `(member_group_id)`
+- MemberGroupMapping: `(member_id, member_group_id)`
+
+**측정 지표**:
+
+| 지표 | 설명 | 목표 |
+|-----------|--------------|-----------|
+| p50 응답 시간 | 중앙값 응답 시간 | 50% 이상 감소 |
+| p95 응답 시간 | 95 백분위 응답 시간 | 50% 이상 감소 |
+| TPS | 초당 처리량 | 2배 이상 증가 |
+| 에러율 | 실패 요청 비율 | 5% 미만 유지 |
+
+---
+
+### 3.2 시나리오 2: Redis 캐시 적용 시 데이터 정합성 검증
+
+**목적**: Redis 캐시 적용 후 성능 개선 및 데이터 정합성 확인
+
+**캐시 전략**:
+
+| 캐시 키 | 데이터 | TTL | 무효화 조건 |
+|-------------------------|-----------|-----|--------------|
+| `home:{orgId}:{filter}` | 비디오 목록 | 5분 | 비디오 생성/수정/삭제 |
+| `video:{videoId}:info` | 비디오 메타데이터 | 10분 | 비디오 수정/삭제 |
+
+**테스트 흐름**:
+
+```
+[캐시 적용 전]
+ │
+ └── 홈 조회 API 부하 테스트
+ └── 결과 저장: before-cache-home.json
+
+[캐시 적용 후]
+ │
+ ├── 홈 조회 API 부하 테스트
+ │ └── 결과 저장: with-cache-home.json
+ │
+ └── 데이터 정합성 검증
+ ├── 1. 캐시 히트 확인 (Redis 로그)
+ ├── 2. 비디오 수정 후 캐시 무효화 확인
+ └── 3. 캐시와 DB 데이터 일치 확인
+```
+
+**정합성 검증 시나리오**:
+
+1. 홈 조회 → 캐시 저장 확인
+2. 비디오 수정 → 캐시 무효화 확인
+3. 홈 재조회 → 최신 데이터 반환 확인
+
+**측정 지표**:
+
+| 지표 | 설명 | 목표 |
+|-------------------|---------------|---------|
+| 캐시 히트율 | 캐시에서 응답한 비율 | 80% 이상 |
+| p50 응답 시간 (캐시 히트) | 캐시 히트 시 응답 시간 | 10ms 미만 |
+| 데이터 정합성 | 캐시와 DB 일치율 | 100% |
+
+---
+
+### 3.3 시나리오 3: Connection Pool 설정 최적화
+
+**목적**: 동시 접속자 증가에 따른 Connection Pool 최적 설정 도출
+
+**테스트 흐름**:
+
+```
+[기본 설정 테스트]
+ │
+ ├── HikariCP 기본값: maximum-pool-size=10
+ └── 영상 세션 시작 API 부하 테스트 (점진적 부하 증가)
+ ├── 10 VUs → 50 VUs → 100 VUs → 150 VUs
+ └── 503/504 에러 발생 지점 확인
+
+[최적화 설정 테스트]
+ │
+ ├── HikariCP 최적화: maximum-pool-size=50
+ └── 동일 부하 테스트 재실행
+ └── 에러 발생 지점 비교
+
+[추가 최적화 테스트]
+ │
+ ├── HikariCP 추가 최적화: maximum-pool-size=100
+ └── 동일 부하 테스트 재실행
+ └── 최적 설정 도출
+```
+
+**Connection Pool 설정 옵션**:
+
+| 설정 | 기본값 | 테스트 값 1 | 테스트 값 2 |
+|--------------------|----------|----------|----------|
+| maximum-pool-size | 10 | 50 | 100 |
+| minimum-idle | 10 | 10 | 20 |
+| connection-timeout | 30000ms | 30000ms | 30000ms |
+| idle-timeout | 600000ms | 600000ms | 300000ms |
+
+**측정 지표**:
+
+| 지표 | 설명 | 목표 |
+|------------------|-----------------------|------------|
+| 503 에러 발생 VUs | Connection Pool 고갈 시점 | 100 VUs 이상 |
+| Connection 대기 시간 | Pool에서 연결 획득 대기 시간 | 100ms 미만 |
+| 최대 동시 처리량 | 에러 없이 처리 가능한 최대 VUs | 100 VUs 이상 |
+
+---
+
+## 4. 테스트 결과에 따른 개선 방향
+
+### 4.1 인덱싱 개선
+
+**예상 결과**:
+
+- 응답 시간 50-70% 감소
+- Full Table Scan → Index Scan으로 변경
+
+**개선 방향**:
+
+| 결과 | 조치 |
+|-----------|---------------------------|
+| 응답 시간 개선됨 | 인덱스 유지, 복합 인덱스 추가 검토 |
+| 개선 미미 | 쿼리 최적화, Covering Index 적용 |
+| 특정 쿼리만 개선 | 문제 쿼리 식별 후 개별 최적화 |
+
+**추가 최적화 고려사항**:
+
+- Partial Index 적용 (WHERE 조건 포함)
+- Covering Index로 추가 테이블 접근 제거
+- 쿼리 리팩토링 (서브쿼리 → JOIN 변환)
+
+---
+
+### 4.2 캐시 개선
+
+**예상 결과**:
+
+- 캐시 히트 시 응답 시간 80-90% 감소
+- DB 부하 50% 이상 감소
+
+**개선 방향**:
+
+| 결과 | 조치 |
+|-----------|----------------------|
+| 캐시 히트율 높음 | TTL 조정, 캐시 범위 확대 |
+| 정합성 문제 발생 | 캐시 무효화 로직 강화, TTL 단축 |
+| 캐시 미스 빈번 | 캐시 키 전략 재검토, 프리워밍 적용 |
+
+**추가 최적화 고려사항**:
+
+- Write-Through 캐시 패턴 적용
+- 캐시 계층화 (L1: 로컬 캐시, L2: Redis)
+- 캐시 프리워밍 (서버 시작 시 주요 데이터 캐싱)
+
+---
+
+### 4.3 Connection Pool 개선
+
+**예상 결과**:
+
+- 동시 처리량 2-3배 증가
+- 503 에러 발생 지점 상향
+
+**개선 방향**:
+
+| 결과 | 조치 |
+|---------------------|--------------------------------|
+| Pool 크기 증가 효과 있음 | 최적 Pool 크기 설정 적용 |
+| Pool 크기 증가해도 개선 안 됨 | 쿼리 최적화, 비동기 처리 도입 |
+| DB 연결 한계 도달 | Read Replica 도입, 커넥션 풀링 서비스 도입 |
+
+**추가 최적화 고려사항**:
+
+- PostgreSQL max_connections 설정 확인
+- PgBouncer 등 외부 Connection Pooler 도입
+- 읽기/쓰기 분리 (Read Replica)
+
+---
+
+## 5. 결론
+
+### 5.1 테스트 우선순위
+
+1. **인덱싱**: 가장 기본적이고 효과적인 최적화
+2. **Connection Pool**: 동시 접속자 증가 대응
+3. **Redis 캐시**: 읽기 성능 극대화 및 DB 부하 분산
+
+### 5.2 기대 효과
+
+| 최적화 | 기대 효과 |
+|-----------------|----------------------------------------|
+| 인덱싱 | 쿼리 응답 시간 50-70% 감소 |
+| Redis 캐시 | 캐시 히트 시 응답 시간 80-90% 감소 |
+| Connection Pool | 동시 처리량 2-3배 증가 |
+| **종합** | **p95 응답 시간 500ms 미만, 100+ VUs 동시 처리** |
+
+### 5.3 향후 계획
+
+1. 테스트 결과 기반 최적 설정 도출
+2. 프로덕션 환경 적용 전 스테이징 환경에서 재검증
+3. 모니터링 시스템 구축 (응답 시간, 에러율, DB 성능)
+4. 정기적인 성능 테스트 자동화
diff --git a/k6-tests/history-api-test.js b/k6-tests/history-api-test.js
new file mode 100644
index 0000000..8e36c36
--- /dev/null
+++ b/k6-tests/history-api-test.js
@@ -0,0 +1,127 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+import { Rate, Trend, Counter } from 'k6/metrics';
+import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
+import { textSummary } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';
+import { config } from './shared/config.js';
+import { login, getAuthHeaders } from './shared/auth.js';
+
+// 커스텀 메트릭
+const errorRate = new Rate('errors');
+const historyApiDuration = new Trend('history_api_duration');
+const requestCounter = new Counter('total_requests');
+
+// 테스트 설정
+export const options = {
+ stages: config.loadTest.stages,
+ thresholds: {
+ 'http_req_duration': ['p(95)<1500', 'p(99)<3000'], // 95%는 1.5초 이하, 99%는 3초 이하
+ 'http_req_failed': ['rate<0.05'], // 에러율 5% 미만
+ 'errors': ['rate<0.05'],
+ },
+};
+
+// 테스트 데이터
+const testData = config.testData;
+
+// 테스트 실행 전 초기화
+export function setup() {
+ console.log('=== 시청 기록 조회 API 부하 테스트 시작 ===');
+ console.log(`Base URL: ${config.baseUrl}`);
+ console.log(`Org ID: ${testData.orgId}`);
+ console.log(`Member ID: ${testData.memberId}`);
+ console.log('⚠️ 로컬 테스트: 외부 API는 사용하지 않습니다.');
+
+ // 로그인하여 토큰 발급
+ const token = login(config.baseUrl, testData.email, testData.password, testData.orgId);
+ if (!token) {
+ console.error('로그인 실패 - 테스트를 중단합니다.');
+ return null;
+ }
+
+ console.log('로그인 성공 - 토큰 발급 완료');
+ return { token };
+}
+
+// 각 VU가 실행하는 메인 함수
+export default function (data) {
+ const token = data ? data.token : null;
+
+ if (!token) {
+ console.error('토큰이 없습니다. 테스트를 건너뜁니다.');
+ return;
+ }
+
+ // 시청 기록 조회 API 호출
+ // 실제 엔드포인트는 프로젝트 구조에 따라 조정 필요
+ const url = `${config.baseUrl}/${testData.orgId}/myactivity/video`;
+
+ const params = {
+ headers: {
+ ...config.http.headers,
+ ...getAuthHeaders(token),
+ },
+ tags: {
+ name: 'History API',
+ },
+ };
+
+ const startTime = Date.now();
+ const response = http.get(url, params);
+ const duration = Date.now() - startTime;
+
+ // 메트릭 업데이트
+ requestCounter.add(1);
+ historyApiDuration.add(duration);
+ errorRate.add(response.status >= 400);
+
+ // 응답 검증
+ const success = check(response, {
+ '시청 기록 조회 API 상태 코드 200': (r) => r.status === 200,
+ '시청 기록 조회 API 응답 시간 < 1.5초': (r) => r.timings.duration < 1500,
+ '시청 기록 조회 API 응답 본문 존재': (r) => r.body && r.body.length > 0,
+ '시청 기록 조회 API JSON 파싱 가능': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body && body.data !== undefined;
+ } catch (e) {
+ return false;
+ }
+ },
+ '시청 기록 목록 반환': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.data && Array.isArray(body.data.histories || body.data);
+ } catch (e) {
+ return false;
+ }
+ },
+ });
+
+ if (!success) {
+ console.error(`시청 기록 조회 API 실패: ${response.status} - ${response.body.substring(0, 200)}`);
+ }
+
+ // 요청 간 대기 시간
+ sleep(Math.random() * 2 + 1); // 1-3초 사이 랜덤 대기
+}
+
+// 테스트 종료 후 실행
+export function teardown(data) {
+ console.log('=== 시청 기록 조회 API 부하 테스트 종료 ===');
+ if (data) {
+ console.log('테스트 완료');
+ }
+}
+
+// 결과 리포트 생성
+export function handleSummary(data) {
+ const resultDir = __ENV.RESULT_DIR || 'results';
+ const prefix = __ENV.RESULT_PREFIX || 'history-api';
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
+ return {
+ [`${resultDir}/${prefix}-${ts}.html`]: htmlReport(data, { title: '시청 기록 조회 API 부하 테스트 리포트' }),
+ [`${resultDir}/${prefix}-${ts}-summary.json`]: JSON.stringify(data, null, 2),
+ stdout: textSummary(data, { indent: ' ', enableColors: true }),
+ };
+}
diff --git a/k6-tests/home-api-test.js b/k6-tests/home-api-test.js
new file mode 100644
index 0000000..1712155
--- /dev/null
+++ b/k6-tests/home-api-test.js
@@ -0,0 +1,120 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+import { Rate, Trend, Counter } from 'k6/metrics';
+import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
+import { textSummary } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';
+import { config } from './shared/config.js';
+import { login, getAuthHeaders, getToken, setToken } from './shared/auth.js';
+
+// 커스텀 메트릭
+const errorRate = new Rate('errors');
+const homeApiDuration = new Trend('home_api_duration');
+const requestCounter = new Counter('total_requests');
+
+// 테스트 설정
+export const options = {
+ stages: config.loadTest.stages,
+ thresholds: {
+ 'http_req_duration': ['p(95)<2000', 'p(99)<5000'], // 95%는 2초 이하, 99%는 5초 이하
+ 'http_req_failed': ['rate<0.05'], // 에러율 5% 미만
+ 'errors': ['rate<0.05'],
+ },
+};
+
+// 테스트 데이터
+const testData = config.testData;
+
+// 테스트 실행 전 초기화 (한 번만 실행)
+export function setup() {
+ console.log('=== 홈 조회 API 부하 테스트 시작 ===');
+ console.log(`Base URL: ${config.baseUrl}`);
+ console.log(`Org ID: ${testData.orgId}`);
+ console.log('⚠️ 로컬 테스트: AWS S3/Gemini AI는 사용하지 않습니다.');
+
+ // 로그인하여 토큰 발급
+ const token = login(config.baseUrl, testData.email, testData.password, testData.orgId);
+ if (!token) {
+ console.error('로그인 실패 - 테스트를 중단합니다.');
+ return null;
+ }
+
+ console.log('로그인 성공 - 토큰 발급 완료');
+ return { token };
+}
+
+// 각 VU가 실행하는 메인 함수
+export default function (data) {
+ const token = data ? data.token : null;
+
+ if (!token) {
+ console.error('토큰이 없습니다. 테스트를 건너뜁니다.');
+ return;
+ }
+
+ // 홈 조회 API 호출
+ const filters = ['RECENT', 'POPULAR', 'RECOMMEND'];
+ const filter = filters[Math.floor(Math.random() * filters.length)];
+ const url = `${config.baseUrl}/${testData.orgId}/home?filter=${filter}`;
+
+ const params = {
+ headers: {
+ ...config.http.headers,
+ ...getAuthHeaders(token),
+ },
+ tags: {
+ name: 'Home API',
+ filter: filter,
+ },
+ };
+
+ const startTime = Date.now();
+ const response = http.get(url, params);
+ const duration = Date.now() - startTime;
+
+ // 메트릭 업데이트
+ requestCounter.add(1);
+ homeApiDuration.add(duration);
+ errorRate.add(response.status >= 400);
+
+ // 응답 검증
+ const success = check(response, {
+ '홈 조회 API 상태 코드 200': (r) => r.status === 200,
+ '홈 조회 API 응답 시간 < 2초': (r) => r.timings.duration < 2000,
+ '홈 조회 API 응답 본문 존재': (r) => r.body && r.body.length > 0,
+ '홈 조회 API JSON 파싱 가능': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body && body.data !== undefined;
+ } catch (e) {
+ return false;
+ }
+ },
+ });
+
+ if (!success) {
+ console.error(`홈 조회 API 실패: ${response.status} - ${response.body.substring(0, 200)}`);
+ }
+
+ // 요청 간 대기 시간 (실제 사용자 행동 시뮬레이션)
+ sleep(Math.random() * 2 + 1); // 1-3초 사이 랜덤 대기
+}
+
+// 테스트 종료 후 실행 (요약 정보 출력)
+export function teardown(data) {
+ console.log('=== 홈 조회 API 부하 테스트 종료 ===');
+ if (data) {
+ console.log('테스트 완료');
+ }
+}
+
+// 결과 리포트 생성
+export function handleSummary(data) {
+ const resultDir = __ENV.RESULT_DIR || 'results';
+ const prefix = __ENV.RESULT_PREFIX || 'home-api';
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
+ return {
+ [`${resultDir}/${prefix}-${ts}.html`]: htmlReport(data, { title: '홈 조회 API 부하 테스트 리포트' }),
+ [`${resultDir}/${prefix}-${ts}-summary.json`]: JSON.stringify(data, null, 2),
+ stdout: textSummary(data, { indent: ' ', enableColors: true }),
+ };
+}
diff --git a/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39-summary.json b/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39-summary.json
new file mode 100644
index 0000000..189c925
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39-summary.json
@@ -0,0 +1,290 @@
+{
+ "state": {
+ "testRunDurationMs": 112268.415,
+ "isStdOutTTY": false,
+ "isStdErrTTY": false
+ },
+ "metrics": {
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3371,
+ "rate": 30.026254490187643
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 2,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "iteration_duration": {
+ "contains": "time",
+ "values": {
+ "p(90)": 2810.3080751999996,
+ "p(95)": 2908.4858329999997,
+ "avg": 2015.6012612920804,
+ "min": 1009.2665,
+ "med": 2015.881208,
+ "max": 3025.655209
+ },
+ "type": "trend"
+ },
+ "http_req_duration": {
+ "values": {
+ "p(90)": 10.933,
+ "p(95)": 16.011,
+ "avg": 8.161517057253054,
+ "min": 4.523,
+ "med": 7.291,
+ "max": 162.458
+ },
+ "thresholds": {
+ "p(99)<3000": {
+ "ok": true
+ },
+ "p(95)<1500": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_waiting": {
+ "contains": "time",
+ "values": {
+ "avg": 8.086907742509629,
+ "min": 4.494,
+ "med": 7.225,
+ "max": 162.265,
+ "p(90)": 10.83,
+ "p(95)": 15.881999999999985
+ },
+ "type": "trend"
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "avg": 8.654496883348175,
+ "min": 4,
+ "med": 7,
+ "max": 56,
+ "p(90)": 13,
+ "p(95)": 20
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 30.00844004077193,
+ "count": 3369
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.008682290121625632,
+ "min": 0,
+ "med": 0,
+ "max": 0.574,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.6,
+ "passes": 10107,
+ "fails": 6738
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3369
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.014775437555621702,
+ "min": 0.005,
+ "med": 0.012,
+ "max": 2.01,
+ "p(90)": 0.021,
+ "p(95)": 0.026
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 4.523,
+ "med": 7.291,
+ "max": 162.458,
+ "p(90)": 10.933,
+ "p(95)": 16.011,
+ "avg": 8.161517057253054
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 37457761,
+ "rate": 333644.6942802212
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3369,
+ "rate": 30.00844004077193
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3371
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.01049999999999985,
+ "avg": 0.4726561851082455,
+ "min": 0.001,
+ "med": 0.003,
+ "max": 30.152,
+ "p(90)": 0.007
+ }
+ },
+ "http_req_tls_handshaking": {
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 29.609,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.45772471076831817,
+ "min": 0
+ },
+ "type": "trend"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.047,
+ "max": 3.44,
+ "p(90)": 0.092,
+ "p(95)": 0.115,
+ "avg": 0.05983387718777804,
+ "min": 0.018
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1646158,
+ "rate": 14662.699210637293
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3NDg0NywiZXhwIjoxNzc1MjY2ODQ3fQ.Lo9EbSNlYHRn84G5s8f5CtqmxaKhyp1xgfnPGLONN1A"
+ },
+ "root_group": {
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3369,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초",
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3369,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3369,
+ "fails": 0
+ },
+ {
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3369,
+ "name": "시청 기록 조회 API JSON 파싱 가능",
+ "path": "::시청 기록 조회 API JSON 파싱 가능"
+ },
+ {
+ "passes": 0,
+ "fails": 3369,
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e"
+ }
+ ],
+ "name": ""
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39.html b/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39.html
new file mode 100644
index 0000000..8521abc
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-history-api-2026-03-05T01-42-39.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3371
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6738
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 8.65 |
+
+ 4.00 |
+
+ 7.00 |
+
+ 56.00 |
+
+ 13.00 |
+
+ 20.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.47 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 30.15 |
+
+ 0.01 |
+
+ 0.01 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.57 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 8.16 |
+
+ 4.52 |
+
+ 7.29 |
+
+ 162.46 |
+
+ 10.93 |
+
+ 16.01 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.06 |
+
+ 0.02 |
+
+ 0.05 |
+
+ 3.44 |
+
+ 0.09 |
+
+ 0.12 |
+
+
+
+
+ | http_req_sending |
+
+ 0.01 |
+
+ 0.01 |
+
+ 0.01 |
+
+ 2.01 |
+
+ 0.02 |
+
+ 0.03 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.46 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 29.61 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 8.09 |
+
+ 4.49 |
+
+ 7.22 |
+
+ 162.26 |
+
+ 10.83 |
+
+ 15.88 |
+
+
+
+
+ | iteration_duration |
+
+ 2015.60 |
+
+ 1009.27 |
+
+ 2015.88 |
+
+ 3025.66 |
+
+ 2810.31 |
+
+ 2908.49 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3369.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3371.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3369.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10107
+
+
+ Failed
+ 6738
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3369
+
+
+ Rate
+ 30.01/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3371
+
+
+
+
+ Rate
+
+ 30.03/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 37.46 MB
+
+
+ Rate
+ 0.33 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.65 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3369 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3369 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3369 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3369 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3369 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34-summary.json b/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34-summary.json
new file mode 100644
index 0000000..a9615a7
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34-summary.json
@@ -0,0 +1,283 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "fails": 0,
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3339
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3339,
+ "fails": 0
+ },
+ {
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3339,
+ "fails": 0,
+ "name": "홈 조회 API 응답 본문 존재"
+ },
+ {
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3339
+ }
+ ]
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "testRunDurationMs": 112225.813,
+ "isStdOutTTY": false,
+ "isStdErrTTY": false
+ },
+ "metrics": {
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 5.054,
+ "p(95)": 5.309,
+ "avg": 4.9313172702783685,
+ "min": 0.074,
+ "med": 4.835,
+ "max": 20.148
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3339,
+ "rate": 29.75251335448111
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(95)": 26,
+ "avg": 15.383048817011082,
+ "min": 12,
+ "med": 14,
+ "max": 77,
+ "p(90)": 20
+ }
+ },
+ "http_req_failed": {
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3341
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate"
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 492595633,
+ "rate": 4389325.591252344
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3341,
+ "rate": 29.77033456643348
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 21.504,
+ "avg": 14.88943070936847,
+ "min": 9.563,
+ "med": 13.866,
+ "max": 165.279,
+ "p(90)": 17.678
+ }
+ },
+ "http_req_blocked": {
+ "values": {
+ "max": 32.181,
+ "p(90)": 0.007,
+ "p(95)": 0.009,
+ "avg": 0.4719341514516332,
+ "min": 0.001,
+ "med": 0.004
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2033.8890951913743,
+ "min": 1021.6175,
+ "med": 2044.009417,
+ "max": 3037.423667,
+ "p(90)": 2842.4304164,
+ "p(95)": 2926.1533704999997
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.4551984435797664,
+ "min": 0,
+ "med": 0,
+ "max": 31.875,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 3,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3339
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.941491170308293,
+ "min": 7.586,
+ "med": 8.983,
+ "max": 165.058,
+ "p(90)": 12.458,
+ "p(95)": 16.1
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.75,
+ "passes": 10017,
+ "fails": 3339
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.010096677641424724,
+ "min": 0,
+ "med": 0,
+ "max": 2.767,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 165.279,
+ "p(90)": 17.678,
+ "p(95)": 21.504,
+ "avg": 14.88943070936847,
+ "min": 9.563,
+ "med": 13.866
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.012,
+ "max": 7.512,
+ "p(90)": 0.022,
+ "p(95)": 0.026,
+ "avg": 0.016622268781802042,
+ "min": 0.004
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3339,
+ "rate": 29.75251335448111
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1644026,
+ "rate": 14649.267900603223
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3NDcyMiwiZXhwIjoxNzc1MjY2NzIyfQ.GQfaIUT7zl6CJA0FsjU_Mc64s_EemNGflE42YhX6HeE"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34.html b/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34.html
new file mode 100644
index 0000000..c5e94a1
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-home-api-2026-03-05T01-40-34.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3341
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3339
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 15.38 |
+
+ 12.00 |
+
+ 14.00 |
+
+ 77.00 |
+
+ 20.00 |
+
+ 26.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.47 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 32.18 |
+
+ 0.01 |
+
+ 0.01 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 2.77 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 14.89 |
+
+ 9.56 |
+
+ 13.87 |
+
+ 165.28 |
+
+ 17.68 |
+
+ 21.50 |
+
+
+
+
+ | http_req_receiving |
+
+ 4.93 |
+
+ 0.07 |
+
+ 4.83 |
+
+ 20.15 |
+
+ 5.05 |
+
+ 5.31 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 7.51 |
+
+ 0.02 |
+
+ 0.03 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.46 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 31.88 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 9.94 |
+
+ 7.59 |
+
+ 8.98 |
+
+ 165.06 |
+
+ 12.46 |
+
+ 16.10 |
+
+
+
+
+ | iteration_duration |
+
+ 2033.89 |
+
+ 1021.62 |
+
+ 2044.01 |
+
+ 3037.42 |
+
+ 2842.43 |
+
+ 2926.15 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3339.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3341.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3339.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10017
+
+
+ Failed
+ 3339
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3339
+
+
+ Rate
+ 29.75/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3341
+
+
+
+
+ Rate
+
+ 29.77/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 492.60 MB
+
+
+ Rate
+ 4.39 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3339 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3339 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3339 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3339 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11-summary.json b/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11-summary.json
new file mode 100644
index 0000000..1578950
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11-summary.json
@@ -0,0 +1,299 @@
+{
+ "root_group": {
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7",
+ "passes": 1,
+ "fails": 5311
+ },
+ {
+ "id": "38fdc37afba80c32cedb5248f693c40c",
+ "passes": 5312,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초"
+ },
+ {
+ "passes": 5312,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d"
+ },
+ {
+ "passes": 0,
+ "fails": 5312,
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945"
+ }
+ ],
+ "name": "",
+ "path": ""
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "testRunDurationMs": 203850.816,
+ "isStdOutTTY": false,
+ "isStdErrTTY": false
+ },
+ "metrics": {
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5312,
+ "rate": 26.05827194726559
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "max": 150,
+ "value": 150,
+ "min": 150
+ }
+ },
+ "data_received": {
+ "contains": "data",
+ "values": {
+ "count": 3398344,
+ "rate": 16670.7402338777
+ },
+ "type": "counter"
+ },
+ "connection_pool_errors": {
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ },
+ "type": "counter"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 4699.156829099999,
+ "p(95)": 4857.8927814,
+ "avg": 3514.4948272149863,
+ "min": 2003.406167,
+ "med": 3521.5719790000003,
+ "max": 5226.729417
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.08925818592397425,
+ "min": 0.015,
+ "med": 0.058,
+ "max": 15.7,
+ "p(90)": 0.12,
+ "p(95)": 0.16234999999999977
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.6966974030861872,
+ "min": 0,
+ "med": 0,
+ "max": 259.425,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5312,
+ "rate": 26.05827194726559
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2677787,
+ "rate": 13136.01315189241
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9994354535190064,
+ "passes": 5311,
+ "fails": 3
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 4,
+ "min": 2,
+ "max": 150
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.7278042905532655,
+ "min": 0.001,
+ "med": 0.005,
+ "max": 261.763,
+ "p(90)": 0.016,
+ "p(95)": 0.03
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9998117469879518,
+ "passes": 5311,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 22.385,
+ "med": 99.112,
+ "max": 165.793,
+ "p(90)": 152.45680000000002,
+ "p(95)": 159.1249,
+ "avg": 95.76333333333332
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.014165412118931126,
+ "min": 0,
+ "med": 0,
+ "max": 17.554,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 26.06808304363128,
+ "count": 5314
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 8.530101242002276,
+ "min": 2.532,
+ "med": 4.2445,
+ "max": 346.856,
+ "p(90)": 11.659200000000006,
+ "p(95)": 21.67274999999999
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.031023146405720396,
+ "min": 0.005,
+ "med": 0.016,
+ "max": 9.86,
+ "p(90)": 0.033,
+ "p(95)": 0.046
+ }
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(90)": 16,
+ "p(95)": 25,
+ "avg": 9.455384036144578,
+ "min": 2,
+ "med": 4,
+ "max": 347
+ }
+ },
+ "http_req_duration": {
+ "values": {
+ "med": 4.3195,
+ "max": 347.047,
+ "p(90)": 11.8595,
+ "p(95)": 22.015649999999994,
+ "avg": 8.65038257433201,
+ "min": 2.593
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.5000470632530121,
+ "passes": 10625,
+ "fails": 10623
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3NDk2OCwiZXhwIjoxNzc1MjY2OTY4fQ.xbMcjKu7yMr_J4HIh9WPKRlfLyv9oTmzgbX_JC_AppA",
+ "videoIds": [
+ "151"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11.html b/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11.html
new file mode 100644
index 0000000..f157ff5
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/after-index-video-join-api-2026-03-05T01-46-11.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5314
+
+
+
+
+
+
+
+
Failed Requests
+
5311
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10623
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.73 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 261.76 |
+
+ 0.02 |
+
+ 0.03 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 17.55 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 8.65 |
+
+ 2.59 |
+
+ 4.32 |
+
+ 347.05 |
+
+ 11.86 |
+
+ 22.02 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.09 |
+
+ 0.01 |
+
+ 0.06 |
+
+ 15.70 |
+
+ 0.12 |
+
+ 0.16 |
+
+
+
+
+ | http_req_sending |
+
+ 0.03 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 9.86 |
+
+ 0.03 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.70 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 259.43 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 8.53 |
+
+ 2.53 |
+
+ 4.24 |
+
+ 346.86 |
+
+ 11.66 |
+
+ 21.67 |
+
+
+
+
+ | iteration_duration |
+
+ 3514.49 |
+
+ 2003.41 |
+
+ 3521.57 |
+
+ 5226.73 |
+
+ 4699.16 |
+
+ 4857.89 |
+
+
+
+
+ | video_join_api_duration |
+
+ 9.46 |
+
+ 2.00 |
+
+ 4.00 |
+
+ 347.00 |
+
+ 16.00 |
+
+ 25.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5311.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5311.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5312.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10625
+
+
+ Failed
+ 10623
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5312
+
+
+ Rate
+ 26.06/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 2
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5314
+
+
+
+
+ Rate
+
+ 26.07/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.40 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.68 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5311 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5312 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5312 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5312 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41-summary.json b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41-summary.json
new file mode 100644
index 0000000..2d645d7
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41-summary.json
@@ -0,0 +1,290 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112454.378
+ },
+ "metrics": {
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2126217,
+ "rate": 18907.37415309878
+ }
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "avg": 16.29171632896305,
+ "min": 3,
+ "med": 8,
+ "max": 165,
+ "p(90)": 39,
+ "p(95)": 58
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "http_req_failed": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": false
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9994044073853484,
+ "passes": 3356,
+ "fails": 2
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.019423764145324596,
+ "min": 0,
+ "med": 0,
+ "max": 5.062,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_tls_handshaking": {
+ "contains": "time",
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 103.424,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.9615077427039906
+ },
+ "type": "trend"
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3356,
+ "rate": 29.84321339628058
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 197.803,
+ "p(90)": 0.019,
+ "p(95)": 0.04614999999999985,
+ "avg": 1.055175402025011,
+ "min": 0.002,
+ "med": 0.008
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0.006,
+ "med": 0.026,
+ "max": 34.862,
+ "p(90)": 0.053,
+ "p(95)": 0.079,
+ "avg": 0.0635795116140554
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 35.8106,
+ "p(95)": 55.24259999999999,
+ "avg": 15.192522036926762,
+ "min": 2.768,
+ "med": 7.226,
+ "max": 296.089
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ },
+ "data_sent": {
+ "values": {
+ "count": 1610221,
+ "rate": 14318.882275975062
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 281.1974,
+ "p(95)": 288.6432,
+ "avg": 221.631,
+ "min": 147.173,
+ "med": 221.631,
+ "max": 296.089
+ }
+ },
+ "http_req_receiving": {
+ "values": {
+ "p(90)": 0.215,
+ "p(95)": 0.302,
+ "avg": 0.17787016081000617,
+ "min": 0.017,
+ "med": 0.105,
+ "max": 16.774
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 6712,
+ "fails": 10068,
+ "rate": 0.4
+ }
+ },
+ "errors": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": false
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 1,
+ "passes": 3356,
+ "fails": 0
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 54.99284999999998,
+ "avg": 14.951072364502691,
+ "min": 2.733,
+ "med": 7.057,
+ "max": 295.736,
+ "p(90)": 35.38120000000001
+ }
+ },
+ "total_requests": {
+ "values": {
+ "count": 3356,
+ "rate": 29.84321339628058
+ },
+ "type": "counter",
+ "contains": "default"
+ },
+ "http_reqs": {
+ "values": {
+ "count": 3358,
+ "rate": 29.86099838638563
+ },
+ "type": "counter",
+ "contains": "default"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2024.5002274758606,
+ "min": 1008.466834,
+ "med": 2024.0069375,
+ "max": 3147.262292,
+ "p(90)": 2822.1491875,
+ "p(95)": 2915.8580835
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3Mzk1MCwiZXhwIjoxNzc1MjY1OTUwfQ.y_zTMpEdjRuuBmf5CHISZh1xyCH4eYUgBVUJEFy2n8Y"
+ },
+ "root_group": {
+ "groups": [],
+ "checks": [
+ {
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 0,
+ "fails": 3356,
+ "name": "시청 기록 조회 API 상태 코드 200"
+ },
+ {
+ "passes": 3356,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초",
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d"
+ },
+ {
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3356,
+ "fails": 0
+ },
+ {
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3356,
+ "name": "시청 기록 조회 API JSON 파싱 가능"
+ },
+ {
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e",
+ "passes": 0,
+ "fails": 3356
+ }
+ ],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41.html b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41.html
new file mode 100644
index 0000000..86ade90
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-27-41.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3358
+
+
+
+
+
+
+
+
Failed Requests
+
3356
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10068
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 16.29 |
+
+ 3.00 |
+
+ 8.00 |
+
+ 165.00 |
+
+ 39.00 |
+
+ 58.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 1.06 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 197.80 |
+
+ 0.02 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 5.06 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 15.19 |
+
+ 2.77 |
+
+ 7.23 |
+
+ 296.09 |
+
+ 35.81 |
+
+ 55.24 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.18 |
+
+ 0.02 |
+
+ 0.10 |
+
+ 16.77 |
+
+ 0.21 |
+
+ 0.30 |
+
+
+
+
+ | http_req_sending |
+
+ 0.06 |
+
+ 0.01 |
+
+ 0.03 |
+
+ 34.86 |
+
+ 0.05 |
+
+ 0.08 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.96 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 103.42 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 14.95 |
+
+ 2.73 |
+
+ 7.06 |
+
+ 295.74 |
+
+ 35.38 |
+
+ 54.99 |
+
+
+
+
+ | iteration_duration |
+
+ 2024.50 |
+
+ 1008.47 |
+
+ 2024.01 |
+
+ 3147.26 |
+
+ 2822.15 |
+
+ 2915.86 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 100.00% |
+ 3356.00 |
+ 0.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 2.00 |
+ 3356.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3356.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 6712
+
+
+ Failed
+ 10068
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3356
+
+
+ Rate
+ 29.84/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3358
+
+
+
+
+ Rate
+
+ 29.86/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 2.13 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.61 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 0 |
+ 3356 |
+ 0.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3356 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3356 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3356 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3356 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47-summary.json b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47-summary.json
new file mode 100644
index 0000000..274c0b5
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47-summary.json
@@ -0,0 +1,290 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 113250.108
+ },
+ "metrics": {
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3299
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3301,
+ "rate": 29.1478750731081
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 21.149,
+ "p(90)": 0.281,
+ "p(95)": 0.429,
+ "avg": 0.24065222659800017,
+ "min": 0.03,
+ "med": 0.13
+ }
+ },
+ "http_req_duration": {
+ "values": {
+ "avg": 62.54300060587695,
+ "min": 5.932,
+ "med": 20.871,
+ "max": 1285.8,
+ "p(90)": 142.57,
+ "p(95)": 258.807
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3301
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 10.557,
+ "p(90)": 0.05,
+ "p(95)": 0.074,
+ "avg": 0.05546258709481963,
+ "min": 0.007,
+ "med": 0.026
+ }
+ },
+ "http_req_connecting": {
+ "values": {
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.04044168433807938,
+ "min": 0,
+ "med": 0,
+ "max": 23.843
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 1285.8,
+ "p(90)": 142.57,
+ "p(95)": 258.807,
+ "avg": 62.54300060587695,
+ "min": 5.932,
+ "med": 20.871
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "rate": 323911.39971363207,
+ "count": 36683001
+ }
+ },
+ "vus_max": {
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ },
+ "type": "gauge"
+ },
+ "http_req_waiting": {
+ "contains": "time",
+ "values": {
+ "avg": 62.24688579218427,
+ "min": 5.859,
+ "med": 20.62,
+ "max": 1285.648,
+ "p(90)": 142.139,
+ "p(95)": 258.697
+ },
+ "type": "trend"
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(90)": 146.60000000000022,
+ "p(95)": 262.09999999999985,
+ "avg": 63.917853895119734,
+ "min": 6,
+ "med": 22,
+ "max": 1360
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 1.1440078764010904,
+ "min": 0,
+ "med": 0,
+ "max": 157.854,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.6,
+ "passes": 9897,
+ "fails": 6598
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 29.13021504579934,
+ "count": 3299
+ }
+ },
+ "data_sent": {
+ "values": {
+ "count": 1615288,
+ "rate": 14263.015095756024
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 2961.3499076999997,
+ "avg": 2055.2935443343463,
+ "min": 1013.761709,
+ "med": 2034.01375,
+ "max": 4043.668166,
+ "p(90)": 2848.445275
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 1.2115764919721284,
+ "min": 0.002,
+ "med": 0.008,
+ "max": 161.369,
+ "p(90)": 0.018,
+ "p(95)": 0.052
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3299,
+ "rate": 29.13021504579934
+ }
+ },
+ "vus": {
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ },
+ "type": "gauge",
+ "contains": "default"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3NDEzNSwiZXhwIjoxNzc1MjY2MTM1fQ.rWaAWmhCkv3ic663EfY5Whba1Pbrm25I0KxAFOUOiWE"
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "fails": 0,
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3299
+ },
+ {
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초",
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3299,
+ "fails": 0
+ },
+ {
+ "passes": 3299,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4"
+ },
+ {
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3299,
+ "name": "시청 기록 조회 API JSON 파싱 가능"
+ },
+ {
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e",
+ "passes": 0,
+ "fails": 3299
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47.html b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47.html
new file mode 100644
index 0000000..ec8f612
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-history-api-2026-03-05T01-30-47.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3301
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6598
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 63.92 |
+
+ 6.00 |
+
+ 22.00 |
+
+ 1360.00 |
+
+ 146.60 |
+
+ 262.10 |
+
+
+
+
+ | http_req_blocked |
+
+ 1.21 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 161.37 |
+
+ 0.02 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 23.84 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 62.54 |
+
+ 5.93 |
+
+ 20.87 |
+
+ 1285.80 |
+
+ 142.57 |
+
+ 258.81 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.24 |
+
+ 0.03 |
+
+ 0.13 |
+
+ 21.15 |
+
+ 0.28 |
+
+ 0.43 |
+
+
+
+
+ | http_req_sending |
+
+ 0.06 |
+
+ 0.01 |
+
+ 0.03 |
+
+ 10.56 |
+
+ 0.05 |
+
+ 0.07 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 1.14 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 157.85 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 62.25 |
+
+ 5.86 |
+
+ 20.62 |
+
+ 1285.65 |
+
+ 142.14 |
+
+ 258.70 |
+
+
+
+
+ | iteration_duration |
+
+ 2055.29 |
+
+ 1013.76 |
+
+ 2034.01 |
+
+ 4043.67 |
+
+ 2848.45 |
+
+ 2961.35 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3299.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3301.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3299.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9897
+
+
+ Failed
+ 6598
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3299
+
+
+ Rate
+ 29.13/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3301
+
+
+
+
+ Rate
+
+ 29.15/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 36.68 MB
+
+
+ Rate
+ 0.32 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.62 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3299 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3299 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3299 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3299 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3299 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27-summary.json b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27-summary.json
new file mode 100644
index 0000000..4cc3fec
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27-summary.json
@@ -0,0 +1,92 @@
+{
+ "root_group": {
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [],
+ "name": "",
+ "path": ""
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 2.006
+ },
+ "metrics": {
+ "data_sent": {
+ "values": {
+ "rate": 0,
+ "count": 0
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 0,
+ "rate": 0
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 0,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0,
+ "min": 0,
+ "med": 0
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27.html b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27.html
new file mode 100644
index 0000000..5f5a4d4
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-10-27.html
@@ -0,0 +1,707 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_duration |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 0.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 0.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 1
+
+
+
+
+
Requests
+
+
+ Total
+
+
+
+
+
+
+ Rate
+
+
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 0.00 MB
+
+
+ Rate
+ 0.00 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 0.00 MB
+
+
+ Rate
+ 0.00 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55-summary.json b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55-summary.json
new file mode 100644
index 0000000..b355c08
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55-summary.json
@@ -0,0 +1,211 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "testRunDurationMs": 110462.403,
+ "isStdOutTTY": false,
+ "isStdErrTTY": false
+ },
+ "metrics": {
+ "http_reqs": {
+ "contains": "default",
+ "values": {
+ "count": 1,
+ "rate": 0.00905285393800459
+ },
+ "type": "counter"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2265,
+ "rate": 20.504714169580396
+ }
+ },
+ "http_req_failed": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": false
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 1,
+ "passes": 1,
+ "fails": 0
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.273,
+ "avg": 0.273,
+ "min": 0.273,
+ "med": 0.273,
+ "max": 0.273,
+ "p(90)": 0.273
+ }
+ },
+ "iteration_duration": {
+ "values": {
+ "avg": 1.6714814510087672,
+ "min": 0.008125,
+ "med": 0.02375,
+ "max": 341.126875,
+ "p(90)": 2.654917,
+ "p(95)": 10.902950599999992
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3973345,
+ "rate": 35970.111930300845
+ }
+ },
+ "http_req_blocked": {
+ "contains": "time",
+ "values": {
+ "min": 35.477,
+ "med": 35.477,
+ "max": 35.477,
+ "p(90)": 35.477,
+ "p(95)": 35.477,
+ "avg": 35.477
+ },
+ "type": "trend"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.863,
+ "min": 0.863,
+ "med": 0.863,
+ "max": 0.863,
+ "p(90)": 0.863,
+ "p(95)": 0.863
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.043,
+ "max": 0.043,
+ "p(90)": 0.043,
+ "p(95)": 0.043,
+ "avg": 0.043,
+ "min": 0.043
+ }
+ },
+ "errors": {
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default"
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 6,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "http_req_waiting": {
+ "contains": "time",
+ "values": {
+ "avg": 419.665,
+ "min": 419.665,
+ "med": 419.665,
+ "max": 419.665,
+ "p(90)": 419.665,
+ "p(95)": 419.665
+ },
+ "type": "trend"
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1815,
+ "rate": 16.430929897478332
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 33.078,
+ "min": 33.078,
+ "med": 33.078,
+ "max": 33.078,
+ "p(90)": 33.078,
+ "p(95)": 33.078
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 420.571,
+ "med": 420.571,
+ "max": 420.571,
+ "p(90)": 420.571,
+ "p(95)": 420.571,
+ "avg": 420.571
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ }
+ },
+ "setup_data": null,
+ "root_group": {
+ "checks": [],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": []
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55.html b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55.html
new file mode 100644
index 0000000..ee12307
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-12-55.html
@@ -0,0 +1,839 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 1
+
+
+
+
+
+
+
+
Failed Requests
+
1
+
+
+
+
+
+
Breached Thresholds
+
1
+
+
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 35.48 |
+
+ 35.48 |
+
+ 35.48 |
+
+ 35.48 |
+
+ 35.48 |
+
+ 35.48 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.27 |
+
+ 0.27 |
+
+ 0.27 |
+
+ 0.27 |
+
+ 0.27 |
+
+ 0.27 |
+
+
+
+
+ | http_req_duration |
+
+ 420.57 |
+
+ 420.57 |
+
+ 420.57 |
+
+ 420.57 |
+
+ 420.57 |
+
+ 420.57 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.86 |
+
+ 0.86 |
+
+ 0.86 |
+
+ 0.86 |
+
+ 0.86 |
+
+ 0.86 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.04 |
+
+ 0.04 |
+
+ 0.04 |
+
+ 0.04 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 33.08 |
+
+ 33.08 |
+
+ 33.08 |
+
+ 33.08 |
+
+ 33.08 |
+
+ 33.08 |
+
+
+
+
+ | http_req_waiting |
+
+ 419.67 |
+
+ 419.67 |
+
+ 419.67 |
+
+ 419.67 |
+
+ 419.67 |
+
+ 419.67 |
+
+
+
+
+ | iteration_duration |
+
+ 1.67 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 341.13 |
+
+ 2.65 |
+
+ 10.90 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 0.00 |
+
+
+
+ | http_req_failed |
+
+ 100.00% |
+ 0.00 |
+ 1.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3973345
+
+
+ Rate
+ 35970.11/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 1
+
+
+
+
+ Rate
+
+ 0.01/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 0.00 MB
+
+
+ Rate
+ 0.00 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 0.00 MB
+
+
+ Rate
+ 0.00 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13-summary.json b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13-summary.json
new file mode 100644
index 0000000..571d3a1
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13-summary.json
@@ -0,0 +1,283 @@
+{
+ "metrics": {
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 1,
+ "passes": 3376,
+ "fails": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": false
+ }
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2010.5963363276035,
+ "min": 1002.192167,
+ "med": 2011.7896665,
+ "max": 3003.201125,
+ "p(90)": 2790.6184375000003,
+ "p(95)": 2899.924708
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2.103790938702989,
+ "min": 0.934,
+ "med": 1.747,
+ "max": 171.135,
+ "p(90)": 2.6354,
+ "p(95)": 3.5411999999999955
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_failed": {
+ "contains": "default",
+ "values": {
+ "rate": 0.9997038791827065,
+ "passes": 3376,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": false
+ }
+ },
+ "type": "rate"
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.48825407166123774,
+ "min": 0,
+ "med": 0,
+ "max": 62.065,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "checks": {
+ "contains": "default",
+ "values": {
+ "rate": 0.5,
+ "passes": 6752,
+ "fails": 6752
+ },
+ "type": "rate"
+ },
+ "http_req_waiting": {
+ "values": {
+ "avg": 2.0564995558187813,
+ "min": 0.899,
+ "med": 1.709,
+ "max": 170.607,
+ "p(90)": 2.5596,
+ "p(95)": 3.4383999999999975
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(95)": 5,
+ "avg": 2.6098933649289098,
+ "min": 1,
+ "med": 2,
+ "max": 69,
+ "p(90)": 3
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 171.135,
+ "min": 171.135,
+ "med": 171.135,
+ "max": 171.135,
+ "p(90)": 171.135,
+ "p(95)": 171.135
+ }
+ },
+ "http_req_connecting": {
+ "contains": "time",
+ "values": {
+ "avg": 0.008697956766360673,
+ "min": 0,
+ "med": 0,
+ "max": 0.521,
+ "p(90)": 0,
+ "p(95)": 0
+ },
+ "type": "trend"
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 62.403,
+ "p(90)": 0.007400000000000075,
+ "p(95)": 0.012,
+ "avg": 0.5049342611785318,
+ "min": 0.001,
+ "med": 0.003
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3376,
+ "rate": 30.083119052001077
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3377,
+ "rate": 30.09202992849752
+ }
+ },
+ "data_received": {
+ "contains": "data",
+ "values": {
+ "count": 2059313,
+ "rate": 18350.28381052532
+ },
+ "type": "counter"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.0320192478531245,
+ "min": 0.012,
+ "med": 0.026,
+ "max": 2.165,
+ "p(90)": 0.046,
+ "p(95)": 0.062
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.025,
+ "avg": 0.01527213503109289,
+ "min": 0.004,
+ "med": 0.012,
+ "max": 3.297,
+ "p(90)": 0.021
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 2,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1282029,
+ "rate": 11424.002083861933
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3376,
+ "rate": 30.083119052001077
+ }
+ },
+ "vus_max": {
+ "values": {
+ "max": 100,
+ "value": 100,
+ "min": 100
+ },
+ "type": "gauge",
+ "contains": "default"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjUwMDEsInRva2VuVHlwZSI6IkJPT1RTVFJBUCIsImlhdCI6MTc3MjY3MzUwMSwiZXhwIjoxNzc1MjY1NTAxfQ.P5a6aqt4ZaBUY9J7vSRY8vV1oqXr7ltd6h_Iq4uCIo8"
+ },
+ "root_group": {
+ "checks": [
+ {
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 0,
+ "fails": 3376
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3376,
+ "fails": 0
+ },
+ {
+ "fails": 0,
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3376
+ },
+ {
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3376,
+ "name": "홈 조회 API JSON 파싱 가능"
+ }
+ ],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": []
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112222.406
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13.html b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13.html
new file mode 100644
index 0000000..d887ec0
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-20-13.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3377
+
+
+
+
+
+
+
+
Failed Requests
+
3376
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
6752
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 2.61 |
+
+ 1.00 |
+
+ 2.00 |
+
+ 69.00 |
+
+ 3.00 |
+
+ 5.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.50 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 62.40 |
+
+ 0.01 |
+
+ 0.01 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.52 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 2.10 |
+
+ 0.93 |
+
+ 1.75 |
+
+ 171.13 |
+
+ 2.64 |
+
+ 3.54 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.03 |
+
+ 0.01 |
+
+ 0.03 |
+
+ 2.17 |
+
+ 0.05 |
+
+ 0.06 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 3.30 |
+
+ 0.02 |
+
+ 0.03 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.49 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 62.06 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 2.06 |
+
+ 0.90 |
+
+ 1.71 |
+
+ 170.61 |
+
+ 2.56 |
+
+ 3.44 |
+
+
+
+
+ | iteration_duration |
+
+ 2010.60 |
+
+ 1002.19 |
+
+ 2011.79 |
+
+ 3003.20 |
+
+ 2790.62 |
+
+ 2899.92 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 100.00% |
+ 3376.00 |
+ 0.00 |
+
+
+
+ | http_req_failed |
+
+ 99.97% |
+ 1.00 |
+ 3376.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3376.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 6752
+
+
+ Failed
+ 6752
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3376
+
+
+ Rate
+ 30.08/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3377
+
+
+
+
+ Rate
+
+ 30.09/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 2.06 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.28 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 0 |
+ 3376 |
+ 0.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3376 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3376 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3376 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41-summary.json b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41-summary.json
new file mode 100644
index 0000000..80a820c
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41-summary.json
@@ -0,0 +1,283 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdErrTTY": false,
+ "testRunDurationMs": 113203.414,
+ "isStdOutTTY": false
+ },
+ "metrics": {
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 124.21524167190488,
+ "min": 12.681,
+ "med": 40.366,
+ "max": 1260.264,
+ "p(90)": 365.8176,
+ "p(95)": 490.17394999999965
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.026572281583909493,
+ "min": 0,
+ "med": 0,
+ "max": 11.901,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_sending": {
+ "contains": "time",
+ "values": {
+ "avg": 0.11910025141420416,
+ "min": 0.005,
+ "med": 0.023,
+ "max": 40.739,
+ "p(90)": 0.043,
+ "p(95)": 0.06894999999999986
+ },
+ "type": "trend"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 17.79140000000001,
+ "p(95)": 31.005699999999965,
+ "avg": 10.205684789440603,
+ "min": 0.139,
+ "med": 6.5435,
+ "max": 242.596
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3180
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(95)": 491.09999999999974,
+ "avg": 125.44591194968554,
+ "min": 12,
+ "med": 41,
+ "max": 1260,
+ "p(90)": 367
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3180,
+ "rate": 28.091025593980763
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 1229.042,
+ "p(90)": 346.5675,
+ "p(95)": 463.7649999999998,
+ "avg": 113.8904566310496,
+ "min": 7.93,
+ "med": 32.260999999999996
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 1.0279000628535502,
+ "min": 0.001,
+ "med": 0.007,
+ "max": 228.026,
+ "p(90)": 0.014,
+ "p(95)": 0.04594999999999986
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3182,
+ "rate": 28.10869290567509
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "data_received": {
+ "contains": "data",
+ "values": {
+ "count": 469145359,
+ "rate": 4144268.643700092
+ },
+ "type": "counter"
+ },
+ "vus": {
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ },
+ "type": "gauge",
+ "contains": "default"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 124.21524167190488,
+ "min": 12.681,
+ "med": 40.366,
+ "max": 1260.264,
+ "p(90)": 365.8176,
+ "p(95)": 490.17394999999965
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 225.884,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.9503830923947205,
+ "min": 0
+ }
+ },
+ "data_sent": {
+ "values": {
+ "count": 1573465,
+ "rate": 13899.448297557528
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3182
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 9540,
+ "fails": 3180,
+ "rate": 0.75
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 1024.550458,
+ "med": 2130.0055205,
+ "max": 4077.266167,
+ "p(90)": 2941.850675,
+ "p(95)": 3049.7185776999995,
+ "avg": 2137.1638974619523
+ }
+ },
+ "total_requests": {
+ "contains": "default",
+ "values": {
+ "count": 3180,
+ "rate": 28.091025593980763
+ },
+ "type": "counter"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3MzgyOSwiZXhwIjoxNzc1MjY1ODI5fQ.ZfFFjMc4fvnHMutlnciW7ETyHrSr6E9ISJ2IB4PeG2k"
+ },
+ "root_group": {
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "fails": 0,
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3180
+ },
+ {
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3180,
+ "fails": 0,
+ "name": "홈 조회 API 응답 시간 < 2초"
+ },
+ {
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3180,
+ "fails": 0
+ },
+ {
+ "passes": 0,
+ "fails": 3180,
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13"
+ }
+ ],
+ "name": ""
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41.html b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41.html
new file mode 100644
index 0000000..08d9649
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-home-api-2026-03-05T01-25-41.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3182
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3180
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 125.45 |
+
+ 12.00 |
+
+ 41.00 |
+
+ 1260.00 |
+
+ 367.00 |
+
+ 491.10 |
+
+
+
+
+ | http_req_blocked |
+
+ 1.03 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 228.03 |
+
+ 0.01 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.03 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 11.90 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 124.22 |
+
+ 12.68 |
+
+ 40.37 |
+
+ 1260.26 |
+
+ 365.82 |
+
+ 490.17 |
+
+
+
+
+ | http_req_receiving |
+
+ 10.21 |
+
+ 0.14 |
+
+ 6.54 |
+
+ 242.60 |
+
+ 17.79 |
+
+ 31.01 |
+
+
+
+
+ | http_req_sending |
+
+ 0.12 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 40.74 |
+
+ 0.04 |
+
+ 0.07 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.95 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 225.88 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 113.89 |
+
+ 7.93 |
+
+ 32.26 |
+
+ 1229.04 |
+
+ 346.57 |
+
+ 463.76 |
+
+
+
+
+ | iteration_duration |
+
+ 2137.16 |
+
+ 1024.55 |
+
+ 2130.01 |
+
+ 4077.27 |
+
+ 2941.85 |
+
+ 3049.72 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3180.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3182.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3180.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9540
+
+
+ Failed
+ 3180
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3180
+
+
+ Rate
+ 28.09/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3182
+
+
+
+
+ Rate
+
+ 28.11/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 469.15 MB
+
+
+ Rate
+ 4.14 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.57 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3180 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3180 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3180 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3180 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21-summary.json b/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21-summary.json
new file mode 100644
index 0000000..913553d
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21-summary.json
@@ -0,0 +1,299 @@
+{
+ "metrics": {
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.7543971471471469,
+ "min": 0,
+ "med": 0,
+ "max": 78.104,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.5000469395418701,
+ "passes": 10653,
+ "fails": 10651
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 3406684,
+ "rate": 16747.552179904214
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5328,
+ "rate": 26.192907241919016
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5326,
+ "rate": 26.18307506953091
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.13889977477477475,
+ "min": 0.017,
+ "med": 0.096,
+ "max": 17.551,
+ "p(90)": 0.2093000000000001,
+ "p(95)": 0.278
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 150,
+ "min": 150,
+ "max": 150
+ }
+ },
+ "connection_pool_errors": {
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ },
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ }
+ },
+ "http_req_connecting": {
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 11.276,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.015159534534534537,
+ "min": 0
+ },
+ "type": "trend"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 847.85,
+ "p(90)": 733.9290000000001,
+ "p(95)": 790.8895,
+ "avg": 380.8523333333333,
+ "min": 16.462,
+ "med": 278.245
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9994369369369369,
+ "passes": 5325,
+ "fails": 3
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2673561,
+ "rate": 13143.456321060858
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.7996330705705591,
+ "min": 0.001,
+ "med": 0.007,
+ "max": 79.108,
+ "p(90)": 0.022,
+ "p(95)": 0.04764999999999976
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 6.9885,
+ "max": 847.85,
+ "p(90)": 44.254700000000014,
+ "p(95)": 80.5566999999999,
+ "avg": 19.42915277777782,
+ "min": 2.464
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.04216460210210131,
+ "min": 0.005,
+ "med": 0.023,
+ "max": 7.475,
+ "p(90)": 0.049,
+ "p(95)": 0.067
+ }
+ },
+ "iteration_duration": {
+ "values": {
+ "p(90)": 4698.492125,
+ "p(95)": 4860.8905835,
+ "avg": 3503.3715694063153,
+ "min": 2005.00425,
+ "med": 3482.7226250000003,
+ "max": 5286.7335
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 3,
+ "min": 2,
+ "max": 150
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9998122418325197,
+ "passes": 5325,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5326,
+ "rate": 26.18307506953091
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 847.104,
+ "p(90)": 43.48180000000002,
+ "p(95)": 80.30129999999987,
+ "avg": 19.248088400900855,
+ "min": 2.315,
+ "med": 6.8365
+ }
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "max": 879,
+ "p(90)": 47,
+ "p(95)": 84,
+ "avg": 20.36838152459632,
+ "min": 2,
+ "med": 7
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsIm9yZ0pvaW5TdGF0dXMiOiJBUFBST1ZFRCIsImlhdCI6MTc3MjY3NDI1OCwiZXhwIjoxNzc1MjY2MjU4fQ.7KYEXBZviomLR74taNGo2z-lKsGP8ZU-VCCZ6GkJOjM",
+ "videoIds": [
+ "1"
+ ]
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "fails": 5325,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7",
+ "passes": 1
+ },
+ {
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c",
+ "passes": 5326,
+ "fails": 0
+ },
+ {
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d",
+ "passes": 5326,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 본문 존재"
+ },
+ {
+ "passes": 0,
+ "fails": 5326,
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945"
+ }
+ ]
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "isStdErrTTY": false,
+ "testRunDurationMs": 203413.846,
+ "isStdOutTTY": false
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21.html b/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21.html
new file mode 100644
index 0000000..fe4b73e
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/before-index-video-join-api-2026-03-05T01-34-21.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5328
+
+
+
+
+
+
+
+
Failed Requests
+
5325
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10651
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.80 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 79.11 |
+
+ 0.02 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 11.28 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 19.43 |
+
+ 2.46 |
+
+ 6.99 |
+
+ 847.85 |
+
+ 44.25 |
+
+ 80.56 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.14 |
+
+ 0.02 |
+
+ 0.10 |
+
+ 17.55 |
+
+ 0.21 |
+
+ 0.28 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 7.47 |
+
+ 0.05 |
+
+ 0.07 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.75 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 78.10 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 19.25 |
+
+ 2.31 |
+
+ 6.84 |
+
+ 847.10 |
+
+ 43.48 |
+
+ 80.30 |
+
+
+
+
+ | iteration_duration |
+
+ 3503.37 |
+
+ 2005.00 |
+
+ 3482.72 |
+
+ 5286.73 |
+
+ 4698.49 |
+
+ 4860.89 |
+
+
+
+
+ | video_join_api_duration |
+
+ 20.37 |
+
+ 2.00 |
+
+ 7.00 |
+
+ 879.00 |
+
+ 47.00 |
+
+ 84.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5325.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5325.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5326.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10653
+
+
+ Failed
+ 10651
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5326
+
+
+ Rate
+ 26.18/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 2
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5328
+
+
+
+
+ Rate
+
+ 26.19/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.41 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.67 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5325 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5326 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5326 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5326 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario1-indexing/scenario1-index-result.md b/k6-tests/results/scenario1-indexing/scenario1-index-result.md
new file mode 100644
index 0000000..951ed04
--- /dev/null
+++ b/k6-tests/results/scenario1-indexing/scenario1-index-result.md
@@ -0,0 +1,251 @@
+# 시나리오 1: DB 인덱스 최적화 부하 테스트 결과
+
+> 테스트 일시: 2026-03-05 10:10 ~ 10:46 (KST)
+> 테스트 환경: macOS (로컬), Spring Boot + PostgreSQL + Redis
+
+---
+
+## 1. 테스트 조건
+
+| 항목 | 값 |
+|------|---|
+| 테스트 도구 | k6 v0.55.0 |
+| Before 조건 | 커스텀 인덱스 0개 (PK/FK 기본 인덱스만 존재) |
+| After 조건 | Partial Index 20개 적용 (`add-indexes.sql`) |
+| Redis 캐시 | 각 테스트 전 `FLUSHALL`로 초기화 |
+
+### 테스트 데이터 규모
+
+| 테이블 | 전체 행 수 | 테스트 대상(org=1) | 비고 |
+|--------|-----------|-------------------|------|
+| video | 1,500 | 500 | 3개 조직, 각 500건 |
+| history | 7,551 | 51 (member=7501) | 시청 기록 |
+| video_category_mapping | 4,500 | 1,500 | 비디오당 3개 카테고리 |
+| video_member_group_mapping | 600 | - | 비디오 접근 권한 매핑 |
+| member_group_mapping | 302 | 2 | 멤버-그룹 매핑 |
+| scrap | 1,510 | 10 | 스크랩 |
+| member | 151 | 50 (org=1) | - |
+
+### 부하 설정
+
+| API | 최대 VU | 테스트 시간 | 부하 패턴 |
+|-----|---------|-----------|----------|
+| Home API | 100명 | 1분 50초 | 0→50→100→100→50→0 단계적 증감 |
+| History API | 100명 | 1분 50초 | 동일 |
+| Video Join API | 150명 | 3분 20초 | 0→20→50→100→100→150→150→0 |
+
+---
+
+## 2. 테스트 결과 비교
+
+### Home API (`GET /{orgId}/home?filter=RECENT|POPULAR|RECOMMEND`)
+
+| 지표 | Before | After | 개선율 |
+|------|--------|-------|--------|
+| **평균 응답시간** | 124.2ms | 14.9ms | **88.0% 감소** |
+| **중앙값 (p50)** | 40.4ms | 13.9ms | 65.6% 감소 |
+| **p90** | 365.8ms | 17.7ms | 95.2% 감소 |
+| **p95** | 490.2ms | 21.5ms | **95.6% 감소** |
+| **최대 응답시간** | 1,260ms | 165ms | 86.9% 감소 |
+| 처리량 (RPS) | 28.1/s | 29.8/s | 6.0% 증가 |
+| 에러율 | 0.00% | 0.00% | - |
+| 총 요청 수 | 3,180 | 3,339 | - |
+
+### History API (`GET /{orgId}/myactivity/video`)
+
+| 지표 | Before | After | 개선율 |
+|------|--------|-------|--------|
+| **평균 응답시간** | 62.5ms | 8.2ms | **86.9% 감소** |
+| **중앙값 (p50)** | 20.9ms | 7.3ms | 65.1% 감소 |
+| **p90** | 142.6ms | 10.9ms | 92.4% 감소 |
+| **p95** | 258.8ms | 16.0ms | **93.8% 감소** |
+| **최대 응답시간** | 1,286ms | 163ms | 87.4% 감소 |
+| 처리량 (RPS) | 29.1/s | 30.0/s | 3.1% 증가 |
+| 에러율 | 0.00% | 0.00% | - |
+| 총 요청 수 | 3,299 | 3,369 | - |
+
+### Video Join API (`POST /{orgId}/video/{videoId}/join`)
+
+| 지표 | Before | After | 개선율 |
+|------|--------|-------|--------|
+| **평균 응답시간** | 19.4ms | 8.7ms | **55.5% 감소** |
+| **중앙값 (p50)** | 7.0ms | 4.3ms | 38.6% 감소 |
+| **p90** | 44.3ms | 11.9ms | 73.2% 감소 |
+| **p95** | 80.6ms | 22.0ms | **72.7% 감소** |
+| **최대 응답시간** | 848ms | 347ms | 59.1% 감소 |
+| 처리량 (RPS) | 26.2/s | 26.1/s | - |
+| 에러율 | 99.94%* | 99.94%* | - |
+| 총 요청 수 | 5,326 | 5,312 | - |
+
+> *Video Join API의 높은 에러율은 단일 테스트 사용자가 동일 영상에 반복 요청하여 발생하는 **409 Conflict("이미 시청 중")**로, 비즈니스 로직상 정상 동작. 첫 1회만 200, 이후 모두 409 반환. 응답시간 지표로 성능 비교 가능.
+
+---
+
+## 3. API별 쿼리-인덱스 매핑 분석
+
+### 3-1. Home API — 가장 큰 개선 (p95: 490ms → 22ms)
+
+Home API가 가장 극적인 개선을 보인 이유는 **다중 테이블 JOIN + 서브쿼리** 구조에서 인덱스 효과가 복합적으로 작용했기 때문이다.
+
+**실행되는 쿼리 체인:**
+
+```
+1. memberRepository.findByIdAndOrganizationIdAndStatus() → member 테이블
+2. videoRepository.findHomeVideos() → video + JOIN 4개 테이블
+3. videoRepository.findCategoriesForHomeVideos() → video_category_mapping + category
+```
+
+**핵심 병목: `findHomeVideos()` QueryDSL 쿼리**
+
+```sql
+SELECT v.*, (scrap 존재 여부 서브쿼리)
+FROM video v
+LEFT JOIN video_member_group_mapping vmgm ON v.id = vmgm.video_id
+LEFT JOIN member_group_mapping mgm ON vmgm.member_group_id = mgm.member_group_id
+WHERE v.organization_id = ? AND v.upload_status = 'COMPLETE' AND v.status = 'ACTIVE'
+ AND (vmgm이 없거나 mgm.member_id = ?)
+ORDER BY v.created_at DESC -- 또는 v.watch_cnt DESC
+```
+
+| 적용 인덱스 | 역할 | 효과 |
+|------------|------|------|
+| `idx_video_org_status_created` | WHERE + ORDER BY 동시 커버 | Full Table Scan → **Index Scan + 정렬 제거** |
+| `idx_video_member_group_mapping_video` | LEFT JOIN 조건 최적화 | Nested Loop Join 시 video_id 기반 즉시 탐색 |
+| `idx_member_group_mapping_member` | 멤버 그룹 필터링 | member_id 기반 그룹 조회 최적화 |
+| `idx_scrap_member_video` | 스크랩 존재 여부 서브쿼리 | EXISTS 서브쿼리 실행 시간 단축 |
+| `idx_member_org_status` | 멤버 조회 최적화 | organization_id + status 복합 조건 커버 |
+
+**분석:** 인덱스 미적용 시 `video` 테이블 1,500행을 Full Scan하면서 매 행마다 `video_member_group_mapping`(600행), `member_group_mapping`(302행), `scrap`(1,510행) 서브쿼리를 실행하므로, 동시 사용자 증가 시 **p95가 490ms까지 치솟는 현상** 발생. 인덱스 적용 후 각 JOIN/서브쿼리가 Index Seek로 전환되어 p95가 22ms로 감소.
+
+### 3-2. History API — 정렬 인덱스 효과 (p95: 259ms → 16ms)
+
+**실행되는 쿼리 체인:**
+
+```
+1. memberRepository.existsByIdAndOrganizationIdAndStatus() → member 테이블
+2. historyRepository.findByMemberId() → history + video + scrap
+```
+
+**핵심 병목: `findByMemberId()` QueryDSL 쿼리**
+
+```sql
+SELECT h.*, v.*, (scrap 존재 여부 서브쿼리)
+FROM history h
+JOIN video v ON h.video_id = v.id
+WHERE h.member_id = ? AND h.status = 'ACTIVE'
+ AND v.upload_status = 'COMPLETE' AND v.join_status = 'APPROVED'
+ORDER BY h.last_watched_at DESC
+```
+
+| 적용 인덱스 | 역할 | 효과 |
+|------------|------|------|
+| `idx_history_member_last_watched` | WHERE member_id + ORDER BY last_watched_at DESC | **Covering Index**: 필터링과 정렬을 인덱스만으로 해결 |
+| `idx_scrap_member_video` | 스크랩 존재 여부 서브쿼리 | EXISTS 서브쿼리 최적화 |
+| `idx_member_org_status` | 멤버 존재 확인 | 복합 조건 인덱스 커버 |
+
+**분석:** `idx_history_member_last_watched`가 `(member_id, last_watched_at DESC)` 복합 인덱스이므로, PostgreSQL이 **별도 정렬(Sort) 없이** 인덱스 순서대로 결과를 반환. 인덱스 미적용 시 history 7,551행 Full Scan → 필터링 → Sort 과정이 필요했으나, 적용 후 Index Scan으로 직접 정렬된 결과 반환.
+
+### 3-3. Video Join API — 다중 쿼리 최적화 (p95: 81ms → 22ms)
+
+**실행되는 쿼리 체인 (가장 복잡):**
+
+```
+1. memberRepository.findByIdAndOrganizationIdAndStatus() → member
+2. videoRepository.findById() → video (PK)
+3. memberGroupRepository.isAccessibleToVideo() → video_member_group_mapping + member_group_mapping
+4. scrapRepository.existsByMemberIdAndVideoId() → scrap
+5. categoryRepository.findAllByVideoId() → video_category_mapping + category
+6. historyRepository.findByMemberIdAndVideoId() → history
+7. Redis 세션 생성/조회 → Redis
+```
+
+| 적용 인덱스 | 역할 | 효과 |
+|------------|------|------|
+| `idx_video_member_group_mapping_video` | 비디오 접근 권한 확인 (1차) | video_id 기반 그룹 매핑 조회 |
+| `idx_video_member_group_mapping_group` | 비디오 접근 권한 확인 (2차) | member_group_id + video_id 복합 조건 |
+| `idx_member_group_mapping_member` | 멤버 그룹 소속 확인 | member_id 기반 그룹 조회 |
+| `idx_history_member_video` | 기존 시청 기록 조회 | member_id + video_id 복합 조건 |
+| `idx_scrap_member_video` | 스크랩 상태 확인 | EXISTS 쿼리 최적화 |
+| `idx_video_category_mapping_video` | 카테고리 목록 조회 | video_id 기반 카테고리 매핑 |
+| `idx_member_org_status` | 멤버 조회 | 복합 조건 커버 |
+
+**분석:** Video Join API는 **7개 쿼리를 순차 실행**하는 구조로, 개별 쿼리의 개선 폭은 작지만(각 수~십 ms), 인덱스가 7개 쿼리 전체에 걸쳐 효과를 발휘하여 누적 개선이 발생. 특히 `isAccessibleToVideo()`의 2단계 접근 권한 확인 쿼리에서 `idx_video_member_group_mapping_*` 인덱스의 효과가 큼.
+
+---
+
+## 4. 인덱스별 영향 범위
+
+| 인덱스 | Home API | History API | Video Join API |
+|--------|:--------:|:-----------:|:--------------:|
+| `idx_video_org_status_created` | **핵심** | - | - |
+| `idx_history_member_last_watched` | - | **핵심** | - |
+| `idx_history_member_video` | - | - | O |
+| `idx_video_member_group_mapping_video` | O | - | **핵심** |
+| `idx_video_member_group_mapping_group` | - | - | **핵심** |
+| `idx_member_group_mapping_member` | O | - | O |
+| `idx_scrap_member_video` | O | O | O |
+| `idx_video_category_mapping_video` | O | - | O |
+| `idx_member_org_status` | O | O | O |
+
+> **핵심**: 해당 API의 주요 병목 쿼리에 직접 작용하는 인덱스
+> **O**: 보조적으로 성능에 기여하는 인덱스
+
+---
+
+## 5. 핵심 개선 요약
+
+```
+Home API p95: 490ms → 22ms (95.6% 감소, ~22배 개선)
+History API p95: 259ms → 16ms (93.8% 감소, ~16배 개선)
+Video Join p95: 81ms → 22ms (72.7% 감소, ~3.7배 개선)
+```
+
+---
+
+## 6. Tail Latency 분석
+
+| API | Before max | After max | 감소율 | 원인 분석 |
+|-----|-----------|----------|--------|----------|
+| Home API | 1,260ms | 165ms | 86.9% | Full Scan + 다중 JOIN 제거 |
+| History API | 1,286ms | 163ms | 87.4% | Sort 제거 (인덱스 정렬 활용) |
+| Video Join | 848ms | 347ms | 59.1% | 7개 쿼리 누적 지연 감소 |
+
+Before 상태에서 **최대 응답시간이 1초를 초과**하는 것은 동시 사용자 증가 시 PostgreSQL의 Shared Buffer 경합과 Full Table Scan의 I/O 비용이 복합적으로 작용한 결과. 인덱스 적용 후 모든 API의 최대 응답시간이 **347ms 이하**로 안정화.
+
+---
+
+## 7. 적용 인덱스 전체 목록 (20개)
+
+| # | 테이블 | 인덱스명 | 컬럼 | 조건 |
+|---|--------|---------|------|------|
+| 1 | history | `idx_history_member_last_watched` | member_id, last_watched_at DESC | WHERE status='ACTIVE' |
+| 2 | history | `idx_history_member_video` | member_id, video_id | WHERE status='ACTIVE' |
+| 3 | history | `idx_history_video_member` | video_id, member_id, last_watched_at DESC | WHERE status='ACTIVE' |
+| 4 | history | `idx_history_member_completed` | member_id, is_complete, completed_at | WHERE status='ACTIVE' AND is_complete=true |
+| 5 | video | `idx_video_org_status_created` | organization_id, upload_status, created_at DESC | WHERE status='ACTIVE' |
+| 6 | video | `idx_video_org_creator_status` | organization_id, member_id, upload_status, created_at DESC | WHERE status='ACTIVE' |
+| 7 | video | `idx_video_org_title_status` | organization_id, upload_status, title | WHERE status='ACTIVE' |
+| 8 | video | `idx_video_video_key` | video_url | WHERE status='ACTIVE' |
+| 9 | video_member_group_mapping | `idx_video_member_group_mapping_video` | video_id, status | WHERE status='ACTIVE' |
+| 10 | video_member_group_mapping | `idx_video_member_group_mapping_group` | member_group_id, video_id, status | WHERE status='ACTIVE' |
+| 11 | member_group_mapping | `idx_member_group_mapping_member` | member_id, member_group_id, status | WHERE status='ACTIVE' |
+| 12 | member_group_mapping | `idx_member_group_mapping_group` | member_group_id, member_id, status | WHERE status='ACTIVE' |
+| 13 | video_category_mapping | `idx_video_category_mapping_video` | video_id, category_id, status | WHERE status='ACTIVE' |
+| 14 | video_category_mapping | `idx_video_category_mapping_category` | category_id, video_id, status | WHERE status='ACTIVE' |
+| 15 | scrap | `idx_scrap_member_video` | member_id, video_id, status | WHERE status='ACTIVE' |
+| 16 | member | `idx_member_org_status` | organization_id, id, status, join_status | WHERE status='ACTIVE' |
+| 17 | member | `idx_member_user_status` | user_id, status, join_status | WHERE status='ACTIVE' |
+| 18 | notice_member_group_mapping | `idx_notice_member_group_mapping_notice` | notice_id, member_group_id | (조건 없음) |
+| 19 | comment | `idx_comment_video_status` | video_id, status, created_at DESC | WHERE status='ACTIVE' |
+| 20 | comment | `idx_comment_parent` | parent_comment_id, status, created_at | WHERE status='ACTIVE' AND is_child=true |
+
+> 19개 인덱스는 `WHERE status = 'ACTIVE'` Partial Index로 생성하여 **비활성 데이터를 인덱스에서 제외**, 인덱스 크기 최소화 및 INSERT/UPDATE 오버헤드 절감.
+
+---
+
+## 8. 결론
+
+- **Home API**의 다중 JOIN 쿼리에서 인덱스 효과가 가장 극대화 (p95 기준 22배 개선)
+- Partial Index(`WHERE status = 'ACTIVE'`)를 활용하여 인덱스 크기를 최소화하면서 쿼리 성능 극대화
+- 인덱스 적용만으로 모든 API의 **Tail Latency(최대 응답시간)가 1초 이상에서 350ms 이하로 안정화**
+- 처리량(RPS)은 인덱스 유무와 무관하게 유사 → 병목이 DB I/O에서 네트워크 오버헤드로 이동한 것으로 판단
diff --git a/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37-summary.json b/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37-summary.json
new file mode 100644
index 0000000..d418327
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37-summary.json
@@ -0,0 +1,290 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112212.598
+ },
+ "metrics": {
+ "total_requests": {
+ "contains": "default",
+ "values": {
+ "count": 3388,
+ "rate": 30.192688346811114
+ },
+ "type": "counter"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 3037.637125,
+ "p(90)": 2801.7496126,
+ "p(95)": 2900.33111305,
+ "avg": 2001.9907669881934,
+ "min": 1009.391333,
+ "med": 2010.0415835
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3390,
+ "rate": 30.210511657523515
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0,
+ "avg": 0.009416814159292039,
+ "min": 0,
+ "med": 0,
+ "max": 2.267,
+ "p(90)": 0
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3388,
+ "rate": 30.192688346811114
+ }
+ },
+ "errors": {
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3388
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default"
+ },
+ "http_req_blocked": {
+ "contains": "time",
+ "values": {
+ "p(95)": 0.02,
+ "avg": 0.375478171091428,
+ "min": 0.001,
+ "med": 0.004,
+ "max": 31.426,
+ "p(90)": 0.01
+ },
+ "type": "trend"
+ },
+ "http_req_tls_handshaking": {
+ "values": {
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.3571513274336283,
+ "min": 0,
+ "med": 0,
+ "max": 29.836
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_failed": {
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3390
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 100.927,
+ "p(90)": 16.689100000000003,
+ "p(95)": 21.489099999999997,
+ "avg": 9.166242772861345,
+ "min": 3.737,
+ "med": 7.101
+ }
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "avg": 9.584710743801653,
+ "min": 3,
+ "med": 7,
+ "max": 67,
+ "p(90)": 18,
+ "p(95)": 23
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0.004,
+ "med": 0.012,
+ "max": 1.962,
+ "p(90)": 0.026,
+ "p(95)": 0.034,
+ "avg": 0.01809056047197662
+ }
+ },
+ "http_req_receiving": {
+ "contains": "time",
+ "values": {
+ "avg": 0.06931592920353984,
+ "min": 0.012,
+ "med": 0.05,
+ "max": 6.49,
+ "p(90)": 0.123,
+ "p(95)": 0.162
+ },
+ "type": "trend"
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.166242772861345,
+ "min": 3.737,
+ "med": 7.101,
+ "max": 100.927,
+ "p(90)": 16.689100000000003,
+ "p(95)": 21.489099999999997
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.078836283185824,
+ "min": 3.711,
+ "med": 7.0205,
+ "max": 100.615,
+ "p(90)": 16.538200000000003,
+ "p(95)": 21.29404999999999
+ }
+ },
+ "data_sent": {
+ "values": {
+ "rate": 14744.663518083771,
+ "count": 1654537
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "data_received": {
+ "values": {
+ "count": 38355817,
+ "rate": 341813.8220095394
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.6,
+ "passes": 10164,
+ "fails": 6776
+ }
+ },
+ "vus": {
+ "values": {
+ "value": 2,
+ "min": 1,
+ "max": 100
+ },
+ "type": "gauge",
+ "contains": "default"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3NjgyNSwiZXhwIjoxNzc1MjY4ODI1fQ._MStknJ0xuCQI2Hkpa8a0fjSx3GpN5nFAisc4FlQ-50"
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3388,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초",
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3388,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3388,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API JSON 파싱 가능",
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3388
+ },
+ {
+ "passes": 0,
+ "fails": 3388,
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37.html b/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37.html
new file mode 100644
index 0000000..dfb8ce8
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-history-api-2026-03-05T02-15-37.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3390
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6776
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 9.58 |
+
+ 3.00 |
+
+ 7.00 |
+
+ 67.00 |
+
+ 18.00 |
+
+ 23.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.38 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 31.43 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 2.27 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 9.17 |
+
+ 3.74 |
+
+ 7.10 |
+
+ 100.93 |
+
+ 16.69 |
+
+ 21.49 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.07 |
+
+ 0.01 |
+
+ 0.05 |
+
+ 6.49 |
+
+ 0.12 |
+
+ 0.16 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 1.96 |
+
+ 0.03 |
+
+ 0.03 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.36 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 29.84 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 9.08 |
+
+ 3.71 |
+
+ 7.02 |
+
+ 100.61 |
+
+ 16.54 |
+
+ 21.29 |
+
+
+
+
+ | iteration_duration |
+
+ 2001.99 |
+
+ 1009.39 |
+
+ 2010.04 |
+
+ 3037.64 |
+
+ 2801.75 |
+
+ 2900.33 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3388.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3390.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3388.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10164
+
+
+ Failed
+ 6776
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3388
+
+
+ Rate
+ 30.19/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3390
+
+
+
+
+ Rate
+
+ 30.21/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 38.36 MB
+
+
+ Rate
+ 0.34 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.65 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3388 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3388 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3388 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3388 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3388 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40-summary.json b/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40-summary.json
new file mode 100644
index 0000000..d70f0ac
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40-summary.json
@@ -0,0 +1,283 @@
+{
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3NjcwOCwiZXhwIjoxNzc1MjY4NzA4fQ.vvYCn9kQT_oe71yvNAfSyplJAwvIrcosPzGLgl2L3UQ"
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3323,
+ "fails": 0
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3323,
+ "fails": 0
+ },
+ {
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3323,
+ "fails": 0
+ },
+ {
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3323
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112665.168
+ },
+ "metrics": {
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 64.1382,
+ "avg": 28.05070827067662,
+ "min": 10.878,
+ "med": 19.597,
+ "max": 421.976,
+ "p(90)": 44.50280000000001
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1637049,
+ "rate": 14530.213987698487
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3325,
+ "rate": 29.51222688453276
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.022823157894737208,
+ "min": 0.003,
+ "med": 0.015,
+ "max": 2.814,
+ "p(90)": 0.03,
+ "p(95)": 0.04
+ }
+ },
+ "checks": {
+ "contains": "default",
+ "values": {
+ "rate": 0.75,
+ "passes": 9969,
+ "fails": 3323
+ },
+ "type": "rate"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 6.254,
+ "avg": 3.6586372932330824,
+ "min": 0.063,
+ "med": 3.028,
+ "max": 62.191,
+ "p(90)": 5.0586
+ }
+ },
+ "http_req_failed": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 3325,
+ "rate": 0,
+ "passes": 0
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 457268257,
+ "rate": 4058647.9842643114
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3323,
+ "rate": 29.494475169113493
+ }
+ },
+ "errors": {
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3323
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate"
+ },
+ "http_req_connecting": {
+ "values": {
+ "med": 0,
+ "max": 4.881,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.013522105263157901,
+ "min": 0
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3323,
+ "rate": 29.494475169113493
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(95)": 65,
+ "avg": 28.54619319891664,
+ "min": 11,
+ "med": 20,
+ "max": 422,
+ "p(90)": 45
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "http_req_tls_handshaking": {
+ "contains": "time",
+ "values": {
+ "max": 48.14,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.38880030075187977,
+ "min": 0,
+ "med": 0
+ },
+ "type": "trend"
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0.001,
+ "med": 0.004,
+ "max": 48.517,
+ "p(90)": 0.009,
+ "p(95)": 0.015,
+ "avg": 0.4109326315789382
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2042.3221801363245,
+ "min": 1022.700709,
+ "med": 2034.494917,
+ "max": 3099.478958,
+ "p(90)": 2840.4642586,
+ "p(95)": 2939.7174419
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 421.976,
+ "p(90)": 44.50280000000001,
+ "p(95)": 64.1382,
+ "avg": 28.05070827067662,
+ "min": 10.878,
+ "med": 19.597
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "max": 100,
+ "value": 100,
+ "min": 100
+ }
+ },
+ "http_req_waiting": {
+ "values": {
+ "avg": 24.369247819548914,
+ "min": 8.104,
+ "med": 16.297,
+ "max": 417.481,
+ "p(90)": 39.9212,
+ "p(95)": 57.8106
+ },
+ "type": "trend",
+ "contains": "time"
+ }
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40.html b/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40.html
new file mode 100644
index 0000000..f9e6d48
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-home-api-2026-03-05T02-13-40.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3325
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3323
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 28.55 |
+
+ 11.00 |
+
+ 20.00 |
+
+ 422.00 |
+
+ 45.00 |
+
+ 65.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.41 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 48.52 |
+
+ 0.01 |
+
+ 0.01 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 4.88 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 28.05 |
+
+ 10.88 |
+
+ 19.60 |
+
+ 421.98 |
+
+ 44.50 |
+
+ 64.14 |
+
+
+
+
+ | http_req_receiving |
+
+ 3.66 |
+
+ 0.06 |
+
+ 3.03 |
+
+ 62.19 |
+
+ 5.06 |
+
+ 6.25 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 2.81 |
+
+ 0.03 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.39 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 48.14 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 24.37 |
+
+ 8.10 |
+
+ 16.30 |
+
+ 417.48 |
+
+ 39.92 |
+
+ 57.81 |
+
+
+
+
+ | iteration_duration |
+
+ 2042.32 |
+
+ 1022.70 |
+
+ 2034.49 |
+
+ 3099.48 |
+
+ 2840.46 |
+
+ 2939.72 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3323.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3325.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3323.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9969
+
+
+ Failed
+ 3323
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3323
+
+
+ Rate
+ 29.49/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3325
+
+
+
+
+ Rate
+
+ 29.51/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 457.27 MB
+
+
+ Rate
+ 4.06 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3323 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3323 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3323 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3323 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06-summary.json b/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06-summary.json
new file mode 100644
index 0000000..b6c103d
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06-summary.json
@@ -0,0 +1,299 @@
+{
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 203340.17
+ },
+ "metrics": {
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2672279,
+ "rate": 13141.913867781264
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 5299,
+ "fails": 3,
+ "rate": 0.9994341757827235
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 26.074533133320386,
+ "count": 5302
+ }
+ },
+ "checks": {
+ "contains": "default",
+ "values": {
+ "rate": 0.5000471698113208,
+ "passes": 10601,
+ "fails": 10599
+ },
+ "type": "rate"
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "max": 150,
+ "value": 2,
+ "min": 2
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.999811320754717,
+ "passes": 5299,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 3391240,
+ "rate": 16677.668755760358
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 150,
+ "min": 150,
+ "max": 150
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "contains": "time",
+ "values": {
+ "p(95)": 112.5319,
+ "avg": 63.79999999999999,
+ "min": 8.593,
+ "med": 64.993,
+ "max": 117.814,
+ "p(90)": 107.2498
+ },
+ "type": "trend"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 4871.24563685,
+ "avg": 3516.176150016585,
+ "min": 2004.024916,
+ "med": 3524.4983334999997,
+ "max": 5057.481125,
+ "p(90)": 4719.2201955
+ }
+ },
+ "connection_pool_errors": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "count": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 6.71452319879291,
+ "min": 1.831,
+ "med": 4.165,
+ "max": 127.758,
+ "p(90)": 13.856700000000004,
+ "p(95)": 20.579049999999974
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "iterations": {
+ "values": {
+ "count": 5300,
+ "rate": 26.064697398453045
+ },
+ "type": "counter",
+ "contains": "default"
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 2.739,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.008344586948321388,
+ "min": 0
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 127.713,
+ "p(90)": 13.616700000000002,
+ "p(95)": 20.08665,
+ "avg": 6.605357412297231,
+ "min": 1.801,
+ "med": 4.089
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.07240475292342492,
+ "min": 0.011,
+ "med": 0.051,
+ "max": 4.405,
+ "p(90)": 0.125,
+ "p(95)": 0.165
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5300,
+ "rate": 26.064697398453045
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.3418264805733684,
+ "min": 0,
+ "med": 0,
+ "max": 61.07,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.017,
+ "max": 34.047,
+ "p(90)": 0.037,
+ "p(95)": 0.057,
+ "avg": 0.03676103357223676,
+ "min": 0.004
+ }
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "min": 1,
+ "med": 4,
+ "max": 128,
+ "p(90)": 15,
+ "p(95)": 21.049999999999766,
+ "avg": 7.190943396226415
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.3759805733685322,
+ "min": 0.001,
+ "med": 0.006,
+ "max": 64.44,
+ "p(90)": 0.019,
+ "p(95)": 0.04589999999999953
+ }
+ }
+ },
+ "setup_data": {
+ "videoIds": [
+ "151"
+ ],
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3Njk0MywiZXhwIjoxNzc1MjY4OTQzfQ.KCDffnqvV22dseEK2fKU6kv4yuToN3byZhXOSu41nQE"
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7",
+ "passes": 1,
+ "fails": 5299,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201"
+ },
+ {
+ "passes": 5300,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c"
+ },
+ {
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d",
+ "passes": 5300,
+ "fails": 0
+ },
+ {
+ "fails": 5300,
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945",
+ "passes": 0
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06.html b/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06.html
new file mode 100644
index 0000000..d70bedc
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/after-cache-video-join-api-2026-03-05T02-19-06.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5302
+
+
+
+
+
+
+
+
Failed Requests
+
5299
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10599
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.38 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 64.44 |
+
+ 0.02 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 2.74 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 6.71 |
+
+ 1.83 |
+
+ 4.17 |
+
+ 127.76 |
+
+ 13.86 |
+
+ 20.58 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.07 |
+
+ 0.01 |
+
+ 0.05 |
+
+ 4.41 |
+
+ 0.13 |
+
+ 0.17 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.02 |
+
+ 34.05 |
+
+ 0.04 |
+
+ 0.06 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.34 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 61.07 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 6.61 |
+
+ 1.80 |
+
+ 4.09 |
+
+ 127.71 |
+
+ 13.62 |
+
+ 20.09 |
+
+
+
+
+ | iteration_duration |
+
+ 3516.18 |
+
+ 2004.02 |
+
+ 3524.50 |
+
+ 5057.48 |
+
+ 4719.22 |
+
+ 4871.25 |
+
+
+
+
+ | video_join_api_duration |
+
+ 7.19 |
+
+ 1.00 |
+
+ 4.00 |
+
+ 128.00 |
+
+ 15.00 |
+
+ 21.05 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5299.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5299.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5300.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10601
+
+
+ Failed
+ 10599
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5300
+
+
+ Rate
+ 26.06/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 2
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5302
+
+
+
+
+ Rate
+
+ 26.07/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.39 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.67 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5299 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5300 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5300 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5300 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00-summary.json b/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00-summary.json
new file mode 100644
index 0000000..418fb4f
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00-summary.json
@@ -0,0 +1,290 @@
+{
+ "metrics": {
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 6732,
+ "rate": 0.6,
+ "passes": 10098
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 30.049321059426575,
+ "count": 3366
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 0.026,
+ "p(95)": 0.035,
+ "avg": 0.017667458432304307,
+ "min": 0.003,
+ "med": 0.013,
+ "max": 2.409
+ }
+ },
+ "errors": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 0,
+ "fails": 3366,
+ "rate": 0
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3368,
+ "rate": 30.067175676811853
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.45719032066507,
+ "min": 3.831,
+ "med": 7.14,
+ "max": 108.966,
+ "p(90)": 17.844400000000007,
+ "p(95)": 22.596499999999995
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.161,
+ "avg": 0.07087203087885965,
+ "min": 0.011,
+ "med": 0.053,
+ "max": 3.944,
+ "p(90)": 0.124
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 0,
+ "fails": 3368,
+ "rate": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 33.249,
+ "p(90)": 0.01,
+ "p(95)": 0.01764999999999985,
+ "avg": 0.3581511282660146,
+ "min": 0.001,
+ "med": 0.004
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "http_req_waiting": {
+ "values": {
+ "avg": 9.368650831353913,
+ "min": 3.807,
+ "med": 7.057,
+ "max": 108.726,
+ "p(90)": 17.6609,
+ "p(95)": 22.459449999999993
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 38107855,
+ "rate": 340200.5851993685
+ }
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "avg": 9.857694592988711,
+ "min": 4,
+ "med": 7,
+ "max": 78,
+ "p(90)": 19,
+ "p(95)": 23
+ }
+ },
+ "http_req_connecting": {
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 3.841,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.01022832541567696
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.33838301662707837,
+ "min": 0,
+ "med": 0,
+ "max": 32.941,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 3011.337583,
+ "p(90)": 2816.952667,
+ "p(95)": 2919.66361475,
+ "avg": 2017.6495372932327,
+ "min": 1006.277209,
+ "med": 2003.962771
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3366,
+ "rate": 30.049321059426575
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1644835,
+ "rate": 14683.949793458678
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.45719032066507,
+ "min": 3.831,
+ "med": 7.14,
+ "max": 108.966,
+ "p(90)": 17.844400000000007,
+ "p(95)": 22.596499999999995
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3NjE4OCwiZXhwIjoxNzc1MjY4MTg4fQ.uc3PR0883EYVOJnKqciwfo0gGp-590Yaot2dszkubX8"
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3366,
+ "fails": 0
+ },
+ {
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3366,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초"
+ },
+ {
+ "passes": 3366,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4"
+ },
+ {
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3366,
+ "name": "시청 기록 조회 API JSON 파싱 가능"
+ },
+ {
+ "id": "3795741ae94c333f94ea574c9e3cb95e",
+ "passes": 0,
+ "fails": 3366,
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환"
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112015.842
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00.html b/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00.html
new file mode 100644
index 0000000..1123083
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-history-api-2026-03-05T02-05-00.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3368
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6732
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 9.86 |
+
+ 4.00 |
+
+ 7.00 |
+
+ 78.00 |
+
+ 19.00 |
+
+ 23.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.36 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 33.25 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 3.84 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 9.46 |
+
+ 3.83 |
+
+ 7.14 |
+
+ 108.97 |
+
+ 17.84 |
+
+ 22.60 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.07 |
+
+ 0.01 |
+
+ 0.05 |
+
+ 3.94 |
+
+ 0.12 |
+
+ 0.16 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 2.41 |
+
+ 0.03 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.34 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 32.94 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 9.37 |
+
+ 3.81 |
+
+ 7.06 |
+
+ 108.73 |
+
+ 17.66 |
+
+ 22.46 |
+
+
+
+
+ | iteration_duration |
+
+ 2017.65 |
+
+ 1006.28 |
+
+ 2003.96 |
+
+ 3011.34 |
+
+ 2816.95 |
+
+ 2919.66 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3366.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3368.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3366.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10098
+
+
+ Failed
+ 6732
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3366
+
+
+ Rate
+ 30.05/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3368
+
+
+
+
+ Rate
+
+ 30.07/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 38.11 MB
+
+
+ Rate
+ 0.34 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3366 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3366 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3366 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3366 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3366 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02-summary.json b/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02-summary.json
new file mode 100644
index 0000000..b0d5238
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02-summary.json
@@ -0,0 +1,283 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3223,
+ "fails": 0
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3210,
+ "fails": 13
+ },
+ {
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3223,
+ "fails": 0
+ },
+ {
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3223,
+ "name": "홈 조회 API JSON 파싱 가능"
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 116034.258
+ },
+ "metrics": {
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1592586,
+ "rate": 13725.136243815168
+ }
+ },
+ "vus": {
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 0,
+ "max": 100
+ },
+ "type": "gauge"
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.013,
+ "max": 16.586,
+ "p(90)": 0.036,
+ "p(95)": 0.051,
+ "avg": 0.03540186046511677,
+ "min": 0.004
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 443512657,
+ "rate": 3822256.1564533813
+ }
+ },
+ "checks": {
+ "contains": "default",
+ "values": {
+ "rate": 0.7489916227117592,
+ "passes": 9656,
+ "fails": 3236
+ },
+ "type": "rate"
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3225,
+ "rate": 27.793515945954514
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3223
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "iteration_duration": {
+ "contains": "time",
+ "values": {
+ "avg": 2106.6562891548256,
+ "min": 1018.611792,
+ "med": 2100.501708,
+ "max": 5917.013334,
+ "p(90)": 2903.1556914,
+ "p(95)": 3011.1195869
+ },
+ "type": "trend"
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3223,
+ "rate": 27.776279656995783
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 5.079159069767442,
+ "min": 0.067,
+ "med": 2.944,
+ "max": 182.329,
+ "p(90)": 7.863000000000001,
+ "p(95)": 11.564599999999999
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "avg": 110.86596338814769,
+ "min": 11,
+ "med": 22,
+ "max": 3328,
+ "p(90)": 224.80000000000007,
+ "p(95)": 611.7999999999997
+ }
+ },
+ "http_req_duration": {
+ "contains": "time",
+ "values": {
+ "avg": 110.63936527131793,
+ "min": 10.861,
+ "med": 21.15,
+ "max": 3287.154,
+ "p(90)": 222.54180000000014,
+ "p(95)": 613.9345999999996
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend"
+ },
+ "http_req_duration{expected_response:true}": {
+ "contains": "time",
+ "values": {
+ "avg": 110.63936527131793,
+ "min": 10.861,
+ "med": 21.15,
+ "max": 3287.154,
+ "p(90)": 222.54180000000014,
+ "p(95)": 613.9345999999996
+ },
+ "type": "trend"
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 8.036,
+ "med": 17.954,
+ "max": 3253.32,
+ "p(90)": 205.62460000000004,
+ "p(95)": 603.1695999999996,
+ "avg": 105.52480434108493
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 1.4168406201549972,
+ "min": 0.001,
+ "med": 0.004,
+ "max": 325.757,
+ "p(90)": 0.012,
+ "p(95)": 0.022799999999999855
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.02716186046511627,
+ "min": 0,
+ "med": 0,
+ "max": 8.131
+ }
+ },
+ "http_req_failed": {
+ "values": {
+ "passes": 0,
+ "fails": 3225,
+ "rate": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default"
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3223,
+ "rate": 27.776279656995783
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0,
+ "avg": 1.3646620155038767,
+ "min": 0,
+ "med": 0,
+ "max": 320.529,
+ "p(90)": 0
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3NjA3MSwiZXhwIjoxNzc1MjY4MDcxfQ.trXIFjLmuGBCABtCnQUqFKnO2fx4WET9hF1jSy-ML5M"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02.html b/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02.html
new file mode 100644
index 0000000..cec666f
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-home-api-2026-03-05T02-03-02.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3225
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3236
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 110.87 |
+
+ 11.00 |
+
+ 22.00 |
+
+ 3328.00 |
+
+ 224.80 |
+
+ 611.80 |
+
+
+
+
+ | http_req_blocked |
+
+ 1.42 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 325.76 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.03 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 8.13 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 110.64 |
+
+ 10.86 |
+
+ 21.15 |
+
+ 3287.15 |
+
+ 222.54 |
+
+ 613.93 |
+
+
+
+
+ | http_req_receiving |
+
+ 5.08 |
+
+ 0.07 |
+
+ 2.94 |
+
+ 182.33 |
+
+ 7.86 |
+
+ 11.56 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 16.59 |
+
+ 0.04 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 1.36 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 320.53 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 105.52 |
+
+ 8.04 |
+
+ 17.95 |
+
+ 3253.32 |
+
+ 205.62 |
+
+ 603.17 |
+
+
+
+
+ | iteration_duration |
+
+ 2106.66 |
+
+ 1018.61 |
+
+ 2100.50 |
+
+ 5917.01 |
+
+ 2903.16 |
+
+ 3011.12 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3223.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3225.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3223.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9656
+
+
+ Failed
+ 3236
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3223
+
+
+ Rate
+ 27.78/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 0
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3225
+
+
+
+
+ Rate
+
+ 27.79/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 443.51 MB
+
+
+ Rate
+ 3.82 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.59 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3223 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3210 |
+ 13 |
+ 99.60 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3223 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3223 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28-summary.json b/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28-summary.json
new file mode 100644
index 0000000..d9fc524
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28-summary.json
@@ -0,0 +1,299 @@
+{
+ "root_group": {
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "passes": 1,
+ "fails": 5337,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7"
+ },
+ {
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c",
+ "passes": 5338,
+ "fails": 0
+ },
+ {
+ "passes": 5338,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d"
+ },
+ {
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945",
+ "passes": 0,
+ "fails": 5338
+ }
+ ],
+ "name": ""
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 203192.055
+ },
+ "metrics": {
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 3413736,
+ "rate": 16800.538780908533
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.999438202247191,
+ "passes": 5337,
+ "fails": 3
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 150,
+ "min": 150,
+ "max": 150
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 96.475,
+ "max": 165.079,
+ "p(90)": 151.3582,
+ "p(95)": 158.2186,
+ "avg": 89.54766666666667,
+ "min": 7.089
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "min": 2,
+ "max": 150,
+ "value": 2
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 4.236,
+ "max": 179.303,
+ "p(90)": 13.3801,
+ "p(95)": 18.184699999999992,
+ "avg": 6.535473408239702,
+ "min": 1.7
+ }
+ },
+ "checks": {
+ "values": {
+ "rate": 0.5000468340202323,
+ "passes": 10677,
+ "fails": 10675
+ },
+ "type": "rate",
+ "contains": "default"
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2689721,
+ "rate": 13237.333516805073
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9998126639190709,
+ "passes": 5337,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 179.383,
+ "p(90)": 13.5352,
+ "p(95)": 18.34454999999999,
+ "avg": 6.638881273408248,
+ "min": 1.73,
+ "med": 4.331
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(90)": 15,
+ "p(95)": 19,
+ "avg": 7.064818284001499,
+ "min": 2,
+ "med": 5,
+ "max": 180
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.041,
+ "avg": 0.34267172284643566,
+ "min": 0.001,
+ "med": 0.006,
+ "max": 37.11,
+ "p(90)": 0.018
+ }
+ },
+ "http_req_sending": {
+ "contains": "time",
+ "values": {
+ "p(90)": 0.035,
+ "p(95)": 0.049,
+ "avg": 0.024476029962547226,
+ "min": 0.005,
+ "med": 0.017,
+ "max": 1.916
+ },
+ "type": "trend"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.07893183520599262,
+ "min": 0.01,
+ "med": 0.0615,
+ "max": 4.597,
+ "p(90)": 0.132,
+ "p(95)": 0.17
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0,
+ "avg": 0.007665917602996256,
+ "min": 0,
+ "med": 0,
+ "max": 0.654,
+ "p(90)": 0
+ }
+ },
+ "iterations": {
+ "contains": "default",
+ "values": {
+ "rate": 26.270712208703237,
+ "count": 5338
+ },
+ "type": "counter"
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 36.552,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.3194936329588015,
+ "min": 0,
+ "med": 0
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5338,
+ "rate": 26.270712208703237
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 3491.0413120000003,
+ "max": 5034.807375,
+ "p(90)": 4690.7329125,
+ "p(95)": 4852.12512955,
+ "avg": 3487.629693233991,
+ "min": 2005.55375
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 26.28055511324003,
+ "count": 5340
+ }
+ },
+ "connection_pool_errors": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NzUwMSwidXNlcklkIjo1MDAxLCJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsImlhdCI6MTc3MjY3NjMwNSwiZXhwIjoxNzc1MjY4MzA1fQ.jrlwJgvdD3BnoXGr_jDyIYRxeAPkQHkh4uaBZnCOqKA",
+ "videoIds": [
+ "151"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28.html b/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28.html
new file mode 100644
index 0000000..851014e
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/before-cache-video-join-api-2026-03-05T02-08-28.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5340
+
+
+
+
+
+
+
+
Failed Requests
+
5337
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10675
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.34 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 37.11 |
+
+ 0.02 |
+
+ 0.04 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.65 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 6.64 |
+
+ 1.73 |
+
+ 4.33 |
+
+ 179.38 |
+
+ 13.54 |
+
+ 18.34 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.08 |
+
+ 0.01 |
+
+ 0.06 |
+
+ 4.60 |
+
+ 0.13 |
+
+ 0.17 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 1.92 |
+
+ 0.04 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.32 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 36.55 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 6.54 |
+
+ 1.70 |
+
+ 4.24 |
+
+ 179.30 |
+
+ 13.38 |
+
+ 18.18 |
+
+
+
+
+ | iteration_duration |
+
+ 3487.63 |
+
+ 2005.55 |
+
+ 3491.04 |
+
+ 5034.81 |
+
+ 4690.73 |
+
+ 4852.13 |
+
+
+
+
+ | video_join_api_duration |
+
+ 7.06 |
+
+ 2.00 |
+
+ 5.00 |
+
+ 180.00 |
+
+ 15.00 |
+
+ 19.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5337.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5337.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5338.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10677
+
+
+ Failed
+ 10675
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5338
+
+
+ Rate
+ 26.27/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 2
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5340
+
+
+
+
+ Rate
+
+ 26.28/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.41 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.69 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5337 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5338 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5338 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5338 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario2-cache/scenario2-cache-result.md b/k6-tests/results/scenario2-cache/scenario2-cache-result.md
new file mode 100644
index 0000000..8d04bf2
--- /dev/null
+++ b/k6-tests/results/scenario2-cache/scenario2-cache-result.md
@@ -0,0 +1,176 @@
+# 시나리오 2: Redis 캐시 최적화 부하 테스트 결과
+
+> 테스트 일시: 2026-03-05 11:01 ~ 11:19 (KST)
+> 테스트 환경: macOS (로컬), Spring Boot + PostgreSQL + Redis
+
+---
+
+## 1. 테스트 조건
+
+| 항목 | 값 |
+|------|---|
+| 테스트 도구 | k6 v0.55.0 |
+| DB 인덱스 | 20개 적용 상태 (인덱스 변수 통제) |
+| Before 조건 | `application-nocache.yml` 적용 (`app.cache.enabled=false`) |
+| After 조건 | 기본 `local` 프로필 (`app.cache.enabled=true`, 기본값) |
+| Redis 캐시 | 각 테스트 전 `FLUSHALL`로 초기화 (Cold Start) |
+
+### 캐시 구현 현황
+
+| 캐시 대상 | 키 패턴 | TTL | 캐싱 데이터 |
+|----------|--------|-----|-----------|
+| 홈 비디오 목록 | `home:{orgId}:{filter}` | 5분 | 비디오 목록 (id, title, thumbnail, creator 등) |
+| 비디오 상세 정보 | `video:{videoId}:info` | 10분 | 비디오 메타데이터 (title, description, watchCnt 등) |
+| 시청 세션 | `watch:{sessionId}` | 2시간 | 세션 멤버 ID (중복 시청 방지용) |
+
+> **캐시 미적용 항목**: History(시청기록) API는 Redis 캐시 레이어가 없음. 매 요청마다 DB 직접 조회.
+
+### 부하 설정
+
+| API | 최대 VU | 테스트 시간 |
+|-----|---------|-----------|
+| Home API | 100명 | 1분 50초 |
+| History API | 100명 | 1분 50초 |
+| Video Join API | 150명 | 3분 20초 |
+
+---
+
+## 2. 테스트 결과 비교
+
+### Home API (`GET /{orgId}/home?filter=RECENT|POPULAR|RECOMMEND`)
+
+| 지표 | Before (캐시 OFF) | After (캐시 ON) | 개선율 |
+|------|-------------------|----------------|--------|
+| **평균 응답시간** | 110.6ms | 28.1ms | **74.6% 감소** |
+| **중앙값 (p50)** | 21.1ms | 19.6ms | 7.1% 감소 |
+| **p90** | 222.5ms | 44.5ms | 80.0% 감소 |
+| **p95** | 613.9ms | 64.1ms | **89.6% 감소** |
+| **최대 응답시간** | 3,287ms | 422ms | 87.2% 감소 |
+| 처리량 (RPS) | 27.8/s | 29.5/s | 6.1% 증가 |
+| 에러율 | 0.00% | 0.00% | - |
+
+### History API (`GET /{orgId}/myactivity/video`)
+
+| 지표 | Before (캐시 OFF) | After (캐시 ON) | 개선율 |
+|------|-------------------|----------------|--------|
+| **평균 응답시간** | 9.5ms | 9.2ms | 3.2% 감소 |
+| **중앙값 (p50)** | 7.1ms | 7.1ms | 변화 없음 |
+| **p90** | 17.8ms | 16.7ms | 6.2% 감소 |
+| **p95** | 22.6ms | 21.5ms | **4.9% 감소** |
+| **최대 응답시간** | 109.0ms | 100.9ms | 7.4% 감소 |
+| 처리량 (RPS) | 30.1/s | 30.2/s | - |
+| 에러율 | 0.00% | 0.00% | - |
+
+### Video Join API (`POST /{orgId}/video/{videoId}/join`)
+
+| 지표 | Before (캐시 OFF) | After (캐시 ON) | 개선율 |
+|------|-------------------|----------------|--------|
+| **평균 응답시간** | 6.6ms | 6.7ms | 변화 없음 |
+| **중앙값 (p50)** | 4.3ms | 4.2ms | 변화 없음 |
+| **p90** | 13.5ms | 13.9ms | 변화 없음 |
+| **p95** | 18.3ms | 20.6ms | 변화 없음 |
+| **최대 응답시간** | 179.4ms | 127.8ms | 28.8% 감소 |
+| 처리량 (RPS) | 26.3/s | 26.1/s | - |
+| 에러율 | 99.94%* | 99.94%* | - |
+
+> *Video Join API의 에러율은 단일 사용자 반복 요청에 의한 409 Conflict. 비즈니스 로직상 정상.
+
+---
+
+## 3. 분석: 캐시 효과가 API별로 다른 이유
+
+### 3-1. Home API — 캐시 효과 극대화 (p95: 614ms → 64ms)
+
+Home API는 캐시의 효과가 가장 극적으로 나타난 API이다.
+
+**캐시 동작 흐름:**
+
+```
+[요청] GET /1/home?filter=RECENT
+ ↓
+[1] Redis에서 home:1:RECENT 키 조회
+ ├── Cache HIT → Redis 데이터로 즉시 응답 (DB 접근 없음)
+ └── Cache MISS → DB 쿼리 실행 → 결과를 Redis에 저장 (TTL: 5분) → 응답
+```
+
+**캐시가 효과적인 이유:**
+
+1. **높은 Cache Hit Rate**: 동일한 `orgId + filter` 조합은 3가지(RECENT, POPULAR, RECOMMEND)뿐. VU 100명이 3개 키에 집중하므로 첫 3회 요청 이후 **거의 100% Cache Hit**
+2. **비싼 DB 쿼리 회피**: Home API의 `findHomeVideos()`는 video + video_member_group_mapping + member_group_mapping + scrap 4개 테이블 JOIN 쿼리. 캐시 히트 시 이 전체 쿼리 생략
+3. **동시성 부하 흡수**: 캐시 OFF 상태에서 VU 100명이 동시에 같은 쿼리를 실행하면 **DB 커넥션 경합** 발생 → p95=614ms, max=3,287ms. 캐시 ON 시 대부분 Redis에서 서빙하므로 DB 부하 격리
+
+**중앙값(p50)이 비슷한 이유 (21.1ms vs 19.6ms):**
+- 동시 사용자 수가 적은 구간(Ramp-up 초반, Ramp-down 후반)에서는 캐시 OFF여도 DB 쿼리가 빠름 (인덱스 적용 상태)
+- **캐시의 핵심 가치는 평균 속도가 아닌 "고부하 시 Tail Latency 억제"**
+
+### 3-2. History API — 캐시 효과 없음 (p95: 22.6ms → 21.5ms)
+
+| 구분 | 설명 |
+|------|-----|
+| 캐시 레이어 | **없음** (Redis 캐시 미구현) |
+| 쿼리 경로 | 항상 DB 직접 조회: `historyRepository.findByMemberId()` |
+| Before/After 차이 | 오차 범위 내 (4.9% 차이는 통계적으로 유의미하지 않음) |
+
+History API의 `findByMemberId()`는 Redis 캐시가 구현되어 있지 않다. `cacheEnabled` 플래그와 무관하게 **매 요청마다 DB를 직접 조회**한다. Before/After 간 미세한 차이(~1ms)는 시스템 부하 변동에 의한 오차이다.
+
+> **시사점**: 인덱스(`idx_history_member_last_watched`)가 적용된 상태에서 History API는 이미 p95=22ms로 충분히 빠르기 때문에, 캐시 추가의 우선순위는 낮음.
+
+### 3-3. Video Join API — 캐시 효과 없음 (p95: 18.3ms → 20.6ms)
+
+Video Join API에는 2종류의 Redis 사용이 존재하지만, 테스트 조건에서 효과가 발현되지 않았다.
+
+| Redis 용도 | 키 패턴 | Before(nocache)에서 동작 | 효과 |
+|-----------|--------|------------------------|------|
+| 시청 세션 관리 | `watch:{sessionId}` | **동작함** (cacheEnabled와 무관) | Before/After 동일 |
+| 비디오 상세 캐시 | `video:{videoId}:info` | 동작 안함 | 첫 1회만 해당, 이후 409 |
+
+**캐시 효과가 없는 이유:**
+
+1. **시청 세션(`watch:*`)은 캐시 토글과 무관**: `createWatchSession()`과 `existsWatchSession()`은 `cacheEnabled` 조건 없이 항상 실행. Before/After 모두 동일하게 Redis 세션 확인
+2. **99.94%가 409 응답**: 첫 1회 요청만 비디오 정보 캐시를 사용, 이후 모든 요청은 세션 존재 확인 후 즉시 409 반환. 비디오 정보 캐시 로직에 도달하지 않음
+
+---
+
+## 4. 캐시 적용 범위와 효과 매트릭스
+
+| API | Redis 캐시 | 캐시 대상 | TTL | 효과 |
+|-----|-----------|----------|-----|------|
+| **Home API** | `home:{orgId}:{filter}` | 비디오 목록 (4테이블 JOIN 결과) | 5분 | **p95 89.6% 감소** |
+| **History API** | 없음 | - | - | 효과 없음 |
+| **Video Join API** | `video:{videoId}:info` | 비디오 메타데이터 | 10분 | 효과 미미 (409로 미도달) |
+| (공통) | `watch:{sessionId}` | 시청 세션 | 2시간 | Before/After 동일 동작 |
+
+---
+
+## 5. 핵심 개선 요약
+
+```
+Home API p95: 614ms → 64ms (89.6% 감소, ~9.6배 개선)
+History API p95: 22.6ms → 21.5ms (변화 없음, 캐시 미적용)
+Video Join p95: 18.3ms → 20.6ms (변화 없음, 409로 캐시 미도달)
+```
+
+---
+
+## 6. 캐시의 역할: "평균 속도 개선"이 아닌 "고부하 안정성 확보"
+
+| 지표 | Before (캐시 OFF) | After (캐시 ON) | 해석 |
+|------|-------------------|----------------|------|
+| Home p50 (중앙값) | 21.1ms | 19.6ms | **거의 동일** |
+| Home p95 | 613.9ms | 64.1ms | **9.6배 차이** |
+| Home max | 3,287ms | 422ms | **7.8배 차이** |
+
+중앙값은 거의 동일하지만, p95와 max에서 극적인 차이가 발생한다. 이는 다음을 의미한다:
+
+- **저부하 구간**: 인덱스만으로 충분히 빠름 (캐시 유무 무관)
+- **고부하 구간 (VU 80~100)**: 캐시 OFF 시 DB 커넥션 경합 → Tail Latency 급등 (p95=614ms, max=3.3s)
+- **캐시의 핵심 가치**: DB 커넥션 풀을 보호하여 **고부하 시에도 일관된 응답시간 보장**
+
+---
+
+## 7. 결론
+
+- Redis 캐시는 **다중 테이블 JOIN을 수행하는 Home API에서만 유의미한 개선** (p95 기준 9.6배)
+- 캐시의 핵심 가치는 평균 응답시간 단축이 아닌 **고부하 시 Tail Latency 억제와 DB 부하 격리**
+- History API는 캐시 레이어가 없어 개선 효과 없음 → 필요 시 별도 캐시 레이어 추가 고려
+- 동일 요청이 반복되는 API(Home API)일수록 캐시 효과가 극대화됨 (3개 filter 조합 → 높은 Hit Rate)
diff --git a/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26-summary.json b/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26-summary.json
new file mode 100644
index 0000000..f888e2a
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26-summary.json
@@ -0,0 +1,290 @@
+{
+ "root_group": {
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3367,
+ "fails": 0
+ },
+ {
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초",
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3367,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초"
+ },
+ {
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3367,
+ "fails": 0
+ },
+ {
+ "name": "시청 기록 조회 API JSON 파싱 가능",
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3367
+ },
+ {
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e",
+ "passes": 0,
+ "fails": 3367,
+ "name": "시청 기록 목록 반환"
+ }
+ ],
+ "name": ""
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112299.792
+ },
+ "metrics": {
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2011.8579396462703,
+ "min": 1008.779333,
+ "med": 2023.724625,
+ "max": 3088.334875,
+ "p(90)": 2816.8862,
+ "p(95)": 2909.8623454999997
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.062,
+ "max": 5.761,
+ "p(90)": 0.166,
+ "p(95)": 0.21859999999999985,
+ "avg": 0.09769100623330337,
+ "min": 0.014
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 11.469129118432717,
+ "min": 3.762,
+ "med": 7.035,
+ "max": 252.658,
+ "p(90)": 19.350200000000005,
+ "p(95)": 32.689999999999976
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3369
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "http_req_sending": {
+ "values": {
+ "p(95)": 0.045,
+ "avg": 0.026857821311962318,
+ "min": 0.004,
+ "med": 0.015,
+ "max": 2.957,
+ "p(90)": 0.033
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "min": 100,
+ "max": 100,
+ "value": 100
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3367,
+ "rate": 29.982246093563557
+ }
+ },
+ "http_req_connecting": {
+ "contains": "time",
+ "values": {
+ "max": 1.809,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.008969427129712078,
+ "min": 0,
+ "med": 0
+ },
+ "type": "trend"
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3367
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 3.809,
+ "med": 7.13,
+ "max": 252.758,
+ "p(90)": 19.469,
+ "p(95)": 33.2296,
+ "avg": 11.5936779459781
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "min": 4,
+ "med": 7,
+ "max": 253,
+ "p(90)": 21,
+ "p(95)": 34.699999999999854,
+ "avg": 12.056430056430056
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.39582338972988007,
+ "min": 0.001,
+ "med": 0.004,
+ "max": 46.868,
+ "p(90)": 0.012,
+ "p(95)": 0.025
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1645276,
+ "rate": 14650.74841812708
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 29.982246093563557,
+ "count": 3367
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "rate": 339440.75337200984,
+ "count": 38119126
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "contains": "time",
+ "values": {
+ "p(90)": 19.469,
+ "p(95)": 33.2296,
+ "avg": 11.5936779459781,
+ "min": 3.809,
+ "med": 7.13,
+ "max": 252.758
+ },
+ "type": "trend"
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3369,
+ "rate": 30.000055565552607
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 44.969,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.37050133570792515,
+ "min": 0
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 6734,
+ "rate": 0.6,
+ "passes": 10101
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODI1NCwiZXhwIjoxNzc1MjcwMjU0fQ.rAJo05M9c3KOO_qIOSTYzRWcQROhYVcFbB2ygpijQq4"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26.html b/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26.html
new file mode 100644
index 0000000..4eb5f14
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-history-api-2026-03-05T02-39-26.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3369
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6734
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 12.06 |
+
+ 4.00 |
+
+ 7.00 |
+
+ 253.00 |
+
+ 21.00 |
+
+ 34.70 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.40 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 46.87 |
+
+ 0.01 |
+
+ 0.03 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 1.81 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 11.59 |
+
+ 3.81 |
+
+ 7.13 |
+
+ 252.76 |
+
+ 19.47 |
+
+ 33.23 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.10 |
+
+ 0.01 |
+
+ 0.06 |
+
+ 5.76 |
+
+ 0.17 |
+
+ 0.22 |
+
+
+
+
+ | http_req_sending |
+
+ 0.03 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 2.96 |
+
+ 0.03 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.37 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 44.97 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 11.47 |
+
+ 3.76 |
+
+ 7.04 |
+
+ 252.66 |
+
+ 19.35 |
+
+ 32.69 |
+
+
+
+
+ | iteration_duration |
+
+ 2011.86 |
+
+ 1008.78 |
+
+ 2023.72 |
+
+ 3088.33 |
+
+ 2816.89 |
+
+ 2909.86 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3367.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3369.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3367.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10101
+
+
+ Failed
+ 6734
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3367
+
+
+ Rate
+ 29.98/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3369
+
+
+
+
+ Rate
+
+ 30.00/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 38.12 MB
+
+
+ Rate
+ 0.34 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.65 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3367 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3367 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3367 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3367 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3367 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24-summary.json b/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24-summary.json
new file mode 100644
index 0000000..ea96801
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24-summary.json
@@ -0,0 +1,283 @@
+{
+ "root_group": {
+ "groups": [],
+ "checks": [
+ {
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3327,
+ "fails": 0
+ },
+ {
+ "fails": 0,
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3327
+ },
+ {
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3327,
+ "fails": 0,
+ "name": "홈 조회 API 응답 본문 존재"
+ },
+ {
+ "passes": 0,
+ "fails": 3327,
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능",
+ "id": "c11c9388b82c69a51e689a6927384a13"
+ }
+ ],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e"
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112776.519
+ },
+ "metrics": {
+ "data_sent": {
+ "contains": "data",
+ "values": {
+ "count": 1638652,
+ "rate": 14530.08139043554
+ },
+ "type": "counter"
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3327,
+ "rate": 29.500821886513453
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 30.33606067888249,
+ "min": 8.018,
+ "med": 12.147,
+ "max": 931.924,
+ "p(90)": 41.122800000000005,
+ "p(95)": 81.61899999999999
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3327,
+ "rate": 29.500821886513453
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 3329,
+ "rate": 0,
+ "passes": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3329,
+ "rate": 29.51855607460273
+ }
+ },
+ "vus_max": {
+ "contains": "default",
+ "values": {
+ "max": 100,
+ "value": 100,
+ "min": 100
+ },
+ "type": "gauge"
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "max": 1247,
+ "p(90)": 42.40000000000008,
+ "p(95)": 82,
+ "avg": 31.21761346558461,
+ "min": 8,
+ "med": 13
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 437.32,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.750632021628116
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 0.011,
+ "p(95)": 0.022,
+ "avg": 0.7794992490237348,
+ "min": 0.001,
+ "med": 0.005,
+ "max": 438.147
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.04298798437969349,
+ "min": 0.004,
+ "med": 0.015,
+ "max": 37.879,
+ "p(90)": 0.034,
+ "p(95)": 0.049
+ }
+ },
+ "http_req_duration": {
+ "values": {
+ "p(95)": 81.61899999999999,
+ "avg": 30.33606067888249,
+ "min": 8.018,
+ "med": 12.147,
+ "max": 931.924,
+ "p(90)": 41.122800000000005
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus": {
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ },
+ "type": "gauge"
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3327
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_waiting": {
+ "values": {
+ "avg": 25.08242174827285,
+ "min": 4.978,
+ "med": 8.512,
+ "max": 904.068,
+ "p(90)": 35.02540000000002,
+ "p(95)": 72.30499999999995
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 5.2106509462301,
+ "min": 0.103,
+ "med": 3.317,
+ "max": 393.167,
+ "p(90)": 6.639600000000001,
+ "p(95)": 9.306599999999998
+ }
+ },
+ "data_received": {
+ "values": {
+ "count": 490825801,
+ "rate": 4352198.537002193
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.75,
+ "passes": 9981,
+ "fails": 3327
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0,
+ "max": 13.414,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.01735145689396215,
+ "min": 0
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 1016.01875,
+ "med": 2034.563292,
+ "max": 3614.4895,
+ "p(90)": 2847.7762996,
+ "p(95)": 2946.3564,
+ "avg": 2040.162148361892
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODEzMSwiZXhwIjoxNzc1MjcwMTMxfQ.dVwLv5OlRBJEeepev36PvB2hUaG9PtMx43BhuGxG9oA"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24.html b/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24.html
new file mode 100644
index 0000000..f940fa3
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-home-api-2026-03-05T02-37-24.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3329
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3327
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 31.22 |
+
+ 8.00 |
+
+ 13.00 |
+
+ 1247.00 |
+
+ 42.40 |
+
+ 82.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.78 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 438.15 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 13.41 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 30.34 |
+
+ 8.02 |
+
+ 12.15 |
+
+ 931.92 |
+
+ 41.12 |
+
+ 81.62 |
+
+
+
+
+ | http_req_receiving |
+
+ 5.21 |
+
+ 0.10 |
+
+ 3.32 |
+
+ 393.17 |
+
+ 6.64 |
+
+ 9.31 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 37.88 |
+
+ 0.03 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.75 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 437.32 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 25.08 |
+
+ 4.98 |
+
+ 8.51 |
+
+ 904.07 |
+
+ 35.03 |
+
+ 72.30 |
+
+
+
+
+ | iteration_duration |
+
+ 2040.16 |
+
+ 1016.02 |
+
+ 2034.56 |
+
+ 3614.49 |
+
+ 2847.78 |
+
+ 2946.36 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3327.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3329.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3327.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9981
+
+
+ Failed
+ 3327
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3327
+
+
+ Rate
+ 29.50/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3329
+
+
+
+
+ Rate
+
+ 29.52/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 490.83 MB
+
+
+ Rate
+ 4.35 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3327 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3327 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3327 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3327 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59-summary.json b/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59-summary.json
new file mode 100644
index 0000000..d7b222d
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59-summary.json
@@ -0,0 +1,299 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "id": "b292b3b10176b79638a488dedde259f7",
+ "passes": 1,
+ "fails": 5280,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201"
+ },
+ {
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c",
+ "passes": 5281,
+ "fails": 0
+ },
+ {
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d",
+ "passes": 5281,
+ "fails": 0
+ },
+ {
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945",
+ "passes": 0,
+ "fails": 5281
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 203455.139
+ },
+ "metrics": {
+ "http_req_duration": {
+ "values": {
+ "med": 4.028,
+ "max": 139.549,
+ "p(90)": 10.991800000000003,
+ "p(95)": 17.2899,
+ "avg": 6.351199129282619,
+ "min": 1.79
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5281,
+ "rate": 25.956582006021485
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 150
+ }
+ },
+ "connection_pool_errors": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_failed": {
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9994321408290744,
+ "passes": 5280,
+ "fails": 3
+ }
+ },
+ "iteration_duration": {
+ "values": {
+ "avg": 3531.086046842068,
+ "min": 2004.957833,
+ "med": 3548.895458,
+ "max": 5035.975917,
+ "p(90)": 4707.444709,
+ "p(95)": 4858.850167
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "data_sent": {
+ "values": {
+ "count": 2663558,
+ "rate": 13091.623112061081
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 10561,
+ "rate": 0.5000473395190305,
+ "passes": 10563
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.999810641923878,
+ "passes": 5280,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 0.033,
+ "p(95)": 0.047,
+ "avg": 0.029352072685974156,
+ "min": 0.004,
+ "med": 0.016,
+ "max": 5.099
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "values": {
+ "med": 86.48,
+ "max": 118.022,
+ "p(90)": 111.7136,
+ "p(95)": 114.8678,
+ "avg": 70.976,
+ "min": 8.426
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 3.952,
+ "max": 139.397,
+ "p(90)": 10.8094,
+ "p(95)": 17.161999999999992,
+ "avg": 6.239330872610262,
+ "min": 1.745
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 0.051,
+ "max": 16.605,
+ "p(90)": 0.12,
+ "p(95)": 0.16189999999999977,
+ "avg": 0.08251618398637123,
+ "min": 0.012
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5283,
+ "rate": 25.96641218288421
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 50.83,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.3580946431951545
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5281,
+ "rate": 25.956582006021485
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 3379992,
+ "rate": 16612.959577295318
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "min": 150,
+ "max": 150,
+ "value": 150
+ }
+ },
+ "http_req_blocked": {
+ "contains": "time",
+ "values": {
+ "avg": 0.38396289986748894,
+ "min": 0.001,
+ "med": 0.005,
+ "max": 51.355,
+ "p(90)": 0.017,
+ "p(95)": 0.038
+ },
+ "type": "trend"
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.008082718152564833,
+ "min": 0,
+ "med": 0,
+ "max": 0.572,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "video_join_api_duration": {
+ "values": {
+ "max": 139,
+ "p(90)": 13,
+ "p(95)": 19,
+ "avg": 6.828630941109639,
+ "min": 2,
+ "med": 4
+ },
+ "type": "trend",
+ "contains": "default"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODM3NiwiZXhwIjoxNzc1MjcwMzc2fQ.bjnAg_1KUwEOwHni1r2Mmd673dU_0s8go2XeCRGaCY0",
+ "videoIds": [
+ "151"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59.html b/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59.html
new file mode 100644
index 0000000..6e95e0f
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-10-video-join-api-2026-03-05T02-42-59.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5283
+
+
+
+
+
+
+
+
Failed Requests
+
5280
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10561
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.38 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 51.35 |
+
+ 0.02 |
+
+ 0.04 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 0.57 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 6.35 |
+
+ 1.79 |
+
+ 4.03 |
+
+ 139.55 |
+
+ 10.99 |
+
+ 17.29 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.08 |
+
+ 0.01 |
+
+ 0.05 |
+
+ 16.61 |
+
+ 0.12 |
+
+ 0.16 |
+
+
+
+
+ | http_req_sending |
+
+ 0.03 |
+
+ 0.00 |
+
+ 0.02 |
+
+ 5.10 |
+
+ 0.03 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.36 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 50.83 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 6.24 |
+
+ 1.75 |
+
+ 3.95 |
+
+ 139.40 |
+
+ 10.81 |
+
+ 17.16 |
+
+
+
+
+ | iteration_duration |
+
+ 3531.09 |
+
+ 2004.96 |
+
+ 3548.90 |
+
+ 5035.98 |
+
+ 4707.44 |
+
+ 4858.85 |
+
+
+
+
+ | video_join_api_duration |
+
+ 6.83 |
+
+ 2.00 |
+
+ 4.00 |
+
+ 139.00 |
+
+ 13.00 |
+
+ 19.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5280.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5280.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5281.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10563
+
+
+ Failed
+ 10561
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5281
+
+
+ Rate
+ 25.96/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5283
+
+
+
+
+ Rate
+
+ 25.97/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.38 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.66 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5280 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5281 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5281 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5281 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14-summary.json b/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14-summary.json
new file mode 100644
index 0000000..4303b3a
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14-summary.json
@@ -0,0 +1,290 @@
+{
+ "root_group": {
+ "checks": [
+ {
+ "passes": 3348,
+ "fails": 0,
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02"
+ },
+ {
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3348,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초"
+ },
+ {
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3348
+ },
+ {
+ "name": "시청 기록 조회 API JSON 파싱 가능",
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0,
+ "fails": 3348
+ },
+ {
+ "passes": 0,
+ "fails": 3348,
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e"
+ }
+ ],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": []
+ },
+ "options": {
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": ""
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112649.088
+ },
+ "metrics": {
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 8.816500000000001,
+ "max": 412.466,
+ "p(90)": 38.8254,
+ "p(95)": 63.98829999999998,
+ "avg": 18.19243791044775,
+ "min": 3.953
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_tls_handshaking": {
+ "values": {
+ "avg": 0.8369537313432838,
+ "min": 0,
+ "med": 0,
+ "max": 258.818,
+ "p(90)": 0,
+ "p(95)": 0
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_sending": {
+ "values": {
+ "avg": 0.04079791044776134,
+ "min": 0.004,
+ "med": 0.019,
+ "max": 4.505,
+ "p(90)": 0.052,
+ "p(95)": 0.08854999999999985
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus_max": {
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ },
+ "type": "gauge",
+ "contains": "default"
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1636897,
+ "rate": 14530.93876800849
+ }
+ },
+ "data_received": {
+ "contains": "data",
+ "values": {
+ "count": 37904977,
+ "rate": 336487.2070690887
+ },
+ "type": "counter"
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3348,
+ "rate": 29.720613450505695
+ }
+ },
+ "http_req_failed": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3350
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.14941194029850713,
+ "min": 0.016,
+ "med": 0.087,
+ "max": 9.456,
+ "p(90)": 0.215,
+ "p(95)": 0.31354999999999983
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3348,
+ "rate": 29.720613450505695
+ }
+ },
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(95)": 69,
+ "avg": 19.140083632019117,
+ "min": 4,
+ "med": 9,
+ "max": 343,
+ "p(90)": 41
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3350,
+ "rate": 29.738367699878758
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 38.8254,
+ "p(95)": 63.98829999999998,
+ "avg": 18.19243791044775,
+ "min": 3.953,
+ "med": 8.816500000000001,
+ "max": 412.466
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 3.883,
+ "med": 8.65,
+ "max": 412.263,
+ "p(90)": 38.5405,
+ "p(95)": 63.77729999999998,
+ "avg": 18.00222805970153
+ }
+ },
+ "checks": {
+ "contains": "default",
+ "values": {
+ "rate": 0.6,
+ "passes": 10044,
+ "fails": 6696
+ },
+ "type": "rate"
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 2029.034396,
+ "max": 3119.958375,
+ "p(90)": 2816.9276081,
+ "p(95)": 2916.4719169,
+ "avg": 2022.9566770860215,
+ "min": 1008.519292
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 100
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.8915611940298596,
+ "min": 0.001,
+ "med": 0.006,
+ "max": 259.193,
+ "p(90)": 0.022,
+ "p(95)": 0.1
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3348
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_connecting": {
+ "values": {
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.023771641791044774,
+ "min": 0,
+ "med": 0,
+ "max": 15.283
+ },
+ "type": "trend",
+ "contains": "time"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3NzY0MiwiZXhwIjoxNzc1MjY5NjQyfQ.Ju0zQCE_kF8cQK5WbOciNpV3DY_7Fe8e2Ju7MYYMbFw"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14.html b/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14.html
new file mode 100644
index 0000000..dd40c91
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-history-api-2026-03-05T02-29-14.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3350
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6696
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 19.14 |
+
+ 4.00 |
+
+ 9.00 |
+
+ 343.00 |
+
+ 41.00 |
+
+ 69.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.89 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 259.19 |
+
+ 0.02 |
+
+ 0.10 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 15.28 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 18.19 |
+
+ 3.95 |
+
+ 8.82 |
+
+ 412.47 |
+
+ 38.83 |
+
+ 63.99 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.15 |
+
+ 0.02 |
+
+ 0.09 |
+
+ 9.46 |
+
+ 0.21 |
+
+ 0.31 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.02 |
+
+ 4.50 |
+
+ 0.05 |
+
+ 0.09 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.84 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 258.82 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 18.00 |
+
+ 3.88 |
+
+ 8.65 |
+
+ 412.26 |
+
+ 38.54 |
+
+ 63.78 |
+
+
+
+
+ | iteration_duration |
+
+ 2022.96 |
+
+ 1008.52 |
+
+ 2029.03 |
+
+ 3119.96 |
+
+ 2816.93 |
+
+ 2916.47 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3348.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3350.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3348.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10044
+
+
+ Failed
+ 6696
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3348
+
+
+ Rate
+ 29.72/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3350
+
+
+
+
+ Rate
+
+ 29.74/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 37.90 MB
+
+
+ Rate
+ 0.34 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3348 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3348 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3348 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3348 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3348 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11-summary.json b/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11-summary.json
new file mode 100644
index 0000000..2897607
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11-summary.json
@@ -0,0 +1,283 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "passes": 3351,
+ "fails": 0,
+ "name": "홈 조회 API 상태 코드 200",
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7"
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3351,
+ "fails": 0
+ },
+ {
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재",
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3351,
+ "fails": 0
+ },
+ {
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3351,
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능"
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 113537.509
+ },
+ "metrics": {
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.4670280345958843,
+ "min": 0,
+ "med": 0,
+ "max": 56.186,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 47.6376,
+ "p(95)": 66.3902,
+ "avg": 21.40788547569342,
+ "min": 5.128,
+ "med": 11.864,
+ "max": 997.432
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 1015.322875,
+ "med": 2010.085292,
+ "max": 3563.347375,
+ "p(90)": 2836.966333,
+ "p(95)": 2931.038375,
+ "avg": 2025.194641040884
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 26.178573218013778,
+ "min": 8.101,
+ "med": 16.236,
+ "max": 1016.996,
+ "p(90)": 54.3472,
+ "p(95)": 75.69779999999996
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_sending": {
+ "values": {
+ "avg": 0.03326722338204633,
+ "min": 0.004,
+ "med": 0.017,
+ "max": 7.662,
+ "p(90)": 0.035,
+ "p(95)": 0.05
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 3353,
+ "rate": 0,
+ "passes": 0
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1649479,
+ "rate": 14528.053455884787
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 8.464,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.01884193259767373
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3351,
+ "rate": 29.514475255926214
+ }
+ },
+ "http_req_receiving": {
+ "values": {
+ "avg": 4.737420518938267,
+ "min": 0.08,
+ "med": 3.636,
+ "max": 83.63,
+ "p(90)": 7.2854,
+ "p(95)": 9.3924
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3351,
+ "rate": 29.514475255926214
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "med": 17,
+ "max": 1038,
+ "p(90)": 55,
+ "p(95)": 77,
+ "avg": 26.470904207699196,
+ "min": 8
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0.001,
+ "med": 0.005,
+ "max": 57.837,
+ "p(90)": 0.012,
+ "p(95)": 0.024,
+ "avg": 0.4972857142857135
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 10053,
+ "fails": 3351,
+ "rate": 0.75
+ }
+ },
+ "errors": {
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ },
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3351
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3353,
+ "rate": 29.532090579862906
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 3,
+ "min": 0,
+ "max": 100
+ }
+ },
+ "data_received": {
+ "values": {
+ "count": 494365465,
+ "rate": 4354203.904544004
+ },
+ "type": "counter",
+ "contains": "data"
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 54.3472,
+ "p(95)": 75.69779999999996,
+ "avg": 26.178573218013778,
+ "min": 8.101,
+ "med": 16.236,
+ "max": 1016.996
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3NzUxOSwiZXhwIjoxNzc1MjY5NTE5fQ.rQa5YbSwJhTgy5lb6BEdnTdbVlLAubQ7ojwBKveLl6Y"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11.html b/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11.html
new file mode 100644
index 0000000..b9f85d1
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-home-api-2026-03-05T02-27-11.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3353
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3351
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 26.47 |
+
+ 8.00 |
+
+ 17.00 |
+
+ 1038.00 |
+
+ 55.00 |
+
+ 77.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.50 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 57.84 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 8.46 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 26.18 |
+
+ 8.10 |
+
+ 16.24 |
+
+ 1017.00 |
+
+ 54.35 |
+
+ 75.70 |
+
+
+
+
+ | http_req_receiving |
+
+ 4.74 |
+
+ 0.08 |
+
+ 3.64 |
+
+ 83.63 |
+
+ 7.29 |
+
+ 9.39 |
+
+
+
+
+ | http_req_sending |
+
+ 0.03 |
+
+ 0.00 |
+
+ 0.02 |
+
+ 7.66 |
+
+ 0.04 |
+
+ 0.05 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.47 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 56.19 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 21.41 |
+
+ 5.13 |
+
+ 11.86 |
+
+ 997.43 |
+
+ 47.64 |
+
+ 66.39 |
+
+
+
+
+ | iteration_duration |
+
+ 2025.19 |
+
+ 1015.32 |
+
+ 2010.09 |
+
+ 3563.35 |
+
+ 2836.97 |
+
+ 2931.04 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3351.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3353.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3351.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10053
+
+
+ Failed
+ 3351
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3351
+
+
+ Rate
+ 29.51/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 0
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3353
+
+
+
+
+ Rate
+
+ 29.53/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 494.37 MB
+
+
+ Rate
+ 4.35 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.65 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3351 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3351 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3351 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3351 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44-summary.json b/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44-summary.json
new file mode 100644
index 0000000..32325d0
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44-summary.json
@@ -0,0 +1,299 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "passes": 1,
+ "fails": 5301,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7"
+ },
+ {
+ "passes": 5302,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c"
+ },
+ {
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재",
+ "id": "af400638963cc60ef15d8a9793e15a5d",
+ "passes": 5302,
+ "fails": 0
+ },
+ {
+ "passes": 0,
+ "fails": 5302,
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능",
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945"
+ }
+ ]
+ },
+ "options": {
+ "summaryTimeUnit": "",
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ]
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 204445.316
+ },
+ "metrics": {
+ "vus": {
+ "values": {
+ "min": 1,
+ "max": 150,
+ "value": 1
+ },
+ "type": "gauge",
+ "contains": "default"
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "p(90)": 15,
+ "p(95)": 21,
+ "avg": 7.26235382874387,
+ "min": 2,
+ "med": 4,
+ "max": 355
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "rate": 16593.30752287815,
+ "count": 3392424
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 180.5583333333333,
+ "min": 35.693,
+ "med": 163.766,
+ "max": 342.216,
+ "p(90)": 306.526,
+ "p(95)": 324.371
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 2673197,
+ "rate": 13075.364367848859
+ }
+ },
+ "connection_pool_errors": {
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ },
+ "type": "counter"
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.9998113919275745,
+ "passes": 5301,
+ "fails": 1
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.3712332202111545,
+ "min": 0.001,
+ "med": 0.006,
+ "max": 39.292,
+ "p(90)": 0.02,
+ "p(95)": 0.05
+ }
+ },
+ "iteration_duration": {
+ "values": {
+ "p(90)": 4728.2742661,
+ "p(95)": 4869.34979645,
+ "avg": 3514.808983810452,
+ "min": 2004.400166,
+ "med": 3520.479229,
+ "max": 5029.866917
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5302,
+ "rate": 25.9335850961731
+ }
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 5301,
+ "fails": 3,
+ "rate": 0.9994343891402715
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0,
+ "avg": 0.3418193815987935,
+ "min": 0,
+ "med": 0,
+ "max": 36.918,
+ "p(90)": 0
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0.011,
+ "med": 0.066,
+ "max": 3.042,
+ "p(90)": 0.141,
+ "p(95)": 0.179,
+ "avg": 0.08651150075414789
+ }
+ },
+ "http_req_duration": {
+ "thresholds": {
+ "p(99)<5000": {
+ "ok": true
+ },
+ "p(95)<3000": {
+ "ok": true
+ }
+ },
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 6.812947775263927,
+ "min": 1.86,
+ "med": 4.2435,
+ "max": 342.216,
+ "p(90)": 13.061900000000007,
+ "p(95)": 19.18309999999999
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "rate": 25.943367663165247,
+ "count": 5304
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.010571644042232278,
+ "min": 0,
+ "med": 0,
+ "max": 5.458,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "total_requests": {
+ "contains": "default",
+ "values": {
+ "count": 5302,
+ "rate": 25.9335850961731
+ },
+ "type": "counter"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 150,
+ "min": 150,
+ "max": 150
+ }
+ },
+ "http_req_sending": {
+ "contains": "time",
+ "values": {
+ "med": 0.017,
+ "max": 24.198,
+ "p(90)": 0.041,
+ "p(95)": 0.062,
+ "avg": 0.03511161387631955,
+ "min": 0.004
+ },
+ "type": "trend"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 10605,
+ "fails": 10603,
+ "rate": 0.5000471520181063
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 19.03765,
+ "avg": 6.691324660633483,
+ "min": 1.828,
+ "med": 4.1475,
+ "max": 341.921,
+ "p(90)": 12.813400000000005
+ }
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3Nzc2MCwiZXhwIjoxNzc1MjY5NzYwfQ.wXWKGw0DJO4NG-ILG_475ktOd5pAKOWxBS0gbeYWeZA",
+ "videoIds": [
+ "151"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44.html b/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44.html
new file mode 100644
index 0000000..b1e19bc
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-5-video-join-api-2026-03-05T02-32-44.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5304
+
+
+
+
+
+
+
+
Failed Requests
+
5301
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10603
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.37 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 39.29 |
+
+ 0.02 |
+
+ 0.05 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 5.46 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 6.81 |
+
+ 1.86 |
+
+ 4.24 |
+
+ 342.22 |
+
+ 13.06 |
+
+ 19.18 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.09 |
+
+ 0.01 |
+
+ 0.07 |
+
+ 3.04 |
+
+ 0.14 |
+
+ 0.18 |
+
+
+
+
+ | http_req_sending |
+
+ 0.04 |
+
+ 0.00 |
+
+ 0.02 |
+
+ 24.20 |
+
+ 0.04 |
+
+ 0.06 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.34 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 36.92 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 6.69 |
+
+ 1.83 |
+
+ 4.15 |
+
+ 341.92 |
+
+ 12.81 |
+
+ 19.04 |
+
+
+
+
+ | iteration_duration |
+
+ 3514.81 |
+
+ 2004.40 |
+
+ 3520.48 |
+
+ 5029.87 |
+
+ 4728.27 |
+
+ 4869.35 |
+
+
+
+
+ | video_join_api_duration |
+
+ 7.26 |
+
+ 2.00 |
+
+ 4.00 |
+
+ 355.00 |
+
+ 15.00 |
+
+ 21.00 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5301.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5301.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5302.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10605
+
+
+ Failed
+ 10603
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5302
+
+
+ Rate
+ 25.93/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5304
+
+
+
+
+ Rate
+
+ 25.94/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.39 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.67 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5301 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5302 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5302 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5302 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59-summary.json b/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59-summary.json
new file mode 100644
index 0000000..260f231
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59-summary.json
@@ -0,0 +1,290 @@
+{
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODc2NywiZXhwIjoxNzc1MjcwNzY3fQ.qa3-vegVUFQD0I2I0fAoANRQkH1RzbHTPoPjgCiqYjg"
+ },
+ "root_group": {
+ "checks": [
+ {
+ "name": "시청 기록 조회 API 상태 코드 200",
+ "path": "::시청 기록 조회 API 상태 코드 200",
+ "id": "bebcd16d9c180175771f573c68668e02",
+ "passes": 3375,
+ "fails": 0
+ },
+ {
+ "id": "cf9c2c515f41b2607d24aaa76ba24e8d",
+ "passes": 3375,
+ "fails": 0,
+ "name": "시청 기록 조회 API 응답 시간 < 1.5초",
+ "path": "::시청 기록 조회 API 응답 시간 < 1.5초"
+ },
+ {
+ "name": "시청 기록 조회 API 응답 본문 존재",
+ "path": "::시청 기록 조회 API 응답 본문 존재",
+ "id": "a4726479ed9e054806b2394d1e3245d4",
+ "passes": 3375,
+ "fails": 0
+ },
+ {
+ "fails": 3375,
+ "name": "시청 기록 조회 API JSON 파싱 가능",
+ "path": "::시청 기록 조회 API JSON 파싱 가능",
+ "id": "4273e24be3eb6c2c92b46598fc66628e",
+ "passes": 0
+ },
+ {
+ "name": "시청 기록 목록 반환",
+ "path": "::시청 기록 목록 반환",
+ "id": "3795741ae94c333f94ea574c9e3cb95e",
+ "passes": 0,
+ "fails": 3375
+ }
+ ],
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": []
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112726.439
+ },
+ "metrics": {
+ "history_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "med": 7,
+ "max": 130,
+ "p(90)": 18,
+ "p(95)": 23,
+ "avg": 9.75911111111111,
+ "min": 4
+ }
+ },
+ "iteration_duration": {
+ "values": {
+ "p(90)": 2807.2585672,
+ "p(95)": 2914.4599212,
+ "avg": 2009.8993515549716,
+ "min": 1008.751041,
+ "med": 2011.202833,
+ "max": 3018.713542
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3375,
+ "rate": 29.939737562365472
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.37577790938701866,
+ "min": 0.001,
+ "med": 0.004,
+ "max": 31.292,
+ "p(90)": 0.011,
+ "p(95)": 0.025
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3377,
+ "rate": 29.957479629069095
+ }
+ },
+ "http_req_connecting": {
+ "values": {
+ "max": 3.785,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.011278649689073144,
+ "min": 0,
+ "med": 0
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 9.21764228605269,
+ "min": 3.852,
+ "med": 7.053,
+ "max": 136.386,
+ "p(90)": 16.1964,
+ "p(95)": 21.362999999999992
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "fails": 6750,
+ "rate": 0.6,
+ "passes": 10125
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3375
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.35430085875037026,
+ "min": 0,
+ "med": 0,
+ "max": 30.651,
+ "p(90)": 0,
+ "p(95)": 0
+ }
+ },
+ "http_req_sending": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 0.022207284572105718,
+ "min": 0.004,
+ "med": 0.015,
+ "max": 2.765,
+ "p(90)": 0.032,
+ "p(95)": 0.043
+ }
+ },
+ "vus": {
+ "contains": "default",
+ "values": {
+ "max": 100,
+ "value": 1,
+ "min": 1
+ },
+ "type": "gauge"
+ },
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3377
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1648804,
+ "rate": 14626.595274601019
+ }
+ },
+ "http_req_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 21.5888,
+ "avg": 9.332103938406867,
+ "min": 3.883,
+ "med": 7.145,
+ "max": 136.636,
+ "p(90)": 16.3276
+ },
+ "thresholds": {
+ "p(95)<1500": {
+ "ok": true
+ },
+ "p(99)<3000": {
+ "ok": true
+ }
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "med": 7.145,
+ "max": 136.636,
+ "p(90)": 16.3276,
+ "p(95)": 21.5888,
+ "avg": 9.332103938406867,
+ "min": 3.883
+ }
+ },
+ "http_req_receiving": {
+ "values": {
+ "min": 0.012,
+ "med": 0.062,
+ "max": 3.325,
+ "p(90)": 0.159,
+ "p(95)": 0.2063999999999997,
+ "avg": 0.09225436778205504
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "min": 100,
+ "max": 100,
+ "value": 100
+ }
+ },
+ "total_requests": {
+ "values": {
+ "count": 3375,
+ "rate": 29.939737562365472
+ },
+ "type": "counter",
+ "contains": "default"
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 38209294,
+ "rate": 338955.9214231898
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59.html b/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59.html
new file mode 100644
index 0000000..0f5b0ac
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-history-api-2026-03-05T02-47-59.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 시청 기록 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3377
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
6750
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | history_api_duration |
+
+ 9.76 |
+
+ 4.00 |
+
+ 7.00 |
+
+ 130.00 |
+
+ 18.00 |
+
+ 23.00 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.38 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 31.29 |
+
+ 0.01 |
+
+ 0.03 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 3.79 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 9.33 |
+
+ 3.88 |
+
+ 7.14 |
+
+ 136.64 |
+
+ 16.33 |
+
+ 21.59 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.09 |
+
+ 0.01 |
+
+ 0.06 |
+
+ 3.33 |
+
+ 0.16 |
+
+ 0.21 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 2.77 |
+
+ 0.03 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.35 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 30.65 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 9.22 |
+
+ 3.85 |
+
+ 7.05 |
+
+ 136.39 |
+
+ 16.20 |
+
+ 21.36 |
+
+
+
+
+ | iteration_duration |
+
+ 2009.90 |
+
+ 1008.75 |
+
+ 2011.20 |
+
+ 3018.71 |
+
+ 2807.26 |
+
+ 2914.46 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3375.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3377.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3375.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10125
+
+
+ Failed
+ 6750
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3375
+
+
+ Rate
+ 29.94/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3377
+
+
+
+
+ Rate
+
+ 29.96/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 38.21 MB
+
+
+ Rate
+ 0.34 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.65 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 시청 기록 조회 API 상태 코드 200 |
+ 3375 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 시간 < 1.5초 |
+ 3375 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API 응답 본문 존재 |
+ 3375 |
+ 0 |
+ 100.00 |
+
+
+
+ | 시청 기록 조회 API JSON 파싱 가능 |
+ 0 |
+ 3375 |
+ 0.00 |
+
+
+
+ | 시청 기록 목록 반환 |
+ 0 |
+ 3375 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01-summary.json b/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01-summary.json
new file mode 100644
index 0000000..2eba320
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01-summary.json
@@ -0,0 +1,283 @@
+{
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "path": "::홈 조회 API 상태 코드 200",
+ "id": "898303d2534cabc104689c837854e0e7",
+ "passes": 3324,
+ "fails": 0,
+ "name": "홈 조회 API 상태 코드 200"
+ },
+ {
+ "name": "홈 조회 API 응답 시간 < 2초",
+ "path": "::홈 조회 API 응답 시간 < 2초",
+ "id": "aca2e590bde5d77e2c969686c2e4379b",
+ "passes": 3324,
+ "fails": 0
+ },
+ {
+ "id": "ee7ecadff0ac3d012dd8052a0aa247bd",
+ "passes": 3324,
+ "fails": 0,
+ "name": "홈 조회 API 응답 본문 존재",
+ "path": "::홈 조회 API 응답 본문 존재"
+ },
+ {
+ "id": "c11c9388b82c69a51e689a6927384a13",
+ "passes": 0,
+ "fails": 3324,
+ "name": "홈 조회 API JSON 파싱 가능",
+ "path": "::홈 조회 API JSON 파싱 가능"
+ }
+ ]
+ },
+ "options": {
+ "summaryTimeUnit": "",
+ "noColor": false,
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ]
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 112074.562
+ },
+ "metrics": {
+ "http_req_failed": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3326
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 490383343,
+ "rate": 4375509.787849985
+ }
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3326,
+ "rate": 29.67667185708029
+ }
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 0,
+ "med": 0,
+ "max": 3.264,
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.011830126277811187
+ }
+ },
+ "http_req_sending": {
+ "contains": "time",
+ "values": {
+ "p(95)": 0.04,
+ "avg": 0.022266987372219143,
+ "min": 0.003,
+ "med": 0.014,
+ "max": 7.113,
+ "p(90)": 0.028
+ },
+ "type": "trend"
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3324,
+ "rate": 29.658826594388117
+ }
+ },
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0,
+ "passes": 0,
+ "fails": 3324
+ },
+ "thresholds": {
+ "rate<0.05": {
+ "ok": true
+ }
+ }
+ },
+ "data_sent": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 1637390,
+ "rate": 14609.827339766896
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 14.221150030066143,
+ "min": 7.888,
+ "med": 10.748000000000001,
+ "max": 287.782,
+ "p(90)": 22.323000000000004,
+ "p(95)": 30.338749999999994
+ }
+ },
+ "http_req_tls_handshaking": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 0,
+ "p(95)": 0,
+ "avg": 0.45120384846662637,
+ "min": 0,
+ "med": 0,
+ "max": 112.174
+ }
+ },
+ "http_req_receiving": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 52.552,
+ "p(90)": 4.875500000000001,
+ "p(95)": 6.0597499999999975,
+ "avg": 3.6858265183403485,
+ "min": 0.11,
+ "med": 3.155
+ }
+ },
+ "vus_max": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 100,
+ "min": 100,
+ "max": 100
+ }
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.75,
+ "passes": 9972,
+ "fails": 3324
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0.017,
+ "avg": 0.47242122669871794,
+ "min": 0.001,
+ "med": 0.004,
+ "max": 116.234,
+ "p(90)": 0.009
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "min": 1,
+ "max": 100,
+ "value": 1
+ }
+ },
+ "home_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "max": 211,
+ "p(90)": 23,
+ "p(95)": 31.849999999999852,
+ "avg": 14.693742478941035,
+ "min": 8,
+ "med": 11
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 2040.8341407987348,
+ "min": 1016.724041,
+ "med": 2048.3737085,
+ "max": 3057.683916,
+ "p(90)": 2838.9209838,
+ "p(95)": 2926.4363811999997
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 3324,
+ "rate": 29.658826594388117
+ }
+ },
+ "http_req_duration": {
+ "contains": "time",
+ "values": {
+ "p(90)": 22.323000000000004,
+ "p(95)": 30.338749999999994,
+ "avg": 14.221150030066143,
+ "min": 7.888,
+ "med": 10.748000000000001,
+ "max": 287.782
+ },
+ "thresholds": {
+ "p(95)<2000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend"
+ },
+ "http_req_waiting": {
+ "values": {
+ "avg": 10.513056524353583,
+ "min": 4.924,
+ "med": 7.401999999999999,
+ "max": 286.92,
+ "p(90)": 17.573500000000006,
+ "p(95)": 25.04849999999995
+ },
+ "type": "trend",
+ "contains": "time"
+ }
+ },
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODY1MCwiZXhwIjoxNzc1MjcwNjUwfQ.0YgP8I8qZY-Hikp5J0pv0fcXtUu9-_i55zOoo7xznQU"
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01.html b/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01.html
new file mode 100644
index 0000000..a1d6c2c
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-home-api-2026-03-05T02-46-01.html
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 홈 조회 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 3326
+
+
+
+
+
+
+
+
Failed Requests
+
0
+
+
+
+
+
+
Breached Thresholds
+
0
+
+
+
+
+
Failed Checks
+
3324
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | home_api_duration |
+
+ 14.69 |
+
+ 8.00 |
+
+ 11.00 |
+
+ 211.00 |
+
+ 23.00 |
+
+ 31.85 |
+
+
+
+
+ | http_req_blocked |
+
+ 0.47 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 116.23 |
+
+ 0.01 |
+
+ 0.02 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.01 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 3.26 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 14.22 |
+
+ 7.89 |
+
+ 10.75 |
+
+ 287.78 |
+
+ 22.32 |
+
+ 30.34 |
+
+
+
+
+ | http_req_receiving |
+
+ 3.69 |
+
+ 0.11 |
+
+ 3.15 |
+
+ 52.55 |
+
+ 4.88 |
+
+ 6.06 |
+
+
+
+
+ | http_req_sending |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 7.11 |
+
+ 0.03 |
+
+ 0.04 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.45 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 112.17 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 10.51 |
+
+ 4.92 |
+
+ 7.40 |
+
+ 286.92 |
+
+ 17.57 |
+
+ 25.05 |
+
+
+
+
+ | iteration_duration |
+
+ 2040.83 |
+
+ 1016.72 |
+
+ 2048.37 |
+
+ 3057.68 |
+
+ 2838.92 |
+
+ 2926.44 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 0.00% |
+ 0.00 |
+ 3324.00 |
+
+
+
+ | http_req_failed |
+
+ 0.00% |
+ 3326.00 |
+ 0.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | total_requests |
+
+
+ 3324.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 9972
+
+
+ Failed
+ 3324
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 3324
+
+
+ Rate
+ 29.66/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 100
+
+
+
+
+
Requests
+
+
+ Total
+
+ 3326
+
+
+
+
+ Rate
+
+ 29.68/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 490.38 MB
+
+
+ Rate
+ 4.38 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 1.64 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 홈 조회 API 상태 코드 200 |
+ 3324 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 시간 < 2초 |
+ 3324 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API 응답 본문 존재 |
+ 3324 |
+ 0 |
+ 100.00 |
+
+
+
+ | 홈 조회 API JSON 파싱 가능 |
+ 0 |
+ 3324 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33-summary.json b/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33-summary.json
new file mode 100644
index 0000000..c21c61d
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33-summary.json
@@ -0,0 +1,299 @@
+{
+ "setup_data": {
+ "token": "eyJhbGciOiJIUzI1NiJ9.eyJvcmdKb2luU3RhdHVzIjoiQVBQUk9WRUQiLCJvcmdJZCI6MSwib3JnUGVybWlzc2lvbiI6MTUsIm9yZ0lzQWRtaW4iOnRydWUsInRva2VuVHlwZSI6Ik9SRyIsIm1lbWJlcklkIjo3NTAxLCJ1c2VySWQiOjUwMDEsImlhdCI6MTc3MjY3ODg4OSwiZXhwIjoxNzc1MjcwODg5fQ.BXVtvGMk2XkJomPGu3z3bED9U-MxGHCKmitC1uWhchs",
+ "videoIds": [
+ "151"
+ ]
+ },
+ "root_group": {
+ "name": "",
+ "path": "",
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
+ "groups": [],
+ "checks": [
+ {
+ "fails": 5307,
+ "name": "영상 세션 시작 API 상태 코드 200 또는 201",
+ "path": "::영상 세션 시작 API 상태 코드 200 또는 201",
+ "id": "b292b3b10176b79638a488dedde259f7",
+ "passes": 1
+ },
+ {
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 시간 < 3초",
+ "path": "::영상 세션 시작 API 응답 시간 < 3초",
+ "id": "38fdc37afba80c32cedb5248f693c40c",
+ "passes": 5308
+ },
+ {
+ "id": "af400638963cc60ef15d8a9793e15a5d",
+ "passes": 5308,
+ "fails": 0,
+ "name": "영상 세션 시작 API 응답 본문 존재",
+ "path": "::영상 세션 시작 API 응답 본문 존재"
+ },
+ {
+ "id": "cd43fcd7173b0fd1d01c1848a9e0a945",
+ "passes": 0,
+ "fails": 5308,
+ "name": "영상 세션 시작 API JSON 파싱 가능",
+ "path": "::영상 세션 시작 API JSON 파싱 가능"
+ }
+ ]
+ },
+ "options": {
+ "summaryTrendStats": [
+ "avg",
+ "min",
+ "med",
+ "max",
+ "p(90)",
+ "p(95)"
+ ],
+ "summaryTimeUnit": "",
+ "noColor": false
+ },
+ "state": {
+ "isStdOutTTY": false,
+ "isStdErrTTY": false,
+ "testRunDurationMs": 204032.363
+ },
+ "metrics": {
+ "errors": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "passes": 5307,
+ "fails": 1,
+ "rate": 0.9998116051243406
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ }
+ },
+ "http_req_duration": {
+ "contains": "time",
+ "values": {
+ "min": 1.871,
+ "med": 4.92,
+ "max": 312.74,
+ "p(90)": 23.9272,
+ "p(95)": 45.86985,
+ "avg": 12.149761393597005
+ },
+ "thresholds": {
+ "p(95)<3000": {
+ "ok": true
+ },
+ "p(99)<5000": {
+ "ok": true
+ }
+ },
+ "type": "trend"
+ },
+ "http_req_connecting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(95)": 0,
+ "avg": 0.015052542372881365,
+ "min": 0,
+ "med": 0,
+ "max": 5.689,
+ "p(90)": 0
+ }
+ },
+ "http_req_waiting": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "avg": 11.962432768361598,
+ "min": 1.79,
+ "med": 4.805,
+ "max": 311.574,
+ "p(90)": 23.43310000000001,
+ "p(95)": 44.90219999999997
+ }
+ },
+ "data_sent": {
+ "contains": "data",
+ "values": {
+ "count": 2675951,
+ "rate": 13115.32621910574
+ },
+ "type": "counter"
+ },
+ "data_received": {
+ "type": "counter",
+ "contains": "data",
+ "values": {
+ "count": 3395976,
+ "rate": 16644.30068870986
+ }
+ },
+ "iterations": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5308,
+ "rate": 26.015480691168587
+ }
+ },
+ "http_req_duration{expected_response:true}": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "min": 18.276,
+ "med": 95.773,
+ "max": 105.293,
+ "p(90)": 103.38900000000001,
+ "p(95)": 104.34100000000001,
+ "avg": 73.11399999999999
+ }
+ },
+ "http_req_sending": {
+ "contains": "time",
+ "values": {
+ "p(90)": 0.059,
+ "p(95)": 0.11,
+ "avg": 0.058317137476458246,
+ "min": 0.005,
+ "med": 0.021,
+ "max": 11.377
+ },
+ "type": "trend"
+ },
+ "http_req_failed": {
+ "contains": "default",
+ "values": {
+ "rate": 0.9994350282485875,
+ "passes": 5307,
+ "fails": 3
+ },
+ "thresholds": {
+ "rate<0.1": {
+ "ok": false
+ }
+ },
+ "type": "rate"
+ },
+ "http_req_receiving": {
+ "contains": "time",
+ "values": {
+ "avg": 0.12901148775894505,
+ "min": 0.012,
+ "med": 0.073,
+ "max": 10.979,
+ "p(90)": 0.17110000000000014,
+ "p(95)": 0.23854999999999976
+ },
+ "type": "trend"
+ },
+ "checks": {
+ "type": "rate",
+ "contains": "default",
+ "values": {
+ "rate": 0.5000470987189148,
+ "passes": 10617,
+ "fails": 10615
+ }
+ },
+ "vus": {
+ "type": "gauge",
+ "contains": "default",
+ "values": {
+ "value": 1,
+ "min": 1,
+ "max": 150
+ }
+ },
+ "http_req_blocked": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "max": 98.781,
+ "p(90)": 0.032,
+ "p(95)": 0.11454999999999976,
+ "avg": 0.5538088512241094,
+ "min": 0.001,
+ "med": 0.008
+ }
+ },
+ "iteration_duration": {
+ "type": "trend",
+ "contains": "time",
+ "values": {
+ "p(90)": 4725.2627999,
+ "p(95)": 4870.07811645,
+ "avg": 3512.905415837796,
+ "min": 2006.496084,
+ "med": 3500.552313,
+ "max": 5197.612166
+ }
+ },
+ "connection_pool_errors": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 0,
+ "rate": 0
+ },
+ "thresholds": {
+ "count<100": {
+ "ok": true
+ }
+ }
+ },
+ "video_join_api_duration": {
+ "type": "trend",
+ "contains": "default",
+ "values": {
+ "med": 5,
+ "max": 336,
+ "p(90)": 26,
+ "p(95)": 48.649999999999764,
+ "avg": 12.8654860587792,
+ "min": 2
+ }
+ },
+ "http_req_tls_handshaking": {
+ "values": {
+ "avg": 0.5001666666666668,
+ "min": 0,
+ "med": 0,
+ "max": 98.252,
+ "p(90)": 0,
+ "p(95)": 0
+ },
+ "type": "trend",
+ "contains": "time"
+ },
+ "http_reqs": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5310,
+ "rate": 26.02528305766865
+ }
+ },
+ "total_requests": {
+ "type": "counter",
+ "contains": "default",
+ "values": {
+ "count": 5308,
+ "rate": 26.015480691168587
+ }
+ },
+ "vus_max": {
+ "values": {
+ "value": 150,
+ "min": 150,
+ "max": 150
+ },
+ "type": "gauge",
+ "contains": "default"
+ }
+ }
+}
\ No newline at end of file
diff --git a/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33.html b/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33.html
new file mode 100644
index 0000000..8950fc1
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/pool-50-video-join-api-2026-03-05T02-51-33.html
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
+ 영상 시청 세션 API 부하 테스트 리포트
+
+
+
+
+
+
+
+
+
Total Requests
+
+ 5310
+
+
+
+
+
+
+
+
Failed Requests
+
5307
+
+
+
+
+
+
Breached Thresholds
+
2
+
+
+
+
+
Failed Checks
+
10615
+
+
+
+
+
+
+
+
+
+
+
Trends & Times
+
+
+
+ |
+
+ Avg |
+
+ Min |
+
+ Med |
+
+ Max |
+
+ P(90) |
+
+ P(95) |
+
+
+
+
+
+
+ | http_req_blocked |
+
+ 0.55 |
+
+ 0.00 |
+
+ 0.01 |
+
+ 98.78 |
+
+ 0.03 |
+
+ 0.11 |
+
+
+
+
+ | http_req_connecting |
+
+ 0.02 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 5.69 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_duration |
+
+ 12.15 |
+
+ 1.87 |
+
+ 4.92 |
+
+ 312.74 |
+
+ 23.93 |
+
+ 45.87 |
+
+
+
+
+ | http_req_receiving |
+
+ 0.13 |
+
+ 0.01 |
+
+ 0.07 |
+
+ 10.98 |
+
+ 0.17 |
+
+ 0.24 |
+
+
+
+
+ | http_req_sending |
+
+ 0.06 |
+
+ 0.01 |
+
+ 0.02 |
+
+ 11.38 |
+
+ 0.06 |
+
+ 0.11 |
+
+
+
+
+ | http_req_tls_handshaking |
+
+ 0.50 |
+
+ 0.00 |
+
+ 0.00 |
+
+ 98.25 |
+
+ 0.00 |
+
+ 0.00 |
+
+
+
+
+ | http_req_waiting |
+
+ 11.96 |
+
+ 1.79 |
+
+ 4.80 |
+
+ 311.57 |
+
+ 23.43 |
+
+ 44.90 |
+
+
+
+
+ | iteration_duration |
+
+ 3512.91 |
+
+ 2006.50 |
+
+ 3500.55 |
+
+ 5197.61 |
+
+ 4725.26 |
+
+ 4870.08 |
+
+
+
+
+ | video_join_api_duration |
+
+ 12.87 |
+
+ 2.00 |
+
+ 5.00 |
+
+ 336.00 |
+
+ 26.00 |
+
+ 48.65 |
+
+
+
+
+
+
+
+
+
Rates
+
+
+
+ |
+ Rate % |
+ Pass Count |
+ Fail Count |
+
+
+
+
+
+ | errors |
+
+ 99.98% |
+ 5307.00 |
+ 1.00 |
+
+
+
+ | http_req_failed |
+
+ 99.94% |
+ 3.00 |
+ 5307.00 |
+
+
+
+
+
+
+
+
Counters
+
+
+
+ |
+ Count |
+
+
+
+
+
+ | connection_pool_errors |
+
+
+ 0.00 |
+
+
+
+
+ | total_requests |
+
+
+ 5308.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Checks
+
+
+ Passed
+ 10617
+
+
+ Failed
+ 10615
+
+
+
+
+
+
+
Iterations
+
+
+ Total
+ 5308
+
+
+ Rate
+ 26.02/s
+
+
+
+
+
+
Virtual Users
+
+
+ Min
+ 1
+
+
+ Max
+ 150
+
+
+
+
+
Requests
+
+
+ Total
+
+ 5310
+
+
+
+
+ Rate
+
+ 26.03/s
+
+
+
+
+
+
+
Data Received
+
+
+ Total
+ 3.40 MB
+
+
+ Rate
+ 0.02 mB/s
+
+
+
+
+
Data Sent
+
+
+ Total
+ 2.68 MB
+
+
+ Rate
+ 0.01 mB/s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other Checks
+
+
+
+ | Check Name |
+ Passes |
+ Failures |
+ % Pass |
+
+
+
+
+
+ | 영상 세션 시작 API 상태 코드 200 또는 201 |
+ 1 |
+ 5307 |
+ 0.02 |
+
+
+
+ | 영상 세션 시작 API 응답 시간 < 3초 |
+ 5308 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API 응답 본문 존재 |
+ 5308 |
+ 0 |
+ 100.00 |
+
+
+
+ | 영상 세션 시작 API JSON 파싱 가능 |
+ 0 |
+ 5308 |
+ 0.00 |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k6-tests/results/scenario3-pool/scenario3-pool-result.md b/k6-tests/results/scenario3-pool/scenario3-pool-result.md
new file mode 100644
index 0000000..fde590e
--- /dev/null
+++ b/k6-tests/results/scenario3-pool/scenario3-pool-result.md
@@ -0,0 +1,194 @@
+# 시나리오 3: HikariCP Connection Pool 크기별 부하 테스트 결과
+
+> 테스트 일시: 2026-03-05 11:25 ~ 11:52 (KST)
+> 테스트 환경: macOS (로컬), Spring Boot + PostgreSQL + Redis
+
+---
+
+## 1. 테스트 조건
+
+| 항목 | 값 |
+|------|---|
+| 테스트 도구 | k6 v0.55.0 |
+| DB 인덱스 | 20개 적용 상태 (변수 통제) |
+| Redis 캐시 | 활성화 상태, 각 테스트 전 `FLUSHALL` (변수 통제) |
+| 변수 | `HIKARI_MAX_POOL_SIZE` = 5, 10, 50 |
+| 기타 HikariCP 설정 | minimum-idle=10, connection-timeout=30s, idle-timeout=10min |
+
+### 부하 설정
+
+| API | 최대 VU | 테스트 시간 |
+|-----|---------|-----------|
+| Home API | 100명 | 1분 50초 |
+| History API | 100명 | 1분 50초 |
+| Video Join API | 150명 | 3분 20초 |
+
+---
+
+## 2. 테스트 결과 비교
+
+### Home API (`GET /{orgId}/home`)
+
+| 지표 | Pool=5 | Pool=10 | Pool=50 |
+|------|--------|---------|---------|
+| **평균 응답시간** | 26.2ms | 30.3ms | **14.2ms** |
+| **중앙값 (p50)** | 16.2ms | 12.1ms | **10.7ms** |
+| **p90** | 54.3ms | 41.1ms | **22.3ms** |
+| **p95** | 75.7ms | 81.6ms | **30.3ms** |
+| **최대 응답시간** | 1,017ms | 932ms | **288ms** |
+| 처리량 (RPS) | 29.5/s | 29.5/s | 29.7/s |
+| 에러율 | 0.00% | 0.00% | 0.00% |
+
+### History API (`GET /{orgId}/myactivity/video`)
+
+| 지표 | Pool=5 | Pool=10 | Pool=50 |
+|------|--------|---------|---------|
+| **평균 응답시간** | 18.2ms | 11.6ms | **9.3ms** |
+| **중앙값 (p50)** | 8.8ms | 7.1ms | **7.1ms** |
+| **p90** | 38.8ms | 19.5ms | **16.3ms** |
+| **p95** | 64.0ms | 33.2ms | **21.6ms** |
+| **최대 응답시간** | 412ms | 253ms | **137ms** |
+| 처리량 (RPS) | 29.7/s | 30.0/s | 30.0/s |
+| 에러율 | 0.00% | 0.00% | 0.00% |
+
+### Video Join API (`POST /{orgId}/video/{videoId}/join`)
+
+| 지표 | Pool=5 | Pool=10 | Pool=50 |
+|------|--------|---------|---------|
+| **평균 응답시간** | 6.8ms | **6.4ms** | 12.1ms |
+| **중앙값 (p50)** | 4.2ms | **4.0ms** | 4.9ms |
+| **p90** | 13.1ms | **11.0ms** | 23.9ms |
+| **p95** | 19.2ms | **17.3ms** | 45.9ms |
+| **최대 응답시간** | 342ms | **140ms** | 313ms |
+| 처리량 (RPS) | 25.9/s | 26.0/s | 26.0/s |
+| 에러율 | 99.94%* | 99.94%* | 99.94%* |
+
+> *Video Join API의 에러율은 단일 사용자 반복 요청에 의한 409 Conflict. 비즈니스 로직상 정상.
+> Video Join API의 Pool=50 결과가 Pool=10보다 느린 것은 **99.94%가 Redis 전용 경로(409)**를 타므로 Pool 크기와 무관하며, 시스템 부하 변동에 의한 오차로 판단.
+
+---
+
+## 3. API별 분석
+
+### 3-1. History API — Pool 크기 효과가 가장 뚜렷
+
+```
+Pool=5 → p95: 64.0ms, max: 412ms
+Pool=10 → p95: 33.2ms, max: 253ms (48% 감소)
+Pool=50 → p95: 21.6ms, max: 137ms (66% 감소, Pool=5 대비)
+```
+
+**History API가 Pool 크기에 가장 민감한 이유:**
+
+1. **Redis 캐시 미적용**: History API는 캐시 레이어가 없어 **모든 요청이 DB 커넥션을 사용**
+2. **매 요청마다 DB 커넥션 점유**: `findByMemberId()` 쿼리 실행 시 커넥션 1개 점유
+3. **VU 100명 vs Pool 5개**: VU 100명이 동시에 DB 접근 시 커넥션 대기 큐 발생
+
+**커넥션 대기 발생 구간:**
+
+| Pool Size | 동시 처리 가능 | VU 100일 때 대기 비율 | 예상 대기 시간 |
+|-----------|-------------|-------------------|-------------|
+| 5 | 5 요청 | 95% 대기 | 높음 |
+| 10 | 10 요청 | 90% 대기 | 중간 |
+| 50 | 50 요청 | 50% 대기 | 낮음 |
+
+> Pool=5에서 p50=8.8ms인데 p95=64.0ms인 것은, **대다수 요청은 빠르게 처리되지만 커넥션 대기 큐에 걸린 요청이 지연**되는 전형적인 커넥션 풀 병목 패턴.
+
+### 3-2. Home API — 캐시가 Pool 부족을 부분적으로 보완
+
+```
+Pool=5 → p95: 75.7ms, max: 1,017ms
+Pool=10 → p95: 81.6ms, max: 932ms
+Pool=50 → p95: 30.3ms, max: 288ms
+```
+
+**Home API는 캐시가 있어도 Pool 크기의 영향을 받는 이유:**
+
+1. **Cache Cold Start**: 테스트 시작 시 캐시가 비어있어 초반 요청들은 DB 직접 조회
+2. **Cache Miss 시 다중 JOIN**: 캐시 미스 시 video + 3개 테이블 JOIN 쿼리 실행 → 커넥션 점유 시간이 History API보다 김
+3. **TTL 만료**: 5분 TTL이 만료되면 다시 Cache Miss → DB 접근 필요
+
+**Pool=5 vs Pool=10의 p95가 비슷한 이유 (75.7 vs 81.6ms):**
+- 캐시 히트율이 높아 실제 DB 커넥션 사용 빈도가 낮음
+- Pool 5와 10의 차이가 캐시에 의해 상쇄됨
+- Pool=50에서 유의미하게 개선되는 것은 Cache Miss + 고부하 구간에서의 여유 확보
+
+### 3-3. Video Join API — Pool 크기와 무관
+
+```
+Pool=5 → p95: 19.2ms
+Pool=10 → p95: 17.3ms
+Pool=50 → p95: 45.9ms (오차)
+```
+
+**Video Join API가 Pool 크기에 둔감한 이유:**
+
+```
+[첫 1회 요청]
+ → DB 커넥션 사용 (member 조회 + video 조회 + 권한 확인 + history 조회/생성)
+ → Redis 세션 생성
+
+[이후 99.94% 요청]
+ → Redis에서 세션 존재 확인 → 즉시 409 반환
+ → DB 커넥션 사용하지 않음
+```
+
+99.94%의 요청이 Redis 경로만 타므로 **DB 커넥션 풀 크기가 성능에 영향을 주지 않음**. Pool=50에서 수치가 높은 것은 시스템 부하 변동에 의한 오차.
+
+---
+
+## 4. Pool 크기별 영향도 매트릭스
+
+| API | DB 접근 비율 | Pool=5→50 p95 개선율 | Pool 의존도 |
+|-----|------------|---------------------|-----------|
+| **History API** | 100% (캐시 없음) | 64.0→21.6ms (**66.3% 감소**) | **높음** |
+| **Home API** | ~10% (캐시 히트 후 낮음) | 75.7→30.3ms (**60.0% 감소**) | 중간 |
+| **Video Join API** | ~0.06% (409 경로) | 변화 없음 | **없음** |
+
+---
+
+## 5. 핵심 개선 요약
+
+```
+ Pool=5 Pool=10 Pool=50
+Home API p95: 75.7ms → 81.6ms → 30.3ms
+History p95: 64.0ms → 33.2ms → 21.6ms
+Video Join p95: 19.2ms → 17.3ms → (무관)
+```
+
+---
+
+## 6. Connection Pool 사이징 분석
+
+### Pool 크기 산정 공식
+
+```
+필요 Pool 크기 = 동시 요청 수 × DB 접근 비율 × 평균 커넥션 점유 시간 / 평균 응답 시간
+```
+
+### 현재 환경 기준 분석
+
+| 시나리오 | 동시 VU | DB 접근 비율 | 권장 Pool 크기 | 근거 |
+|---------|--------|------------|-------------|------|
+| 캐시 적용 + 인덱스 적용 | 100 | ~10% | **10~20** | 캐시 히트 시 DB 미접근 |
+| 캐시 미적용 + 인덱스 적용 | 100 | 100% | **30~50** | 모든 요청 DB 접근 |
+| 캐시 미적용 + 인덱스 미적용 | 100 | 100% | **50+** | DB 쿼리 시간 길어 커넥션 점유 증가 |
+
+### Pool=5와 Pool=50의 Tail Latency 비교
+
+| API | Pool=5 max | Pool=50 max | 안정화 효과 |
+|-----|-----------|------------|-----------|
+| Home API | 1,017ms | 288ms | **3.5배 안정화** |
+| History API | 412ms | 137ms | **3.0배 안정화** |
+
+Pool 크기를 5에서 50으로 늘리면 **최대 응답시간이 3~3.5배 안정화**. 이는 커넥션 대기 큐에서의 지연이 제거되었기 때문.
+
+---
+
+## 7. 결론
+
+- **Pool 크기는 DB 직접 접근이 잦은 API에서만 유의미한 영향** (History API: 66.3% 개선)
+- Redis 캐시가 적용된 API(Home)도 Cache Miss 구간에서 Pool 부족의 영향을 받음
+- Redis 전용 경로(Video Join 409)는 Pool 크기와 완전 무관
+- Pool 크기 부족(5)의 주요 증상: **p50은 정상이지만 p95/max가 급등** (커넥션 대기 큐 병목)
+- 캐시 + 인덱스가 적용된 환경에서 VU 100 기준 **Pool=10~20이면 충분**, 캐시 없는 API 비중이 높으면 **Pool=30~50 권장**
diff --git a/k6-tests/run-scenario.sh b/k6-tests/run-scenario.sh
new file mode 100755
index 0000000..54e3777
--- /dev/null
+++ b/k6-tests/run-scenario.sh
@@ -0,0 +1,384 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ============================================
+# 부하 테스트 시나리오 오케스트레이터
+# ============================================
+# 사용법:
+# cd k6-tests
+# ./run-scenario.sh 1 # 인덱스 시나리오
+# ./run-scenario.sh 2 # 캐시 시나리오
+# ./run-scenario.sh 3 # 커넥션풀 시나리오
+# ./run-scenario.sh all # 전체 순차 실행
+# ============================================
+
+# ── 컬러 정의 ──
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+BOLD='\033[1m'
+NC='\033[0m' # No Color
+
+# ── 설정 ──
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+RESULTS_BASE="$SCRIPT_DIR/results"
+
+# PostgreSQL 접속 정보
+DB_HOST="${DB_HOST:-localhost}"
+DB_PORT="${DB_PORT:-5432}"
+DB_NAME="${DB_NAME:-privideo}"
+DB_USER="${DB_USER:-postgres}"
+export PGPASSWORD="${PGPASSWORD:-1234}"
+
+# Redis 접속 정보
+REDIS_HOST="${REDIS_HOST:-localhost}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+
+# API 서버
+BASE_URL="${BASE_URL:-https://localhost:8080}"
+
+# k6 테스트 스크립트 목록
+TEST_SCRIPTS=(
+ "home-api-test.js"
+ "history-api-test.js"
+ "video-join-api-test.js"
+)
+
+# ── 유틸리티 함수 ──
+
+print_header() {
+ echo ""
+ echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}"
+ echo -e "${BOLD}${BLUE}║ $1${NC}"
+ echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}"
+ echo ""
+}
+
+print_step() {
+ echo -e "${CYAN}▶ $1${NC}"
+}
+
+print_success() {
+ echo -e "${GREEN}✔ $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}⚠ $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}✘ $1${NC}"
+}
+
+print_separator() {
+ echo -e "${BLUE}──────────────────────────────────────────────────${NC}"
+}
+
+wait_for_enter() {
+ echo ""
+ echo -e "${YELLOW}$1${NC}"
+ echo -e "${BOLD}Enter를 눌러 계속 진행하세요...${NC}"
+ read -r
+}
+
+# ── 헬스체크 ──
+
+check_postgres() {
+ print_step "PostgreSQL 연결 확인 중..."
+ if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" > /dev/null 2>&1; then
+ print_success "PostgreSQL 연결 성공"
+ return 0
+ else
+ print_error "PostgreSQL 연결 실패 (host=$DB_HOST, port=$DB_PORT, db=$DB_NAME)"
+ return 1
+ fi
+}
+
+check_redis() {
+ print_step "Redis 연결 확인 중..."
+ if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ping > /dev/null 2>&1; then
+ print_success "Redis 연결 성공"
+ return 0
+ else
+ print_error "Redis 연결 실패 (host=$REDIS_HOST, port=$REDIS_PORT)"
+ return 1
+ fi
+}
+
+check_server() {
+ print_step "API 서버 연결 확인 중..."
+ if curl -sk --max-time 5 "$BASE_URL" > /dev/null 2>&1; then
+ print_success "API 서버 연결 성공 ($BASE_URL)"
+ return 0
+ else
+ print_error "API 서버 연결 실패 ($BASE_URL)"
+ return 1
+ fi
+}
+
+check_k6() {
+ print_step "k6 설치 확인 중..."
+ if command -v k6 > /dev/null 2>&1; then
+ print_success "k6 설치 확인 완료 ($(k6 version 2>&1 | head -1))"
+ return 0
+ else
+ print_error "k6가 설치되어 있지 않습니다. brew install k6"
+ return 1
+ fi
+}
+
+check_all() {
+ print_header "환경 헬스체크"
+ local failed=0
+ check_k6 || failed=1
+ check_postgres || failed=1
+ check_redis || failed=1
+ check_server || failed=1
+ if [ $failed -ne 0 ]; then
+ print_error "헬스체크 실패. 위의 오류를 확인하세요."
+ exit 1
+ fi
+ print_success "모든 헬스체크 통과"
+}
+
+# ── Redis 캐시 초기화 (세션 데이터 보존) ──
+
+flush_test_cache() {
+ print_step "Redis 테스트 캐시 초기화 중 (home:*, video:*:info)..."
+ local home_keys video_keys
+ home_keys=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" KEYS "home:*" 2>/dev/null || true)
+ video_keys=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" KEYS "video:*:info" 2>/dev/null || true)
+
+ local count=0
+ if [ -n "$home_keys" ]; then
+ count=$((count + $(echo "$home_keys" | wc -l)))
+ echo "$home_keys" | xargs -r redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" DEL > /dev/null 2>&1
+ fi
+ if [ -n "$video_keys" ]; then
+ count=$((count + $(echo "$video_keys" | wc -l)))
+ echo "$video_keys" | xargs -r redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" DEL > /dev/null 2>&1
+ fi
+ print_success "Redis 캐시 ${count}개 키 삭제 완료 (세션 데이터 보존)"
+}
+
+# ── k6 테스트 실행 ──
+
+run_k6_tests() {
+ local result_dir="$1"
+ local result_prefix="$2"
+ local label="$3"
+
+ mkdir -p "$result_dir"
+
+ for script in "${TEST_SCRIPTS[@]}"; do
+ local test_name="${script%.js}"
+ local prefix="${result_prefix}-${test_name%-test}"
+ print_step "[$label] $test_name 실행 중..."
+ k6 run \
+ --insecure-skip-tls-verify \
+ -e RESULT_DIR="$result_dir" \
+ -e RESULT_PREFIX="$prefix" \
+ -e BASE_URL="$BASE_URL" \
+ "$SCRIPT_DIR/$script" || true
+ print_success "[$label] $test_name 완료"
+ print_separator
+ done
+}
+
+# ── SQL 실행 ──
+
+run_sql() {
+ local sql_file="$1"
+ local label="$2"
+ print_step "$label: $sql_file 실행 중..."
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$sql_file" > /dev/null 2>&1
+ print_success "$label 완료"
+}
+
+# ── 인덱스 개수 확인 ──
+
+count_custom_indexes() {
+ local count
+ count=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \
+ "SELECT count(*) FROM pg_indexes WHERE indexname LIKE 'idx_%';" 2>/dev/null | tr -d ' ')
+ echo "$count"
+}
+
+# ============================================
+# 시나리오 1: 인덱스 Before/After
+# ============================================
+
+run_scenario_1() {
+ local result_dir="$RESULTS_BASE/scenario1-indexing"
+ print_header "시나리오 1: 인덱스 Before/After 테스트"
+
+ # ── Phase 1: Before (인덱스 없이) ──
+ print_step "Phase 1: 인덱스 제거 (Before 상태 준비)"
+ run_sql "$PROJECT_ROOT/scripts/drop-indexes.sql" "인덱스 삭제"
+ local idx_count
+ idx_count=$(count_custom_indexes)
+ print_success "현재 커스텀 인덱스 수: ${idx_count}"
+
+ flush_test_cache
+
+ print_separator
+ echo -e "${BOLD}${YELLOW}[Before] 인덱스 없이 테스트 실행${NC}"
+ run_k6_tests "$result_dir" "before-index" "Before-Index"
+
+ # ── Phase 2: After (인덱스 적용) ──
+ print_step "Phase 2: 인덱스 적용 (After 상태 준비)"
+ run_sql "$PROJECT_ROOT/scripts/add-indexes.sql" "인덱스 생성"
+ idx_count=$(count_custom_indexes)
+ print_success "현재 커스텀 인덱스 수: ${idx_count}"
+
+ flush_test_cache
+
+ print_separator
+ echo -e "${BOLD}${GREEN}[After] 인덱스 적용 후 테스트 실행${NC}"
+ run_k6_tests "$result_dir" "after-index" "After-Index"
+
+ # ── 롤백: 인덱스 제거 ──
+ print_step "롤백: 인덱스 삭제하여 원래 상태 복원"
+ run_sql "$PROJECT_ROOT/scripts/drop-indexes.sql" "인덱스 롤백"
+ idx_count=$(count_custom_indexes)
+ print_success "롤백 완료. 현재 커스텀 인덱스 수: ${idx_count}"
+
+ print_header "시나리오 1 완료"
+ echo -e "결과 디렉토리: ${CYAN}${result_dir}${NC}"
+}
+
+# ============================================
+# 시나리오 2: 캐시 Before/After
+# ============================================
+
+run_scenario_2() {
+ local result_dir="$RESULTS_BASE/scenario2-cache"
+ print_header "시나리오 2: 캐시 Before/After 테스트"
+
+ # ── Phase 1: Before (캐시 비활성화) ──
+ print_warning "캐시 비활성화 상태에서 테스트합니다."
+ echo -e "${BOLD}서버를 nocache 프로필로 재시작하세요:${NC}"
+ echo -e " ${CYAN}SPRING_PROFILES_ACTIVE=local,nocache ./gradlew bootRun${NC}"
+ wait_for_enter "서버가 nocache 프로필로 시작되면 Enter를 누르세요."
+
+ check_server
+
+ flush_test_cache
+
+ print_separator
+ echo -e "${BOLD}${YELLOW}[Before] 캐시 비활성화 상태에서 테스트 실행${NC}"
+ run_k6_tests "$result_dir" "before-cache" "Before-Cache"
+
+ # ── Phase 2: After (캐시 활성화) ──
+ print_warning "캐시 활성화 상태에서 테스트합니다."
+ echo -e "${BOLD}서버를 local 프로필로 재시작하세요:${NC}"
+ echo -e " ${CYAN}SPRING_PROFILES_ACTIVE=local ./gradlew bootRun${NC}"
+ wait_for_enter "서버가 local 프로필로 시작되면 Enter를 누르세요."
+
+ check_server
+
+ flush_test_cache
+
+ print_separator
+ echo -e "${BOLD}${GREEN}[After] 캐시 활성화 상태에서 테스트 실행${NC}"
+ run_k6_tests "$result_dir" "after-cache" "After-Cache"
+
+ print_header "시나리오 2 완료"
+ echo -e "결과 디렉토리: ${CYAN}${result_dir}${NC}"
+}
+
+# ============================================
+# 시나리오 3: Connection Pool 크기 비교
+# ============================================
+
+run_scenario_3() {
+ local result_dir="$RESULTS_BASE/scenario3-pool"
+ local pool_sizes=(10 50 100)
+
+ print_header "시나리오 3: Connection Pool 크기 비교 테스트"
+
+ for pool_size in "${pool_sizes[@]}"; do
+ print_separator
+ print_warning "HikariCP 커넥션 풀 크기: ${pool_size}"
+ echo -e "${BOLD}서버를 HIKARI_MAX_POOL_SIZE=${pool_size}로 재시작하세요:${NC}"
+ echo -e " ${CYAN}HIKARI_MAX_POOL_SIZE=${pool_size} SPRING_PROFILES_ACTIVE=local ./gradlew bootRun${NC}"
+ wait_for_enter "서버가 pool_size=${pool_size}로 시작되면 Enter를 누르세요."
+
+ check_server
+
+ flush_test_cache
+
+ echo -e "${BOLD}${BLUE}[Pool=${pool_size}] 테스트 실행${NC}"
+ run_k6_tests "$result_dir" "pool-${pool_size}" "Pool-${pool_size}"
+ done
+
+ print_header "시나리오 3 완료"
+ echo -e "결과 디렉토리: ${CYAN}${result_dir}${NC}"
+}
+
+# ============================================
+# 메인 실행
+# ============================================
+
+usage() {
+ echo -e "${BOLD}사용법:${NC} $0 {1|2|3|all}"
+ echo ""
+ echo " 1 시나리오 1: 인덱스 Before/After (완전 자동)"
+ echo " 2 시나리오 2: 캐시 Before/After (서버 재시작 필요)"
+ echo " 3 시나리오 3: Connection Pool 크기 비교 (서버 재시작 필요)"
+ echo " all 전체 시나리오 순차 실행"
+ echo ""
+ echo -e "${BOLD}환경변수:${NC}"
+ echo " DB_HOST, DB_PORT, DB_NAME, DB_USER, PGPASSWORD"
+ echo " REDIS_HOST, REDIS_PORT"
+ echo " BASE_URL (기본값: https://localhost:8080)"
+}
+
+main() {
+ if [ $# -lt 1 ]; then
+ usage
+ exit 1
+ fi
+
+ local scenario="$1"
+
+ print_header "부하 테스트 시나리오 오케스트레이터"
+ echo -e "시나리오: ${BOLD}${scenario}${NC}"
+ echo -e "시간: $(date '+%Y-%m-%d %H:%M:%S')"
+ print_separator
+
+ check_all
+
+ case "$scenario" in
+ 1)
+ run_scenario_1
+ ;;
+ 2)
+ run_scenario_2
+ ;;
+ 3)
+ run_scenario_3
+ ;;
+ all)
+ run_scenario_1
+ print_separator
+ run_scenario_2
+ print_separator
+ run_scenario_3
+ ;;
+ *)
+ print_error "알 수 없는 시나리오: $scenario"
+ usage
+ exit 1
+ ;;
+ esac
+
+ print_header "모든 테스트 완료"
+ echo -e "결과 디렉토리: ${CYAN}${RESULTS_BASE}${NC}"
+ echo -e "HTML 리포트를 브라우저에서 열어 확인하세요."
+}
+
+main "$@"
diff --git a/k6-tests/run-test.sh b/k6-tests/run-test.sh
new file mode 100755
index 0000000..a2b6232
--- /dev/null
+++ b/k6-tests/run-test.sh
@@ -0,0 +1,121 @@
+#!/bin/bash
+
+# ============================================
+# k6 부하 테스트 실행 스크립트
+# ============================================
+# 사용법:
+# ./run-test.sh [테스트파일] [옵션]
+#
+# 예시:
+# ./run-test.sh home # 홈 API 테스트
+# ./run-test.sh history # 시청 기록 API 테스트
+# ./run-test.sh video-join # 영상 세션 시작 API 테스트
+# ./run-test.sh all # 모든 테스트 순차 실행
+# ============================================
+
+# 스크립트 디렉토리로 이동
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# .env 파일 로드 (있는 경우)
+if [ -f .env ]; then
+ echo "📁 .env 파일 로드 중..."
+ export $(grep -v '^#' .env | xargs)
+fi
+
+# 기본값 설정
+BASE_URL=${BASE_URL:-"http://localhost:8080"}
+EMAIL=${EMAIL:-"test@example.com"}
+PASSWORD=${PASSWORD:-"password123"}
+USER_ID=${USER_ID:-1}
+MEMBER_ID=${MEMBER_ID:-1}
+ORG_ID=${ORG_ID:-1}
+VIDEO_ID=${VIDEO_ID:-1}
+VUS=${VUS:-10}
+DURATION=${DURATION:-"30s"}
+
+# 결과 디렉토리 생성
+mkdir -p results
+
+# 환경 변수 출력
+echo "============================================"
+echo "🔧 테스트 환경 설정"
+echo "============================================"
+echo "BASE_URL: $BASE_URL"
+echo "EMAIL: $EMAIL"
+echo "ORG_ID: $ORG_ID"
+echo "VIDEO_ID: $VIDEO_ID"
+echo "VUS: $VUS"
+echo "DURATION: $DURATION"
+echo "============================================"
+
+# 공통 k6 옵션
+K6_ENV_OPTS="--env BASE_URL=$BASE_URL \
+ --env EMAIL=$EMAIL \
+ --env PASSWORD=$PASSWORD \
+ --env USER_ID=$USER_ID \
+ --env MEMBER_ID=$MEMBER_ID \
+ --env ORG_ID=$ORG_ID \
+ --env VIDEO_ID=$VIDEO_ID \
+ --env VUS=$VUS \
+ --env DURATION=$DURATION"
+
+# 테스트 실행 함수
+run_test() {
+ local test_name=$1
+ local test_file=$2
+ local output_file="results/${test_name}-$(date +%Y%m%d_%H%M%S).json"
+
+ echo ""
+ echo "🚀 $test_name 테스트 시작..."
+ echo " 출력 파일: $output_file"
+ echo ""
+
+ k6 run $K6_ENV_OPTS \
+ --out json="$output_file" \
+ "$test_file"
+
+ echo ""
+ echo "✅ $test_name 테스트 완료"
+ echo ""
+}
+
+# 메인 로직
+case "${1:-help}" in
+ home)
+ run_test "home-api" "home-api-test.js"
+ ;;
+ history)
+ run_test "history-api" "history-api-test.js"
+ ;;
+ video-join)
+ run_test "video-join-api" "video-join-api-test.js"
+ ;;
+ all)
+ echo "🔄 모든 테스트 순차 실행..."
+ run_test "home-api" "home-api-test.js"
+ run_test "history-api" "history-api-test.js"
+ run_test "video-join-api" "video-join-api-test.js"
+ echo "🎉 모든 테스트 완료!"
+ ;;
+ help|*)
+ echo ""
+ echo "사용법: ./run-test.sh [테스트명] [옵션]"
+ echo ""
+ echo "테스트명:"
+ echo " home - 홈 조회 API 테스트"
+ echo " history - 시청 기록 조회 API 테스트"
+ echo " video-join - 영상 시청 세션 시작 API 테스트"
+ echo " all - 모든 테스트 순차 실행"
+ echo " help - 도움말 출력"
+ echo ""
+ echo "환경 변수 설정:"
+ echo " 1. .env.example을 .env로 복사 후 수정"
+ echo " 2. 또는 export로 직접 설정"
+ echo ""
+ echo "예시:"
+ echo " ./run-test.sh home"
+ echo " VUS=50 DURATION=60s ./run-test.sh home"
+ echo ""
+ ;;
+esac
diff --git a/k6-tests/shared/auth.js b/k6-tests/shared/auth.js
new file mode 100644
index 0000000..a673deb
--- /dev/null
+++ b/k6-tests/shared/auth.js
@@ -0,0 +1,92 @@
+import http from 'k6/http';
+
+/**
+ * 로그인하여 JWT 토큰을 발급받습니다.
+ * @param {string} baseUrl - API 서버 기본 URL
+ * @param {string} email - 사용자 이메일
+ * @param {string} password - 사용자 비밀번호
+ * @returns {string|null} JWT 토큰 또는 null
+ */
+export function login(baseUrl, email, password, orgId) {
+ // Step 1: 로그인 → BOOTSTRAP 토큰 발급
+ const loginUrl = `${baseUrl}/user/login`;
+ const payload = JSON.stringify({
+ email: email,
+ password: password,
+ });
+
+ const loginRes = http.post(loginUrl, payload, {
+ headers: {'Content-Type': 'application/json'},
+ });
+
+ if (loginRes.status !== 200) {
+ console.error(`로그인 실패: ${loginRes.status} - ${loginRes.body}`);
+ return null;
+ }
+
+ const authHeader = loginRes.headers['Authorization'] || loginRes.headers['authorization'];
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ console.error('로그인 응답에 Authorization 헤더가 없습니다.');
+ return null;
+ }
+ const bootstrapToken = authHeader.substring(7);
+
+ // Step 2: 조직 선택 → ORG 토큰 발급
+ const selectOrgUrl = `${baseUrl}/orgs/${orgId || 1}`;
+ const selectRes = http.patch(selectOrgUrl, null, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${bootstrapToken}`,
+ },
+ });
+
+ if (selectRes.status !== 200) {
+ console.error(`조직 선택 실패: ${selectRes.status} - ${selectRes.body}`);
+ return null;
+ }
+
+ const orgAuthHeader = selectRes.headers['Authorization'] || selectRes.headers['authorization'];
+ if (orgAuthHeader && orgAuthHeader.startsWith('Bearer ')) {
+ return orgAuthHeader.substring(7);
+ }
+
+ console.error('조직 선택 응답에 ORG 토큰이 없습니다.');
+ return null;
+}
+
+/**
+ * JWT 토큰을 사용하여 인증 헤더를 생성합니다.
+ * @param {string} token - JWT 토큰
+ * @returns {object} Authorization 헤더가 포함된 객체
+ */
+export function getAuthHeaders(token) {
+ if (!token) {
+ return {};
+ }
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+}
+
+/**
+ * 토큰을 환경 변수나 공유 데이터에서 가져옵니다.
+ * @param {object} sharedData - k6 공유 데이터 객체
+ * @returns {string|null} JWT 토큰 또는 null
+ */
+export function getToken(sharedData) {
+ if (sharedData && sharedData.token) {
+ return sharedData.token;
+ }
+ return null;
+}
+
+/**
+ * 토큰을 공유 데이터에 저장합니다.
+ * @param {object} sharedData - k6 공유 데이터 객체
+ * @param {string} token - JWT 토큰
+ */
+export function setToken(sharedData, token) {
+ if (sharedData) {
+ sharedData.token = token;
+ }
+}
diff --git a/k6-tests/shared/config.js b/k6-tests/shared/config.js
new file mode 100644
index 0000000..fa0820a
--- /dev/null
+++ b/k6-tests/shared/config.js
@@ -0,0 +1,52 @@
+// k6 테스트 공통 설정
+export const config = {
+ // API 서버 기본 URL
+ baseUrl: __ENV.BASE_URL || 'https://localhost:8080',
+
+ // 테스트 데이터
+ testData: {
+ // 테스트용 사용자 정보 (실제 테스트 시 환경변수로 주입 필요)
+ userId: __ENV.USER_ID || 1,
+ memberId: __ENV.MEMBER_ID || 1,
+ orgId: __ENV.ORG_ID || 1,
+ videoId: __ENV.VIDEO_ID || 1,
+
+ // 로그인 정보 (토큰 발급용)
+ email: __ENV.EMAIL || 'test@example.com',
+ password: __ENV.PASSWORD || 'password123',
+ },
+
+ // 부하 테스트 설정
+ loadTest: {
+ // Virtual Users (동시 사용자 수)
+ vus: parseInt(__ENV.VUS) || 10,
+
+ // 테스트 지속 시간
+ duration: __ENV.DURATION || '30s',
+
+ // Ramp-up 설정 (점진적 부하 증가)
+ stages: [
+ { duration: '10s', target: 10 }, // 10초 동안 10명으로 증가
+ { duration: '30s', target: 50 }, // 30초 동안 50명으로 증가
+ { duration: '30s', target: 100 }, // 30초 동안 100명으로 증가
+ { duration: '30s', target: 100 }, // 30초 동안 100명 유지
+ { duration: '10s', target: 0 }, // 10초 동안 0명으로 감소
+ ],
+ },
+
+ // HTTP 요청 설정
+ http: {
+ timeout: '30s',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+
+ // 결과 출력 설정
+ output: {
+ // JSON 결과 파일 경로
+ jsonPath: __ENV.OUTPUT_JSON || 'results/k6-results.json',
+ // CSV 결과 파일 경로
+ csvPath: __ENV.OUTPUT_CSV || 'results/k6-results.csv',
+ },
+};
diff --git a/k6-tests/video-join-api-test.js b/k6-tests/video-join-api-test.js
new file mode 100644
index 0000000..7a7d761
--- /dev/null
+++ b/k6-tests/video-join-api-test.js
@@ -0,0 +1,143 @@
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+import { Rate, Trend, Counter } from 'k6/metrics';
+import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
+import { textSummary } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';
+import { config } from './shared/config.js';
+import { login, getAuthHeaders } from './shared/auth.js';
+
+// 커스텀 메트릭
+const errorRate = new Rate('errors');
+const videoJoinApiDuration = new Trend('video_join_api_duration');
+const requestCounter = new Counter('total_requests');
+const connectionPoolErrors = new Counter('connection_pool_errors');
+
+// 테스트 설정
+export const options = {
+ stages: [
+ { duration: '10s', target: 20 }, // 10초 동안 20명으로 증가
+ { duration: '30s', target: 50 }, // 30초 동안 50명으로 증가
+ { duration: '30s', target: 100 }, // 30초 동안 100명으로 증가
+ { duration: '60s', target: 100 }, // 60초 동안 100명 유지 (Connection Pool 테스트)
+ { duration: '30s', target: 150 }, // 30초 동안 150명으로 증가 (고부하 테스트)
+ { duration: '30s', target: 150 }, // 30초 동안 150명 유지
+ { duration: '10s', target: 0 }, // 10초 동안 0명으로 감소
+ ],
+ thresholds: {
+ 'http_req_duration': ['p(95)<3000', 'p(99)<5000'], // 95%는 3초 이하, 99%는 5초 이하
+ 'http_req_failed': ['rate<0.1'], // 에러율 10% 미만 (Connection Pool 고갈 허용)
+ 'errors': ['rate<0.1'],
+ 'connection_pool_errors': ['count<100'], // Connection Pool 에러 100개 미만
+ },
+};
+
+// 테스트 데이터
+const testData = config.testData;
+
+// 테스트 실행 전 초기화
+export function setup() {
+ console.log('=== 영상 시청 세션 시작 API 부하 테스트 시작 ===');
+ console.log(`Base URL: ${config.baseUrl}`);
+ console.log(`Org ID: ${testData.orgId}`);
+ console.log(`Video ID: ${testData.videoId}`);
+ console.log('⚠️ 로컬 테스트: S3/CloudFront URL 생성은 더미 값으로 반환될 수 있습니다.');
+
+ // 로그인하여 토큰 발급
+ const token = login(config.baseUrl, testData.email, testData.password, testData.orgId);
+ if (!token) {
+ console.error('로그인 실패 - 테스트를 중단합니다.');
+ return null;
+ }
+
+ console.log('로그인 성공 - 토큰 발급 완료');
+
+ // 여러 비디오 ID를 사용할 수 있도록 설정 (실제 테스트 시 여러 비디오 ID 필요)
+ const videoIds = testData.videoIds ? testData.videoIds.split(',') : [testData.videoId];
+
+ return { token, videoIds };
+}
+
+// 각 VU가 실행하는 메인 함수
+export default function (data) {
+ const token = data ? data.token : null;
+ const videoIds = data ? data.videoIds : [testData.videoId];
+
+ if (!token) {
+ console.error('토큰이 없습니다. 테스트를 건너뜁니다.');
+ return;
+ }
+
+ // 랜덤하게 비디오 선택
+ const videoId = videoIds[Math.floor(Math.random() * videoIds.length)];
+
+ // 영상 시청 세션 시작 API 호출
+ const url = `${config.baseUrl}/${testData.orgId}/video/${videoId}/join`;
+
+ const params = {
+ headers: {
+ ...config.http.headers,
+ ...getAuthHeaders(token),
+ },
+ tags: {
+ name: 'Video Join API',
+ videoId: videoId,
+ },
+ };
+
+ const startTime = Date.now();
+ const response = http.post(url, null, params);
+ const duration = Date.now() - startTime;
+
+ // 메트릭 업데이트
+ requestCounter.add(1);
+ videoJoinApiDuration.add(duration);
+ errorRate.add(response.status >= 400);
+
+ // Connection Pool 에러 감지 (503, 504, 또는 타임아웃)
+ if (response.status === 503 || response.status === 504 || response.timings.duration > 30000) {
+ connectionPoolErrors.add(1);
+ }
+
+ // 응답 검증
+ const success = check(response, {
+ '영상 세션 시작 API 상태 코드 200 또는 201': (r) => r.status === 200 || r.status === 201,
+ '영상 세션 시작 API 응답 시간 < 3초': (r) => r.timings.duration < 3000,
+ '영상 세션 시작 API 응답 본문 존재': (r) => r.body && r.body.length > 0,
+ '영상 세션 시작 API JSON 파싱 가능': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body && body.data !== undefined;
+ } catch (e) {
+ return false;
+ }
+ },
+ });
+
+ if (!success && response.status !== 503 && response.status !== 504) {
+ console.error(`영상 세션 시작 API 실패: ${response.status} - ${response.body.substring(0, 200)}`);
+ }
+
+ // 요청 간 대기 시간 (비디오 시청 시뮬레이션)
+ sleep(Math.random() * 3 + 2); // 2-5초 사이 랜덤 대기
+}
+
+// 테스트 종료 후 실행
+export function teardown(data) {
+ console.log('=== 영상 시청 세션 시작 API 부하 테스트 종료 ===');
+ if (data) {
+ console.log('테스트 완료');
+ console.log('Connection Pool 에러 발생 여부를 확인하세요.');
+ }
+}
+
+// 결과 리포트 생성
+export function handleSummary(data) {
+ const resultDir = __ENV.RESULT_DIR || 'results';
+ const prefix = __ENV.RESULT_PREFIX || 'video-join-api';
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
+ return {
+ [`${resultDir}/${prefix}-${ts}.html`]: htmlReport(data, { title: '영상 시청 세션 API 부하 테스트 리포트' }),
+ [`${resultDir}/${prefix}-${ts}-summary.json`]: JSON.stringify(data, null, 2),
+ stdout: textSummary(data, { indent: ' ', enableColors: true }),
+ };
+}
diff --git a/scripts/add-indexes.sql b/scripts/add-indexes.sql
new file mode 100644
index 0000000..0bca1e4
--- /dev/null
+++ b/scripts/add-indexes.sql
@@ -0,0 +1,178 @@
+-- 성능 개선을 위한 인덱스 추가 스크립트
+-- 실행 전: 기존 인덱스 확인 및 중복 방지
+-- 실행 후: EXPLAIN ANALYZE로 쿼리 성능 확인
+
+-- ============================================
+-- 1. History 테이블 인덱스
+-- ============================================
+
+-- 멤버별 시청 기록 조회 및 정렬 최적화
+-- 사용 쿼리: HistoryRepositoryImpl.findByMemberId()
+-- WHERE: member_id, join_status, upload_status
+-- ORDER BY: last_watched_at DESC
+CREATE INDEX IF NOT EXISTS idx_history_member_last_watched
+ON history (member_id, last_watched_at DESC)
+WHERE status = 'ACTIVE';
+
+-- 멤버와 비디오 조합 조회 최적화
+-- 사용 쿼리: HistoryRepository.findByMemberIdAndVideoId()
+CREATE INDEX IF NOT EXISTS idx_history_member_video
+ON history (member_id, video_id)
+WHERE status = 'ACTIVE';
+
+-- 비디오별 시청 기록 조회 최적화
+-- 사용 쿼리: HistoryRepositoryImpl.findVideoWatchLogByVideoId()
+CREATE INDEX IF NOT EXISTS idx_history_video_member
+ON history (video_id, member_id, last_watched_at DESC)
+WHERE status = 'ACTIVE';
+
+-- 완료된 시청 기록 기간별 조회 최적화
+-- 사용 쿼리: HistoryRepositoryImpl.findTopCategoriesByMemberIdWithinPeriod()
+CREATE INDEX IF NOT EXISTS idx_history_member_completed
+ON history (member_id, is_complete, completed_at)
+WHERE status = 'ACTIVE' AND is_complete = true;
+
+-- ============================================
+-- 2. Video 테이블 인덱스
+-- ============================================
+
+-- 조직별 비디오 목록 조회 최적화
+-- 사용 쿼리: VideoRepositoryImpl.findHomeVideos()
+-- WHERE: organization_id, upload_status, join_status, status
+-- ORDER BY: created_at DESC, watch_cnt DESC
+CREATE INDEX IF NOT EXISTS idx_video_org_status_created
+ON video(organization_id, upload_status, created_at DESC)
+WHERE status = 'ACTIVE';
+
+-- 조직별 비디오 조회 (크리에이터 필터링 포함)
+-- 사용 쿼리: VideoRepositoryImpl.findByOrgIdAndCreatorId()
+CREATE INDEX IF NOT EXISTS idx_video_org_creator_status
+ON video(organization_id, member_id, upload_status, created_at DESC)
+WHERE status = 'ACTIVE';
+
+-- 비디오 제목 검색 최적화
+-- 사용 쿼리: VideoRepositoryImpl.findSearchVideos()
+-- WHERE: organization_id, title (LIKE), upload_status
+CREATE INDEX IF NOT EXISTS idx_video_org_title_status
+ON video(organization_id, upload_status, title)
+WHERE status = 'ACTIVE';
+
+-- 비디오 키로 조회 (인코딩 결과 업데이트용)
+-- 사용 쿼리: VideoRepository.findByVideoKey()
+CREATE INDEX IF NOT EXISTS idx_video_video_key
+ON video(video_url)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 3. Video_Member_Group_Mapping 테이블 인덱스
+-- ============================================
+
+-- 비디오별 멤버 그룹 매핑 조회 최적화
+-- 사용 쿼리: VideoMemberGroupMappingRepository.findAllByVideoId()
+CREATE INDEX IF NOT EXISTS idx_video_member_group_mapping_video
+ON video_member_group_mapping (video_id, status)
+WHERE status = 'ACTIVE';
+
+-- 멤버 그룹별 비디오 매핑 조회 최적화
+-- 사용 쿼리: 비디오 접근 권한 확인 쿼리
+CREATE INDEX IF NOT EXISTS idx_video_member_group_mapping_group
+ON video_member_group_mapping (member_group_id, video_id, status)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 4. Member_Group_Mapping 테이블 인덱스
+-- ============================================
+
+-- 멤버별 그룹 매핑 조회 최적화
+-- 사용 쿼리: MemberGroupMappingRepository.findAllByMemberId()
+CREATE INDEX IF NOT EXISTS idx_member_group_mapping_member
+ON member_group_mapping (member_id, member_group_id, status)
+WHERE status = 'ACTIVE';
+
+-- 그룹별 멤버 매핑 조회 최적화
+CREATE INDEX IF NOT EXISTS idx_member_group_mapping_group
+ON member_group_mapping (member_group_id, member_id, status)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 5. Video_Category_Mapping 테이블 인덱스
+-- ============================================
+
+-- 비디오별 카테고리 매핑 조회 최적화
+-- 사용 쿼리: VideoCategoryMappingRepository.findAllByVideoId()
+CREATE INDEX IF NOT EXISTS idx_video_category_mapping_video
+ON video_category_mapping (video_id, category_id, status)
+WHERE status = 'ACTIVE';
+
+-- 카테고리별 비디오 매핑 조회 최적화
+CREATE INDEX IF NOT EXISTS idx_video_category_mapping_category
+ON video_category_mapping (category_id, video_id, status)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 6. Scrap 테이블 인덱스
+-- ============================================
+
+-- 멤버와 비디오 조합으로 스크랩 확인 최적화
+-- 사용 쿼리: ScrapRepository.existsByMemberIdAndVideoId()
+CREATE INDEX IF NOT EXISTS idx_scrap_member_video
+ON scrap (member_id, video_id, status)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 7. Member 테이블 인덱스
+-- ============================================
+
+-- 조직별 멤버 조회 최적화
+-- 사용 쿼리: MemberRepository.findByIdAndOrganizationIdAndStatus()
+CREATE INDEX IF NOT EXISTS idx_member_org_status
+ON "member" (organization_id, id, status, join_status)
+WHERE status = 'ACTIVE';
+
+-- 사용자별 멤버 조회 최적화
+-- 사용 쿼리: MemberRepository.findByUserId()
+CREATE INDEX IF NOT EXISTS idx_member_user_status
+ON "member" (user_id, status, join_status)
+WHERE status = 'ACTIVE';
+
+-- ============================================
+-- 8. Notice_Member_Group_Mapping 테이블 인덱스
+-- ============================================
+
+-- 공지사항별 멤버 그룹 매핑 조회 최적화
+-- 사용 쿼리: NoticeMemberGroupMappingRepository.findAllByNoticeId()
+CREATE INDEX IF NOT EXISTS idx_notice_member_group_mapping_notice
+ON notice_member_group_mapping (notice_id, member_group_id);
+
+-- ============================================
+-- 9. Comment 테이블 인덱스
+-- ============================================
+
+-- 비디오별 댓글 조회 최적화
+-- 사용 쿼리: CommentRepository.findByVideoId()
+CREATE INDEX IF NOT EXISTS idx_comment_video_status
+ON comment (video_id, status, created_at DESC)
+WHERE status = 'ACTIVE';
+
+-- 부모 댓글별 자식 댓글 조회 최적화
+CREATE INDEX IF NOT EXISTS idx_comment_parent
+ON comment (parent_comment_id, status, created_at)
+WHERE status = 'ACTIVE' AND is_child = true;
+
+-- ============================================
+-- 인덱스 생성 완료 확인
+-- ============================================
+
+-- 인덱스 목록 확인 쿼리 (실행 후 확인용)
+-- SELECT
+-- schemaname,
+-- tablename,
+-- indexname,
+-- indexdef
+-- FROM pg_indexes
+-- WHERE tablename IN (
+-- 'History', 'Video', 'Video_Member_Group_Mapping',
+-- 'Member_Group_Mapping', 'Video_Category_Mapping',
+-- 'Scrap', 'member', 'Notice_Member_Group_Mapping', 'Comment'
+-- )
+-- ORDER BY tablename, indexname;
diff --git a/scripts/drop-indexes.sql b/scripts/drop-indexes.sql
new file mode 100644
index 0000000..af3c667
--- /dev/null
+++ b/scripts/drop-indexes.sql
@@ -0,0 +1,40 @@
+-- 인덱스 롤백 스크립트 (add-indexes.sql 역연산)
+-- 모든 커스텀 인덱스를 삭제하여 인덱스 적용 전 상태로 복원
+
+-- 1. History 테이블
+DROP INDEX IF EXISTS idx_history_member_last_watched;
+DROP INDEX IF EXISTS idx_history_member_video;
+DROP INDEX IF EXISTS idx_history_video_member;
+DROP INDEX IF EXISTS idx_history_member_completed;
+
+-- 2. Video 테이블
+DROP INDEX IF EXISTS idx_video_org_status_created;
+DROP INDEX IF EXISTS idx_video_org_creator_status;
+DROP INDEX IF EXISTS idx_video_org_title_status;
+DROP INDEX IF EXISTS idx_video_video_key;
+
+-- 3. Video_Member_Group_Mapping 테이블
+DROP INDEX IF EXISTS idx_video_member_group_mapping_video;
+DROP INDEX IF EXISTS idx_video_member_group_mapping_group;
+
+-- 4. Member_Group_Mapping 테이블
+DROP INDEX IF EXISTS idx_member_group_mapping_member;
+DROP INDEX IF EXISTS idx_member_group_mapping_group;
+
+-- 5. Video_Category_Mapping 테이블
+DROP INDEX IF EXISTS idx_video_category_mapping_video;
+DROP INDEX IF EXISTS idx_video_category_mapping_category;
+
+-- 6. Scrap 테이블
+DROP INDEX IF EXISTS idx_scrap_member_video;
+
+-- 7. Member 테이블
+DROP INDEX IF EXISTS idx_member_org_status;
+DROP INDEX IF EXISTS idx_member_user_status;
+
+-- 8. Notice_Member_Group_Mapping 테이블
+DROP INDEX IF EXISTS idx_notice_member_group_mapping_notice;
+
+-- 9. Comment 테이블
+DROP INDEX IF EXISTS idx_comment_video_status;
+DROP INDEX IF EXISTS idx_comment_parent;
diff --git a/scripts/insert-test-data.sql b/scripts/insert-test-data.sql
new file mode 100644
index 0000000..82721be
--- /dev/null
+++ b/scripts/insert-test-data.sql
@@ -0,0 +1,371 @@
+-- ============================================
+-- Privideo 부하 테스트용 대용량 데이터 삽입 SQL
+-- ============================================
+-- 데이터 규모:
+-- - 사용자: 100명
+-- - 조직: 3개
+-- - 멤버: 조직당 50명 (총 150명)
+-- - 멤버 그룹: 조직당 5개 (총 15개)
+-- - 비디오: 조직당 500개 (총 1,500개)
+-- - 카테고리: 멤버 그룹당 5개 (총 75개)
+-- - 시청 기록: 멤버당 약 50개 (총 7,500개+)
+-- - 스크랩: 약 1,000개
+-- ============================================
+
+-- 기존 데이터 삭제 (필요시 주석 해제)
+-- TRUNCATE TABLE scrap, history, video_category_mapping, video_member_group_mapping, video, category, member_group_mapping, member_group, member, organization, users RESTART IDENTITY CASCADE;
+
+-- ============================================
+-- 1. 사용자 생성 (100명)
+-- ============================================
+-- 비밀번호: password123 (BCrypt 해시)
+-- BCrypt 해시 값: $2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqGU2gB5rTn5MHLAtvNHMKQOjy.mW
+
+INSERT INTO users (id, name, email, password, gender, phone_number, age, created_at, updated_at, status)
+SELECT
+ nextval('users_seq'),
+ '테스트유저' || seq,
+ 'testuser' || seq || '@example.com',
+ '$2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqGU2gB5rTn5MHLAtvNHMKQOjy.mW',
+ CASE WHEN seq % 2 = 0 THEN 'MALE' ELSE 'FEMALE' END,
+ '010-' || LPAD((1000 + seq)::text, 4, '0') || '-' || LPAD((1000 + seq)::text, 4, '0'),
+ 20 + (seq % 30),
+ NOW() - INTERVAL '1 day' * (seq % 30),
+ NOW(),
+ 'ACTIVE'
+FROM generate_series(1, 100) AS seq
+ON CONFLICT (email) DO NOTHING;
+
+-- 테스트용 메인 사용자 (로그인용)
+INSERT INTO users (id, name, email, password, gender, phone_number, age, created_at, updated_at, status)
+VALUES (
+ nextval('users_seq'),
+ '테스트관리자',
+ 'test@example.com',
+ '$2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqGU2gB5rTn5MHLAtvNHMKQOjy.mW',
+ 'MALE',
+ '010-0000-0000',
+ 30,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+)
+ON CONFLICT (email) DO NOTHING;
+
+-- ============================================
+-- 2. 조직 생성 (3개)
+-- ============================================
+
+INSERT INTO organization (id, user_id, name, img_url, description, created_at, updated_at, status)
+SELECT
+ nextval('organization_seq'),
+ (SELECT id FROM users WHERE email = 'test@example.com'),
+ '테스트조직' || seq,
+ 'org-images/org' || seq || '.png',
+ '부하 테스트를 위한 테스트 조직 ' || seq || '입니다.',
+ NOW() - INTERVAL '1 day' * seq,
+ NOW(),
+ 'ACTIVE'
+FROM generate_series(1, 3) AS seq
+ON CONFLICT (name) DO NOTHING;
+
+-- ============================================
+-- 3. 멤버 생성 (조직당 50명)
+-- ============================================
+
+-- 조직 1의 멤버 (관리자 포함)
+INSERT INTO member (id, user_id, organization_id, nickname, is_admin, join_status, permission_code, created_at, updated_at, status)
+SELECT
+ nextval('member_seq'),
+ u.id,
+ (SELECT id FROM organization WHERE name = '테스트조직1'),
+ '멤버_조직1_' || ROW_NUMBER() OVER (ORDER BY u.id),
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN true ELSE false END,
+ 'APPROVED',
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN 15 ELSE 0 END,
+ NOW() - INTERVAL '1 hour' * ROW_NUMBER() OVER (ORDER BY u.id),
+ NOW(),
+ 'ACTIVE'
+FROM users u
+WHERE u.id IN (SELECT id FROM users ORDER BY id LIMIT 50)
+ON CONFLICT DO NOTHING;
+
+-- 조직 2의 멤버
+INSERT INTO member (id, user_id, organization_id, nickname, is_admin, join_status, permission_code, created_at, updated_at, status)
+SELECT
+ nextval('member_seq'),
+ u.id,
+ (SELECT id FROM organization WHERE name = '테스트조직2'),
+ '멤버_조직2_' || ROW_NUMBER() OVER (ORDER BY u.id),
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN true ELSE false END,
+ 'APPROVED',
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN 15 ELSE 0 END,
+ NOW() - INTERVAL '1 hour' * ROW_NUMBER() OVER (ORDER BY u.id),
+ NOW(),
+ 'ACTIVE'
+FROM users u
+WHERE u.id IN (SELECT id FROM users ORDER BY id OFFSET 25 LIMIT 50)
+ON CONFLICT DO NOTHING;
+
+-- 조직 3의 멤버
+INSERT INTO member (id, user_id, organization_id, nickname, is_admin, join_status, permission_code, created_at, updated_at, status)
+SELECT
+ nextval('member_seq'),
+ u.id,
+ (SELECT id FROM organization WHERE name = '테스트조직3'),
+ '멤버_조직3_' || ROW_NUMBER() OVER (ORDER BY u.id),
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN true ELSE false END,
+ 'APPROVED',
+ CASE WHEN ROW_NUMBER() OVER (ORDER BY u.id) <= 3 THEN 15 ELSE 0 END,
+ NOW() - INTERVAL '1 hour' * ROW_NUMBER() OVER (ORDER BY u.id),
+ NOW(),
+ 'ACTIVE'
+FROM users u
+WHERE u.id IN (SELECT id FROM users ORDER BY id OFFSET 50 LIMIT 50)
+ON CONFLICT DO NOTHING;
+
+-- 테스트 관리자를 조직1에 추가
+INSERT INTO member (id, user_id, organization_id, nickname, is_admin, join_status, permission_code, created_at, updated_at, status)
+SELECT
+ nextval('member_seq'),
+ (SELECT id FROM users WHERE email = 'test@example.com'),
+ (SELECT id FROM organization WHERE name = '테스트조직1'),
+ '테스트관리자',
+ true,
+ 'APPROVED',
+ 15,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+WHERE NOT EXISTS (
+ SELECT 1 FROM member
+ WHERE user_id = (SELECT id FROM users WHERE email = 'test@example.com')
+ AND organization_id = (SELECT id FROM organization WHERE name = '테스트조직1')
+);
+
+-- ============================================
+-- 4. 멤버 그룹 생성 (조직당 5개)
+-- ============================================
+
+INSERT INTO member_group (id, organization_id, name, created_at, updated_at, status)
+SELECT
+ nextval('member_group_seq'),
+ o.id,
+ o.name || '_그룹' || g.seq,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM organization o
+CROSS JOIN generate_series(1, 5) AS g(seq)
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 5. 멤버 그룹 매핑 (각 멤버를 1~3개 그룹에 할당)
+-- ============================================
+
+INSERT INTO member_group_mapping (id, member_id, member_group_id, created_at, updated_at, status)
+SELECT
+ nextval('member_group_mapping_seq'),
+ sub.member_id,
+ sub.member_group_id,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM (
+ SELECT m.id as member_id, mg.id as member_group_id,
+ ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY RANDOM()) as rn
+ FROM member m
+ JOIN member_group mg ON mg.organization_id = m.organization_id
+) sub
+WHERE sub.rn <= 2 -- 멤버당 최대 2개 그룹에 속함
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 6. 카테고리 생성 (멤버 그룹당 5개)
+-- ============================================
+
+INSERT INTO category (id, title, member_group_id, created_at, updated_at, status)
+SELECT
+ nextval('category_seq'),
+ mg.name || '_카테고리' || c.seq,
+ mg.id,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM member_group mg
+CROSS JOIN generate_series(1, 5) AS c(seq)
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 7. 비디오 생성 (조직당 500개, 총 1,500개)
+-- ============================================
+
+INSERT INTO video (
+ id, organization_id, member_id, title, description,
+ video_url, thumbnail_url, hls_prefix, whole_time,
+ is_comment, ai_function_type, ai_feedback, ai_summary,
+ expired_at, watch_cnt, quit_cnt, upload_status,
+ created_at, updated_at, status
+)
+SELECT
+ nextval('video_seq'),
+ o.id,
+ (SELECT id FROM member WHERE organization_id = o.id AND is_admin = true LIMIT 1),
+ '테스트 비디오 ' || o.name || ' #' || v.seq,
+ '이것은 부하 테스트를 위한 테스트 비디오 ' || v.seq || '의 설명입니다. ' ||
+ '다양한 주제의 영상 컨텐츠를 포함하고 있으며, 테스트 목적으로 생성되었습니다.',
+ 'videos/' || o.id || '/video_' || v.seq || '.mp4',
+ 'thumbnails/' || o.id || '/thumb_' || v.seq || '.jpg',
+ 'hls/' || o.id || '/video_' || v.seq || '/',
+ 300 + (v.seq % 600), -- 5분 ~ 15분
+ CASE WHEN v.seq % 3 = 0 THEN true ELSE false END,
+ CASE
+ WHEN v.seq % 4 = 0 THEN 'SUMMARY'
+ WHEN v.seq % 4 = 1 THEN 'FEEDBACK'
+ WHEN v.seq % 4 = 2 THEN 'QUIZ'
+ ELSE 'NONE'
+ END,
+ CASE WHEN v.seq % 4 IN (0, 1) THEN 'AI가 생성한 피드백/요약 내용입니다.' ELSE NULL END,
+ CASE WHEN v.seq % 4 = 0 THEN 'AI가 생성한 요약 내용입니다. 이 비디오는 다양한 주제를 다루고 있습니다.' ELSE NULL END,
+ CURRENT_DATE + INTERVAL '1 year',
+ (v.seq % 1000), -- 조회수
+ (v.seq % 100), -- 중도 이탈수
+ 'COMPLETE',
+ NOW() - INTERVAL '1 day' * (v.seq % 90), -- 최근 90일 내 생성
+ NOW(),
+ 'ACTIVE'
+FROM organization o
+CROSS JOIN generate_series(1, 500) AS v(seq);
+
+-- ============================================
+-- 8. 비디오-멤버그룹 매핑 (일부 비디오만 그룹 제한)
+-- ============================================
+
+INSERT INTO video_member_group_mapping (id, member_group_id, video_id, created_at, updated_at, status)
+SELECT
+ nextval('video_member_group_mapping_seq'),
+ sub.member_group_id,
+ sub.video_id,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM (
+ SELECT v.id as video_id, mg.id as member_group_id,
+ ROW_NUMBER() OVER (PARTITION BY v.id ORDER BY RANDOM()) as rn
+ FROM video v
+ JOIN member_group mg ON mg.organization_id = v.organization_id
+ WHERE v.id IN (
+ SELECT id FROM video ORDER BY RANDOM() LIMIT 300 -- 20%의 비디오 (1500 * 0.2 = 300)
+ )
+) sub
+WHERE sub.rn <= 2 -- 비디오당 최대 2개 그룹에 매핑
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 9. 비디오-카테고리 매핑
+-- ============================================
+
+INSERT INTO video_category_mapping (id, video_id, category_id, created_at, updated_at, status)
+SELECT
+ nextval('video_category_mapping_seq'),
+ sub.video_id,
+ sub.category_id,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM (
+ SELECT v.id as video_id, c.id as category_id,
+ ROW_NUMBER() OVER (PARTITION BY v.id ORDER BY RANDOM()) as rn
+ FROM video v
+ JOIN category c ON c.member_group_id IN (
+ SELECT mg.id FROM member_group mg WHERE mg.organization_id = v.organization_id
+ )
+) sub
+WHERE sub.rn <= 3 -- 비디오당 최대 3개 카테고리에 매핑
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 10. 시청 기록 생성 (대용량 - 멤버당 약 50개)
+-- ============================================
+
+INSERT INTO history (
+ id, member_id, video_id, watch_rate, recent_position_sec,
+ started_at, had_end, is_complete, completed_at, last_watched_at,
+ created_at, updated_at, status
+)
+SELECT
+ nextval('history_seq'),
+ sub.member_id,
+ sub.video_id,
+ (RANDOM() * 100)::INTEGER,
+ (RANDOM() * sub.whole_time)::INTEGER,
+ NOW() - INTERVAL '1 day' * (RANDOM() * 30)::INTEGER,
+ CASE WHEN RANDOM() > 0.3 THEN true ELSE false END,
+ CASE WHEN RANDOM() > 0.5 THEN true ELSE false END,
+ CASE WHEN RANDOM() > 0.5 THEN NOW() - INTERVAL '1 day' * (RANDOM() * 10)::INTEGER ELSE NULL END,
+ NOW() - INTERVAL '1 hour' * (RANDOM() * 720)::INTEGER,
+ NOW(),
+ NOW(),
+ 'ACTIVE'
+FROM (
+ SELECT m.id as member_id, v.id as video_id, v.whole_time,
+ ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY RANDOM()) as rn
+ FROM member m
+ JOIN video v ON v.organization_id = m.organization_id
+) sub
+WHERE sub.rn <= 50 -- 멤버당 최대 50개의 시청 기록
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 11. 스크랩 생성 (약 1,000개)
+-- ============================================
+
+INSERT INTO scrap (id, member_id, video_id, created_at, updated_at, status)
+SELECT
+ nextval('scrap_seq'),
+ sub.member_id,
+ sub.video_id,
+ NOW() - INTERVAL '1 day' * (RANDOM() * 30)::INTEGER,
+ NOW(),
+ 'ACTIVE'
+FROM (
+ SELECT m.id as member_id, v.id as video_id,
+ ROW_NUMBER() OVER (PARTITION BY m.id ORDER BY RANDOM()) as rn
+ FROM member m
+ JOIN video v ON v.organization_id = m.organization_id
+) sub
+WHERE sub.rn <= 10 -- 멤버당 최대 10개 스크랩
+ON CONFLICT DO NOTHING;
+
+-- ============================================
+-- 12. 데이터 검증 쿼리
+-- ============================================
+
+SELECT 'Users' as table_name, COUNT(*) as count FROM users
+UNION ALL SELECT 'Organizations', COUNT(*) FROM organization
+UNION ALL SELECT 'Members', COUNT(*) FROM member
+UNION ALL SELECT 'Member Groups', COUNT(*) FROM member_group
+UNION ALL SELECT 'Member Group Mappings', COUNT(*) FROM member_group_mapping
+UNION ALL SELECT 'Categories', COUNT(*) FROM category
+UNION ALL SELECT 'Videos', COUNT(*) FROM video
+UNION ALL SELECT 'Video Member Group Mappings', COUNT(*) FROM video_member_group_mapping
+UNION ALL SELECT 'Video Category Mappings', COUNT(*) FROM video_category_mapping
+UNION ALL SELECT 'Histories', COUNT(*) FROM history
+UNION ALL SELECT 'Scraps', COUNT(*) FROM scrap;
+
+-- ============================================
+-- 테스트 계정 정보 출력
+-- ============================================
+
+SELECT
+ '=== 테스트 계정 정보 ===' as info
+UNION ALL
+SELECT 'Email: test@example.com'
+UNION ALL
+SELECT 'Password: password123'
+UNION ALL
+SELECT 'Org ID: ' || (SELECT id::text FROM organization WHERE name = '테스트조직1')
+UNION ALL
+SELECT 'Member ID: ' || (SELECT m.id::text FROM member m JOIN users u ON m.user_id = u.id WHERE u.email = 'test@example.com' LIMIT 1)
+UNION ALL
+SELECT 'Video ID: ' || (SELECT id::text FROM video WHERE organization_id = (SELECT id FROM organization WHERE name = '테스트조직1') LIMIT 1);
diff --git a/scripts/reset-test-data.sql b/scripts/reset-test-data.sql
new file mode 100644
index 0000000..8c5ead9
--- /dev/null
+++ b/scripts/reset-test-data.sql
@@ -0,0 +1,58 @@
+-- ============================================
+-- Privideo 테스트 데이터 초기화 SQL
+-- ============================================
+-- 주의: 이 스크립트는 모든 데이터를 삭제합니다!
+-- 운영 환경에서는 절대 실행하지 마세요.
+-- ============================================
+
+-- 트랜잭션 시작
+BEGIN;
+
+-- 외래 키 제약 조건을 고려한 순서로 삭제
+-- 자식 테이블부터 삭제
+
+-- 1. Scrap 삭제
+TRUNCATE TABLE scrap RESTART IDENTITY CASCADE;
+
+-- 2. History 삭제
+TRUNCATE TABLE history RESTART IDENTITY CASCADE;
+
+-- 3. Video_Category_Mapping 삭제
+TRUNCATE TABLE video_category_mapping RESTART IDENTITY CASCADE;
+
+-- 4. Video_Member_Group_Mapping 삭제
+TRUNCATE TABLE video_member_group_mapping RESTART IDENTITY CASCADE;
+
+-- 5. Video 삭제
+TRUNCATE TABLE video RESTART IDENTITY CASCADE;
+
+-- 6. Category 삭제
+TRUNCATE TABLE category RESTART IDENTITY CASCADE;
+
+-- 7. Member_Group_Mapping 삭제
+TRUNCATE TABLE member_group_mapping RESTART IDENTITY CASCADE;
+
+-- 8. Member_Group 삭제
+TRUNCATE TABLE member_group RESTART IDENTITY CASCADE;
+
+-- 9. Member 삭제
+TRUNCATE TABLE member RESTART IDENTITY CASCADE;
+
+-- 10. Organization 삭제
+TRUNCATE TABLE organization RESTART IDENTITY CASCADE;
+
+-- 11. Users 삭제
+TRUNCATE TABLE users RESTART IDENTITY CASCADE;
+
+-- 트랜잭션 커밋
+COMMIT;
+
+-- 초기화 완료 확인
+SELECT 'Users' as table_name, COUNT(*) as count FROM users
+UNION ALL SELECT 'Organizations', COUNT(*) FROM organization
+UNION ALL SELECT 'Members', COUNT(*) FROM member
+UNION ALL SELECT 'Videos', COUNT(*) FROM video
+UNION ALL SELECT 'Histories', COUNT(*) FROM history
+UNION ALL SELECT 'Scraps', COUNT(*) FROM scrap;
+
+SELECT '=== 데이터 초기화 완료 ===' as result;
diff --git a/src/main/java/app/allstackproject/privideo/domain/home/service/HomeService.java b/src/main/java/app/allstackproject/privideo/domain/home/service/HomeService.java
index c7c5a04..90714a2 100644
--- a/src/main/java/app/allstackproject/privideo/domain/home/service/HomeService.java
+++ b/src/main/java/app/allstackproject/privideo/domain/home/service/HomeService.java
@@ -24,9 +24,15 @@
import app.allstackproject.privideo.domain.notice.repository.NoticeRepository;
import app.allstackproject.privideo.domain.organization.repository.OrganizationRepository;
import app.allstackproject.privideo.domain.video.repository.VideoRepository;
+import app.allstackproject.privideo.domain.video.repository.VideoRedisRepository;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,6 +48,11 @@ public class HomeService {
private final NoticeMemberGroupMappingRepository noticeMemberGroupMappingRepository;
private final MemberGroupMappingRepository memberGroupMappingRepository;
private final CdnUrlProvider cdnUrlProvider;
+ private final VideoRedisRepository videoRedisRepository;
+ private final ObjectMapper objectMapper;
+
+ @Value("${app.cache.enabled:true}")
+ private boolean cacheEnabled;
@Transactional(readOnly = true)
public ReadHomeResponse readHome(Long memberId, Long orgId, String filterStr) {
@@ -56,37 +67,97 @@ public ReadHomeResponse readHome(Long memberId, Long orgId, String filterStr) {
Boolean isAdmin = member.getPermissionCode() != 0L;
String orgName = organization.getName();
- List videoInfos = videoRepository.findHomeVideos(orgId, memberId, filter);
- videoInfos.forEach(info -> info.setThumbnailUrl(cdnUrlProvider.generateImgUrl(info.getThumbnailUrl())));
-
- List videoIds = videoInfos.stream()
- .map(HomeVideoItem::getId)
- .toList();
+ // 캐시에서 비디오 목록 조회 시도
+ List