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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration8.654.007.0056.0013.0020.00
http_req_blocked0.470.000.0030.150.010.01
http_req_connecting0.010.000.000.570.000.00
http_req_duration8.164.527.29162.4610.9316.01
http_req_receiving0.060.020.053.440.090.12
http_req_sending0.010.010.012.010.020.03
http_req_tls_handshaking0.460.000.0029.610.000.00
http_req_waiting8.094.497.22162.2610.8315.88
iteration_duration2015.601009.272015.883025.662810.312908.49
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003369.00
http_req_failed0.00%3371.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3369.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033690100.00
시청 기록 조회 API 응답 시간 < 1.5초33690100.00
시청 기록 조회 API 응답 본문 존재33690100.00
시청 기록 조회 API JSON 파싱 가능033690.00
시청 기록 목록 반환033690.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration15.3812.0014.0077.0020.0026.00
http_req_blocked0.470.000.0032.180.010.01
http_req_connecting0.010.000.002.770.000.00
http_req_duration14.899.5613.87165.2817.6821.50
http_req_receiving4.930.074.8320.155.055.31
http_req_sending0.020.000.017.510.020.03
http_req_tls_handshaking0.460.000.0031.880.000.00
http_req_waiting9.947.598.98165.0612.4616.10
iteration_duration2033.891021.622044.013037.422842.432926.15
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003339.00
http_req_failed0.00%3341.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3339.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20033390100.00
홈 조회 API 응답 시간 < 2초33390100.00
홈 조회 API 응답 본문 존재33390100.00
홈 조회 API JSON 파싱 가능033390.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.730.000.01261.760.020.03
http_req_connecting0.010.000.0017.550.000.00
http_req_duration8.652.594.32347.0511.8622.02
http_req_receiving0.090.010.0615.700.120.16
http_req_sending0.030.010.029.860.030.05
http_req_tls_handshaking0.700.000.00259.430.000.00
http_req_waiting8.532.534.24346.8611.6621.67
iteration_duration3514.492003.413521.575226.734699.164857.89
video_join_api_duration9.462.004.00347.0016.0025.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5311.001.00
http_req_failed99.94%3.005311.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5312.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201153110.02
영상 세션 시작 API 응답 시간 < 3초53120100.00
영상 세션 시작 API 응답 본문 존재53120100.00
영상 세션 시작 API JSON 파싱 가능053120.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration16.293.008.00165.0039.0058.00
http_req_blocked1.060.000.01197.800.020.05
http_req_connecting0.020.000.005.060.000.00
http_req_duration15.192.777.23296.0935.8155.24
http_req_receiving0.180.020.1016.770.210.30
http_req_sending0.060.010.0334.860.050.08
http_req_tls_handshaking0.960.000.00103.420.000.00
http_req_waiting14.952.737.06295.7435.3854.99
iteration_duration2024.501008.472024.013147.262822.152915.86
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors100.00%3356.000.00
http_req_failed99.94%2.003356.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3356.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 200033560.00
시청 기록 조회 API 응답 시간 < 1.5초33560100.00
시청 기록 조회 API 응답 본문 존재33560100.00
시청 기록 조회 API JSON 파싱 가능033560.00
시청 기록 목록 반환033560.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration63.926.0022.001360.00146.60262.10
http_req_blocked1.210.000.01161.370.020.05
http_req_connecting0.040.000.0023.840.000.00
http_req_duration62.545.9320.871285.80142.57258.81
http_req_receiving0.240.030.1321.150.280.43
http_req_sending0.060.010.0310.560.050.07
http_req_tls_handshaking1.140.000.00157.850.000.00
http_req_waiting62.255.8620.621285.65142.14258.70
iteration_duration2055.291013.762034.014043.672848.452961.35
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003299.00
http_req_failed0.00%3301.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3299.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20032990100.00
시청 기록 조회 API 응답 시간 < 1.5초32990100.00
시청 기록 조회 API 응답 본문 존재32990100.00
시청 기록 조회 API JSON 파싱 가능032990.00
시청 기록 목록 반환032990.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
+
+ +
+ +

Failed Checks

+
0
+
+
+ + +
+ + +
+ + +

Trends & Times

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_duration0.000.000.000.000.000.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.000.00
http_req_failed0.00%0.000.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 NamePassesFailures% 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
+
+ +
+ +

Failed Checks

+
0
+
+
+ + +
+ + +
+ + +

Trends & Times

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked35.4835.4835.4835.4835.4835.48
http_req_connecting0.270.270.270.270.270.27
http_req_duration420.57420.57420.57420.57420.57420.57
http_req_receiving0.860.860.860.860.860.86
http_req_sending0.040.040.040.040.040.04
http_req_tls_handshaking33.0833.0833.0833.0833.0833.08
http_req_waiting419.67419.67419.67419.67419.67419.67
iteration_duration1.670.010.02341.132.6510.90
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.000.00
http_req_failed100.00%0.001.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 NamePassesFailures% 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration2.611.002.0069.003.005.00
http_req_blocked0.500.000.0062.400.010.01
http_req_connecting0.010.000.000.520.000.00
http_req_duration2.100.931.75171.132.643.54
http_req_receiving0.030.010.032.170.050.06
http_req_sending0.020.000.013.300.020.03
http_req_tls_handshaking0.490.000.0062.060.000.00
http_req_waiting2.060.901.71170.612.563.44
iteration_duration2010.601002.192011.793003.202790.622899.92
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors100.00%3376.000.00
http_req_failed99.97%1.003376.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3376.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 200033760.00
홈 조회 API 응답 시간 < 2초33760100.00
홈 조회 API 응답 본문 존재33760100.00
홈 조회 API JSON 파싱 가능033760.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration125.4512.0041.001260.00367.00491.10
http_req_blocked1.030.000.01228.030.010.05
http_req_connecting0.030.000.0011.900.000.00
http_req_duration124.2212.6840.371260.26365.82490.17
http_req_receiving10.210.146.54242.6017.7931.01
http_req_sending0.120.010.0240.740.040.07
http_req_tls_handshaking0.950.000.00225.880.000.00
http_req_waiting113.897.9332.261229.04346.57463.76
iteration_duration2137.161024.552130.014077.272941.853049.72
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003180.00
http_req_failed0.00%3182.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3180.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20031800100.00
홈 조회 API 응답 시간 < 2초31800100.00
홈 조회 API 응답 본문 존재31800100.00
홈 조회 API JSON 파싱 가능031800.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.800.000.0179.110.020.05
http_req_connecting0.020.000.0011.280.000.00
http_req_duration19.432.466.99847.8544.2580.56
http_req_receiving0.140.020.1017.550.210.28
http_req_sending0.040.010.027.470.050.07
http_req_tls_handshaking0.750.000.0078.100.000.00
http_req_waiting19.252.316.84847.1043.4880.30
iteration_duration3503.372005.003482.725286.734698.494860.89
video_join_api_duration20.372.007.00879.0047.0084.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5325.001.00
http_req_failed99.94%3.005325.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5326.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201153250.02
영상 세션 시작 API 응답 시간 < 3초53260100.00
영상 세션 시작 API 응답 본문 존재53260100.00
영상 세션 시작 API JSON 파싱 가능053260.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration9.583.007.0067.0018.0023.00
http_req_blocked0.380.000.0031.430.010.02
http_req_connecting0.010.000.002.270.000.00
http_req_duration9.173.747.10100.9316.6921.49
http_req_receiving0.070.010.056.490.120.16
http_req_sending0.020.000.011.960.030.03
http_req_tls_handshaking0.360.000.0029.840.000.00
http_req_waiting9.083.717.02100.6116.5421.29
iteration_duration2001.991009.392010.043037.642801.752900.33
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003388.00
http_req_failed0.00%3390.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3388.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033880100.00
시청 기록 조회 API 응답 시간 < 1.5초33880100.00
시청 기록 조회 API 응답 본문 존재33880100.00
시청 기록 조회 API JSON 파싱 가능033880.00
시청 기록 목록 반환033880.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration28.5511.0020.00422.0045.0065.00
http_req_blocked0.410.000.0048.520.010.01
http_req_connecting0.010.000.004.880.000.00
http_req_duration28.0510.8819.60421.9844.5064.14
http_req_receiving3.660.063.0362.195.066.25
http_req_sending0.020.000.012.810.030.04
http_req_tls_handshaking0.390.000.0048.140.000.00
http_req_waiting24.378.1016.30417.4839.9257.81
iteration_duration2042.321022.702034.493099.482840.462939.72
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003323.00
http_req_failed0.00%3325.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3323.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20033230100.00
홈 조회 API 응답 시간 < 2초33230100.00
홈 조회 API 응답 본문 존재33230100.00
홈 조회 API JSON 파싱 가능033230.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.380.000.0164.440.020.05
http_req_connecting0.010.000.002.740.000.00
http_req_duration6.711.834.17127.7613.8620.58
http_req_receiving0.070.010.054.410.130.17
http_req_sending0.040.000.0234.050.040.06
http_req_tls_handshaking0.340.000.0061.070.000.00
http_req_waiting6.611.804.09127.7113.6220.09
iteration_duration3516.182004.023524.505057.484719.224871.25
video_join_api_duration7.191.004.00128.0015.0021.05
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5299.001.00
http_req_failed99.94%3.005299.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5300.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201152990.02
영상 세션 시작 API 응답 시간 < 3초53000100.00
영상 세션 시작 API 응답 본문 존재53000100.00
영상 세션 시작 API JSON 파싱 가능053000.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration9.864.007.0078.0019.0023.00
http_req_blocked0.360.000.0033.250.010.02
http_req_connecting0.010.000.003.840.000.00
http_req_duration9.463.837.14108.9717.8422.60
http_req_receiving0.070.010.053.940.120.16
http_req_sending0.020.000.012.410.030.04
http_req_tls_handshaking0.340.000.0032.940.000.00
http_req_waiting9.373.817.06108.7317.6622.46
iteration_duration2017.651006.282003.963011.342816.952919.66
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003366.00
http_req_failed0.00%3368.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3366.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033660100.00
시청 기록 조회 API 응답 시간 < 1.5초33660100.00
시청 기록 조회 API 응답 본문 존재33660100.00
시청 기록 조회 API JSON 파싱 가능033660.00
시청 기록 목록 반환033660.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration110.8711.0022.003328.00224.80611.80
http_req_blocked1.420.000.00325.760.010.02
http_req_connecting0.030.000.008.130.000.00
http_req_duration110.6410.8621.153287.15222.54613.93
http_req_receiving5.080.072.94182.337.8611.56
http_req_sending0.040.000.0116.590.040.05
http_req_tls_handshaking1.360.000.00320.530.000.00
http_req_waiting105.528.0417.953253.32205.62603.17
iteration_duration2106.661018.612100.505917.012903.163011.12
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003223.00
http_req_failed0.00%3225.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3223.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20032230100.00
홈 조회 API 응답 시간 < 2초32101399.60
홈 조회 API 응답 본문 존재32230100.00
홈 조회 API JSON 파싱 가능032230.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.340.000.0137.110.020.04
http_req_connecting0.010.000.000.650.000.00
http_req_duration6.641.734.33179.3813.5418.34
http_req_receiving0.080.010.064.600.130.17
http_req_sending0.020.010.021.920.040.05
http_req_tls_handshaking0.320.000.0036.550.000.00
http_req_waiting6.541.704.24179.3013.3818.18
iteration_duration3487.632005.553491.045034.814690.734852.13
video_join_api_duration7.062.005.00180.0015.0019.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5337.001.00
http_req_failed99.94%3.005337.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5338.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201153370.02
영상 세션 시작 API 응답 시간 < 3초53380100.00
영상 세션 시작 API 응답 본문 존재53380100.00
영상 세션 시작 API JSON 파싱 가능053380.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration12.064.007.00253.0021.0034.70
http_req_blocked0.400.000.0046.870.010.03
http_req_connecting0.010.000.001.810.000.00
http_req_duration11.593.817.13252.7619.4733.23
http_req_receiving0.100.010.065.760.170.22
http_req_sending0.030.000.012.960.030.04
http_req_tls_handshaking0.370.000.0044.970.000.00
http_req_waiting11.473.767.04252.6619.3532.69
iteration_duration2011.861008.782023.723088.332816.892909.86
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003367.00
http_req_failed0.00%3369.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3367.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033670100.00
시청 기록 조회 API 응답 시간 < 1.5초33670100.00
시청 기록 조회 API 응답 본문 존재33670100.00
시청 기록 조회 API JSON 파싱 가능033670.00
시청 기록 목록 반환033670.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration31.228.0013.001247.0042.4082.00
http_req_blocked0.780.000.01438.150.010.02
http_req_connecting0.020.000.0013.410.000.00
http_req_duration30.348.0212.15931.9241.1281.62
http_req_receiving5.210.103.32393.176.649.31
http_req_sending0.040.000.0137.880.030.05
http_req_tls_handshaking0.750.000.00437.320.000.00
http_req_waiting25.084.988.51904.0735.0372.30
iteration_duration2040.161016.022034.563614.492847.782946.36
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003327.00
http_req_failed0.00%3329.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3327.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20033270100.00
홈 조회 API 응답 시간 < 2초33270100.00
홈 조회 API 응답 본문 존재33270100.00
홈 조회 API JSON 파싱 가능033270.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.380.000.0151.350.020.04
http_req_connecting0.010.000.000.570.000.00
http_req_duration6.351.794.03139.5510.9917.29
http_req_receiving0.080.010.0516.610.120.16
http_req_sending0.030.000.025.100.030.05
http_req_tls_handshaking0.360.000.0050.830.000.00
http_req_waiting6.241.753.95139.4010.8117.16
iteration_duration3531.092004.963548.905035.984707.444858.85
video_join_api_duration6.832.004.00139.0013.0019.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5280.001.00
http_req_failed99.94%3.005280.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5281.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201152800.02
영상 세션 시작 API 응답 시간 < 3초52810100.00
영상 세션 시작 API 응답 본문 존재52810100.00
영상 세션 시작 API JSON 파싱 가능052810.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration19.144.009.00343.0041.0069.00
http_req_blocked0.890.000.01259.190.020.10
http_req_connecting0.020.000.0015.280.000.00
http_req_duration18.193.958.82412.4738.8363.99
http_req_receiving0.150.020.099.460.210.31
http_req_sending0.040.000.024.500.050.09
http_req_tls_handshaking0.840.000.00258.820.000.00
http_req_waiting18.003.888.65412.2638.5463.78
iteration_duration2022.961008.522029.033119.962816.932916.47
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003348.00
http_req_failed0.00%3350.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3348.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033480100.00
시청 기록 조회 API 응답 시간 < 1.5초33480100.00
시청 기록 조회 API 응답 본문 존재33480100.00
시청 기록 조회 API JSON 파싱 가능033480.00
시청 기록 목록 반환033480.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration26.478.0017.001038.0055.0077.00
http_req_blocked0.500.000.0157.840.010.02
http_req_connecting0.020.000.008.460.000.00
http_req_duration26.188.1016.241017.0054.3575.70
http_req_receiving4.740.083.6483.637.299.39
http_req_sending0.030.000.027.660.040.05
http_req_tls_handshaking0.470.000.0056.190.000.00
http_req_waiting21.415.1311.86997.4347.6466.39
iteration_duration2025.191015.322010.093563.352836.972931.04
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003351.00
http_req_failed0.00%3353.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3351.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20033510100.00
홈 조회 API 응답 시간 < 2초33510100.00
홈 조회 API 응답 본문 존재33510100.00
홈 조회 API JSON 파싱 가능033510.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.370.000.0139.290.020.05
http_req_connecting0.010.000.005.460.000.00
http_req_duration6.811.864.24342.2213.0619.18
http_req_receiving0.090.010.073.040.140.18
http_req_sending0.040.000.0224.200.040.06
http_req_tls_handshaking0.340.000.0036.920.000.00
http_req_waiting6.691.834.15341.9212.8119.04
iteration_duration3514.812004.403520.485029.874728.274869.35
video_join_api_duration7.262.004.00355.0015.0021.00
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5301.001.00
http_req_failed99.94%3.005301.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5302.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201153010.02
영상 세션 시작 API 응답 시간 < 3초53020100.00
영상 세션 시작 API 응답 본문 존재53020100.00
영상 세션 시작 API JSON 파싱 가능053020.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
history_api_duration9.764.007.00130.0018.0023.00
http_req_blocked0.380.000.0031.290.010.03
http_req_connecting0.010.000.003.790.000.00
http_req_duration9.333.887.14136.6416.3321.59
http_req_receiving0.090.010.063.330.160.21
http_req_sending0.020.000.012.770.030.04
http_req_tls_handshaking0.350.000.0030.650.000.00
http_req_waiting9.223.857.05136.3916.2021.36
iteration_duration2009.901008.752011.203018.712807.262914.46
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003375.00
http_req_failed0.00%3377.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3375.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 NamePassesFailures% Pass
시청 기록 조회 API 상태 코드 20033750100.00
시청 기록 조회 API 응답 시간 < 1.5초33750100.00
시청 기록 조회 API 응답 본문 존재33750100.00
시청 기록 조회 API JSON 파싱 가능033750.00
시청 기록 목록 반환033750.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
home_api_duration14.698.0011.00211.0023.0031.85
http_req_blocked0.470.000.00116.230.010.02
http_req_connecting0.010.000.003.260.000.00
http_req_duration14.227.8910.75287.7822.3230.34
http_req_receiving3.690.113.1552.554.886.06
http_req_sending0.020.000.017.110.030.04
http_req_tls_handshaking0.450.000.00112.170.000.00
http_req_waiting10.514.927.40286.9217.5725.05
iteration_duration2040.831016.722048.373057.682838.922926.44
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors0.00%0.003324.00
http_req_failed0.00%3326.000.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + +
Count
total_requests3324.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 NamePassesFailures% Pass
홈 조회 API 상태 코드 20033240100.00
홈 조회 API 응답 시간 < 2초33240100.00
홈 조회 API 응답 본문 존재33240100.00
홈 조회 API JSON 파싱 가능033240.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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
http_req_blocked0.550.000.0198.780.030.11
http_req_connecting0.020.000.005.690.000.00
http_req_duration12.151.874.92312.7423.9345.87
http_req_receiving0.130.010.0710.980.170.24
http_req_sending0.060.010.0211.380.060.11
http_req_tls_handshaking0.500.000.0098.250.000.00
http_req_waiting11.961.794.80311.5723.4344.90
iteration_duration3512.912006.503500.555197.614725.264870.08
video_join_api_duration12.872.005.00336.0026.0048.65
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
errors99.98%5307.001.00
http_req_failed99.94%3.005307.00
+ + + +

Counters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Count
connection_pool_errors0.00
total_requests5308.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 NamePassesFailures% Pass
영상 세션 시작 API 상태 코드 200 또는 201153070.02
영상 세션 시작 API 응답 시간 < 3초53080100.00
영상 세션 시작 API 응답 본문 존재53080100.00
영상 세션 시작 API JSON 파싱 가능053080.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> cachedVideoData = cacheEnabled + ? videoRedisRepository.getCachedHomeVideos(orgId, filter.name()) + : null; + List homeVideoItems; - Map> categoriesMap = videoRepository.findCategoriesForHomeVideos(memberId, videoIds); - - List homeVideoItems = videoInfos.stream() - .map(v -> new HomeVideoItem( - v.getId(), - v.getTitle(), - v.getThumbnailUrl(), - v.getCreator(), - v.getWholeTime(), - v.getWatchCnt(), - v.getCreatedAt(), - v.getIsScrapped(), - categoriesMap.getOrDefault(v.getId(), List.of()) - )) - .toList(); + if (cachedVideoData != null) { + // 캐시 히트: 캐시된 데이터를 HomeVideoItem으로 변환 + homeVideoItems = convertCachedDataToHomeVideoItems(cachedVideoData, memberId); + } else { + // 캐시 미스: DB에서 조회 + List videoInfos = videoRepository.findHomeVideos(orgId, memberId, filter); + videoInfos.forEach(info -> info.setThumbnailUrl(cdnUrlProvider.generateImgUrl(info.getThumbnailUrl()))); + + List videoIds = videoInfos.stream() + .map(HomeVideoItem::getId) + .toList(); + + Map> categoriesMap = videoRepository.findCategoriesForHomeVideos(memberId, videoIds); + + homeVideoItems = videoInfos.stream() + .map(v -> new HomeVideoItem( + v.getId(), + v.getTitle(), + v.getThumbnailUrl(), + v.getCreator(), + v.getWholeTime(), + v.getWatchCnt(), + v.getCreatedAt(), + v.getIsScrapped(), + categoriesMap.getOrDefault(v.getId(), List.of()) + )) + .toList(); + + // 캐시에 저장 (비디오 목록만 저장, 사용자별 정보는 제외) + if (cacheEnabled) { + List> videoDataForCache = convertHomeVideoItemsToMap(homeVideoItems); + videoRedisRepository.cacheHomeVideos(orgId, filter.name(), videoDataForCache); + } + } - List globalCategories = categoriesMap.values().stream() - .flatMap(List::stream) + List globalCategories = homeVideoItems.stream() + .flatMap(v -> v.getCategories().stream()) .distinct() .toList(); return ReadHomeResponse.of(nickname, isAdmin, orgName, homeVideoItems, globalCategories); } + private List convertCachedDataToHomeVideoItems(List> cachedData, Long memberId) { + List items = new ArrayList<>(); + for (Map data : cachedData) { + // 스크랩 여부는 사용자별로 다르므로 다시 조회 필요 + // 여기서는 기본값으로 설정하고, 필요시 별도 조회 + Boolean isScrapped = data.get("isScrapped") != null + ? Boolean.valueOf(data.get("isScrapped").toString()) + : false; + + HomeVideoItem item = new HomeVideoItem( + Long.valueOf(data.get("id").toString()), + data.get("title").toString(), + cdnUrlProvider.generateImgUrl(data.get("thumbnailUrl").toString()), + data.get("creator").toString(), + Long.valueOf(data.get("wholeTime").toString()), + Long.valueOf(data.get("watchCnt").toString()), + objectMapper.convertValue(data.get("createdAt"), java.time.LocalDateTime.class), + isScrapped, + objectMapper.convertValue(data.get("categories"), new TypeReference>() {}) + ); + items.add(item); + } + return items; + } + + private List> convertHomeVideoItemsToMap(List items) { + List> result = new ArrayList<>(); + for (HomeVideoItem item : items) { + Map map = new HashMap<>(); + map.put("id", item.getId()); + map.put("title", item.getTitle()); + map.put("thumbnailUrl", item.getThumbnailUrl()); + map.put("creator", item.getCreator()); + map.put("wholeTime", item.getWholeTime()); + map.put("watchCnt", item.getWatchCnt()); + map.put("createdAt", item.getCreatedAt()); + map.put("categories", item.getCategories()); + result.add(map); + } + return result; + } + @Transactional(readOnly = true) public List readSearchVideo(Long memberId, Long orgId, String keyword) { if (!memberRepository.existsByIdAndOrganizationIdAndStatus(memberId, orgId, ACTIVE)) { diff --git a/src/main/java/app/allstackproject/privideo/domain/video/repository/VideoRedisRepository.java b/src/main/java/app/allstackproject/privideo/domain/video/repository/VideoRedisRepository.java index 64ea45e..65f4432 100644 --- a/src/main/java/app/allstackproject/privideo/domain/video/repository/VideoRedisRepository.java +++ b/src/main/java/app/allstackproject/privideo/domain/video/repository/VideoRedisRepository.java @@ -4,8 +4,13 @@ import app.allstackproject.privideo.global.util.RedisRetryUtil; import app.allstackproject.privideo.global.util.RedisUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,7 +23,10 @@ public class VideoRedisRepository { private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; private static final long WATCH_SESSION_TTL_HOURS = 2; + private static final long HOME_CACHE_TTL_MINUTES = 5; + private static final long VIDEO_INFO_CACHE_TTL_MINUTES = 10; public void createWatchSession(String sessionId, Long memberId) { String key = RedisUtil.getWatchSessionKey(sessionId); @@ -99,4 +107,144 @@ public void deleteWatchSession(String sessionId) { logContext ); } + + /** + * 홈 비디오 목록을 캐시에 저장합니다. + */ + public void cacheHomeVideos(Long orgId, String filter, List> videoData) { + String key = RedisUtil.getHomeKey(orgId, filter); + String logContext = String.format("홈 비디오 목록 캐싱 = [org:%d,filter:%s]", orgId, filter); + + RedisRetryUtil.executeVoidWithRetry( + () -> { + try { + String json = objectMapper.writeValueAsString(videoData); + redisTemplate.opsForValue().set(key, json, HOME_CACHE_TTL_MINUTES, TimeUnit.MINUTES); + log.debug("홈 비디오 목록 캐싱 성공 [org: {}, filter: {}]", orgId, filter); + } catch (JsonProcessingException e) { + log.warn("홈 비디오 목록 JSON 직렬화 실패", e); + throw new RuntimeException(e); + } + }, + logContext + ); + } + + /** + * 홈 비디오 목록을 캐시에서 조회합니다. + */ + public List> getCachedHomeVideos(Long orgId, String filter) { + String key = RedisUtil.getHomeKey(orgId, filter); + String logContext = String.format("홈 비디오 목록 조회 = [org:%d,filter:%s]", orgId, filter); + + return RedisRetryUtil.executeWithRetry( + () -> { + String json = redisTemplate.opsForValue().get(key); + if (json == null) { + log.debug("홈 비디오 목록 캐시 미스 [org: {}, filter: {}]", orgId, filter); + return null; + } + + try { + List> result = objectMapper.readValue(json, + new TypeReference>>() {}); + log.debug("홈 비디오 목록 캐시 히트 [org: {}, filter: {}]", orgId, filter); + return result; + } catch (JsonProcessingException e) { + log.warn("홈 비디오 목록 JSON 역직렬화 실패", e); + return null; + } + }, + logContext + ); + } + + /** + * 비디오 정보를 캐시에 저장합니다. + */ + public void cacheVideoInfo(Long videoId, Map videoInfo) { + String key = RedisUtil.getVideoInfoKey(videoId); + String logContext = String.format("비디오 정보 캐싱 = [video:%d]", videoId); + + RedisRetryUtil.executeVoidWithRetry( + () -> { + try { + String json = objectMapper.writeValueAsString(videoInfo); + redisTemplate.opsForValue().set(key, json, VIDEO_INFO_CACHE_TTL_MINUTES, TimeUnit.MINUTES); + log.debug("비디오 정보 캐싱 성공 [video: {}]", videoId); + } catch (JsonProcessingException e) { + log.warn("비디오 정보 JSON 직렬화 실패", e); + throw new RuntimeException(e); + } + }, + logContext + ); + } + + /** + * 비디오 정보를 캐시에서 조회합니다. + */ + public Map getCachedVideoInfo(Long videoId) { + String key = RedisUtil.getVideoInfoKey(videoId); + String logContext = String.format("비디오 정보 조회 = [video:%d]", videoId); + + return RedisRetryUtil.executeWithRetry( + () -> { + String json = redisTemplate.opsForValue().get(key); + if (json == null) { + log.debug("비디오 정보 캐시 미스 [video: {}]", videoId); + return null; + } + + try { + Map result = objectMapper.readValue(json, + new TypeReference>() {}); + log.debug("비디오 정보 캐시 히트 [video: {}]", videoId); + return result; + } catch (JsonProcessingException e) { + log.warn("비디오 정보 JSON 역직렬화 실패", e); + return null; + } + }, + logContext + ); + } + + /** + * 비디오 관련 캐시를 무효화합니다. + */ + public void invalidateVideoCache(Long videoId) { + String pattern = RedisUtil.getVideoInfoPattern(videoId); + String logContext = String.format("비디오 캐시 무효화 = [video:%d]", videoId); + + RedisRetryUtil.executeVoidWithRetry( + () -> { + Set keys = redisTemplate.keys(pattern); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.debug("비디오 캐시 무효화 성공 [video: {}, keys: {}]", videoId, keys.size()); + } + }, + logContext + ); + } + + /** + * 조직의 홈 캐시를 무효화합니다. + */ + public void invalidateHomeCache(Long orgId) { + String pattern = RedisUtil.getHomePattern(orgId); + String logContext = String.format("홈 캐시 무효화 = [org:%d]", orgId); + + RedisRetryUtil.executeVoidWithRetry( + () -> { + Set keys = redisTemplate.keys(pattern); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.debug("홈 캐시 무효화 성공 [org: {}, keys: {}]", orgId, keys.size()); + } + }, + logContext + ); + } } diff --git a/src/main/java/app/allstackproject/privideo/domain/video/service/VideoService.java b/src/main/java/app/allstackproject/privideo/domain/video/service/VideoService.java index 3d98c3a..71f038e 100644 --- a/src/main/java/app/allstackproject/privideo/domain/video/service/VideoService.java +++ b/src/main/java/app/allstackproject/privideo/domain/video/service/VideoService.java @@ -74,6 +74,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -81,6 +82,7 @@ import java.util.UUID; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -108,6 +110,9 @@ public class VideoService { private final MemberGroupMappingRepository memberGroupMappingRepository; private final VideoRedisRepository videoRedisRepository; + @Value("${app.cache.enabled:true}") + private boolean cacheEnabled; + @Transactional(readOnly = true) public JoinVideoSessionResult prepareJoinVideoSession(Long memberId, Long orgId, Long videoId) { Member member = memberRepository.findByIdAndOrganizationIdAndStatus(memberId, orgId, ACTIVE) @@ -413,6 +418,98 @@ public UploadStatusType readVideoEncodingResult(Long memberId, Long orgId, Long @Transactional(readOnly = true) public ReadVideoInfoResponse readVideoInfo(Long orgId, Long memberId, Long videoId) { + // 캐시에서 비디오 정보 조회 시도 + Map cachedInfo = cacheEnabled + ? videoRedisRepository.getCachedVideoInfo(videoId) + : null; + + if (cachedInfo != null) { + // 캐시 히트: 캐시된 데이터를 ReadVideoInfoResponse로 변환 + // 단, 사용자별 정보(멤버 그룹 등)는 매번 조회 필요 + Video video = videoRepository.findByIdAndOrganizationId(videoId, orgId) + .orElseThrow(() -> new ApiException(VIDEO_NOT_FOUND)); + + if (!video.getCreator().getId().equals(memberId)) { + throw new ApiException(VIDEO_CREATE_NOT_FOUND); + } + + // 사용자별 정보는 DB에서 조회 + List videoGroupMappings = videoMemberGroupMappingRepository.findAllByVideoId( + videoId); + Set allMappingGroupIds = videoGroupMappings.stream() + .map(m -> m.getMemberGroup().getId()) + .collect(Collectors.toSet()); + + List videoCategoryMappings = videoCategoryMappingRepository.findAllByVideoId(videoId); + Set allMappingCategoryIds = videoCategoryMappings.stream() + .map(m -> m.getCategory().getId()) + .collect(Collectors.toSet()); + OpenScopeType openScope = allMappingGroupIds.isEmpty() ? PUBLIC : GROUP; + + List myGroupMappings = memberGroupMappingRepository.findAllByMemberId(memberId); + Map myGroupsById = myGroupMappings.stream() + .map(MemberGroupMapping::getMemberGroup) + .collect(Collectors.toMap( + MemberGroup::getId, + g -> g, + (g1, g2) -> g1 + )); + + List myGroupIds = new ArrayList<>(myGroupsById.keySet()); + if (myGroupIds.isEmpty()) { + return ReadVideoInfoResponse.of( + (String) cachedInfo.get("title"), + (String) cachedInfo.get("description"), + (String) cachedInfo.get("thumbnailUrl"), + Long.valueOf(cachedInfo.get("watchCnt").toString()), + (java.time.LocalDate) cachedInfo.get("expiredAt"), + Boolean.valueOf(cachedInfo.get("isComment").toString()), + openScope, + List.of() + ); + } + + List categories = categoryRepository.findByMemberGroupIdIn(myGroupIds); + Map> categoriesByGroupId = categories.stream() + .collect(Collectors.groupingBy(Category::getMemberGroupId)); + + List memberGroupItems = myGroupIds.stream() + .map(groupId -> { + MemberGroup group = myGroupsById.get(groupId); + boolean groupSelected = allMappingGroupIds.contains(groupId); + + List categoryItems = categoriesByGroupId + .getOrDefault(groupId, List.of()) + .stream() + .map(c -> new VideoCategoryItem( + c.getId(), + c.getTitle(), + allMappingCategoryIds.contains(c.getId()) + )) + .toList(); + + return new VideoMemberGroupItem( + group.getId(), + group.getName(), + groupSelected, + categoryItems + ); + }) + .toList(); + + return ReadVideoInfoResponse.of( + (String) cachedInfo.get("title"), + (String) cachedInfo.get("description"), + (String) cachedInfo.get("thumbnailUrl"), + Long.valueOf(cachedInfo.get("watchCnt").toString()), + (java.time.LocalDate) cachedInfo.get("expiredAt"), + Boolean.valueOf(cachedInfo.get("isComment").toString()), + openScope, + memberGroupItems + ); + } + + // 캐시 미스: DB에서 조회 Video video = videoRepository.findByIdAndOrganizationId(videoId, orgId) .orElseThrow(() -> new ApiException(VIDEO_NOT_FOUND)); @@ -443,7 +540,7 @@ public ReadVideoInfoResponse readVideoInfo(Long orgId, Long memberId, Long video List myGroupIds = new ArrayList<>(myGroupsById.keySet()); if (myGroupIds.isEmpty()) { - return ReadVideoInfoResponse.of( + ReadVideoInfoResponse response = ReadVideoInfoResponse.of( video.getTitle(), video.getDescription(), thumbnailUrl, @@ -453,6 +550,20 @@ public ReadVideoInfoResponse readVideoInfo(Long orgId, Long memberId, Long video openScope, List.of() ); + + // 캐시에 저장 + if (cacheEnabled) { + Map videoInfoForCache = new HashMap<>(); + videoInfoForCache.put("title", video.getTitle()); + videoInfoForCache.put("description", video.getDescription()); + videoInfoForCache.put("thumbnailUrl", thumbnailUrl); + videoInfoForCache.put("watchCnt", video.getWatchCnt()); + videoInfoForCache.put("expiredAt", video.getExpiredAt()); + videoInfoForCache.put("isComment", video.getIsComment()); + videoRedisRepository.cacheVideoInfo(videoId, videoInfoForCache); + } + + return response; } List categories = categoryRepository.findByMemberGroupIdIn(myGroupIds); @@ -483,7 +594,7 @@ public ReadVideoInfoResponse readVideoInfo(Long orgId, Long memberId, Long video }) .toList(); - return ReadVideoInfoResponse.of( + ReadVideoInfoResponse response = ReadVideoInfoResponse.of( video.getTitle(), video.getDescription(), thumbnailUrl, @@ -493,6 +604,20 @@ public ReadVideoInfoResponse readVideoInfo(Long orgId, Long memberId, Long video openScope, memberGroupItems ); + + // 캐시에 저장 + if (cacheEnabled) { + Map videoInfoForCache = new HashMap<>(); + videoInfoForCache.put("title", video.getTitle()); + videoInfoForCache.put("description", video.getDescription()); + videoInfoForCache.put("thumbnailUrl", thumbnailUrl); + videoInfoForCache.put("watchCnt", video.getWatchCnt()); + videoInfoForCache.put("expiredAt", video.getExpiredAt()); + videoInfoForCache.put("isComment", video.getIsComment()); + videoRedisRepository.cacheVideoInfo(videoId, videoInfoForCache); + } + + return response; } public boolean modifyVideo(Long orgId, Long memberId, Long videoId, ModifyVideoRequest modifyVideoRequest) { @@ -556,6 +681,10 @@ public boolean modifyVideo(Long orgId, Long memberId, Long videoId, ModifyVideoR .toList(); videoCategoryMappingRepository.saveAll(categoryMappings); + // 비디오 수정 시 캐시 무효화 + videoRedisRepository.invalidateVideoCache(videoId); + videoRedisRepository.invalidateHomeCache(orgId); + return true; } @@ -576,6 +705,11 @@ public boolean deleteVideo(Long orgId, Long memberId, Long videoId) { videoMemberGroupMappingRepository.deleteAllByVideoId(videoId); videoCategoryMappingRepository.deleteAllByVideoId(videoId); + + // 비디오 삭제 시 캐시 무효화 + videoRedisRepository.invalidateVideoCache(videoId); + videoRedisRepository.invalidateHomeCache(orgId); + videoRepository.delete(video); return true; diff --git a/src/main/java/app/allstackproject/privideo/global/util/RedisUtil.java b/src/main/java/app/allstackproject/privideo/global/util/RedisUtil.java index 14aea13..ecd2dff 100644 --- a/src/main/java/app/allstackproject/privideo/global/util/RedisUtil.java +++ b/src/main/java/app/allstackproject/privideo/global/util/RedisUtil.java @@ -9,6 +9,8 @@ private RedisUtil() { private static final String ORG_CODE_PREFIX = "orgCode:"; private static final String MEMBER_PREFIX = "member:"; private static final String WATCH_PREFIX = "watch:"; + private static final String HOME_PREFIX = "home:"; + private static final String VIDEO_PREFIX = "video:"; public static String getOrgKey(Long orgId) { return ORG_PREFIX + orgId; @@ -26,6 +28,22 @@ public static String getWatchSessionKey(String sessionId) { return WATCH_PREFIX + sessionId; } + public static String getHomeKey(Long orgId, String filter) { + return String.format(HOME_PREFIX + "%d:%s", orgId, filter); + } + + public static String getVideoInfoKey(Long videoId) { + return VIDEO_PREFIX + videoId + ":info"; + } + + public static String getVideoInfoPattern(Long videoId) { + return VIDEO_PREFIX + videoId + ":*"; + } + + public static String getHomePattern(Long orgId) { + return HOME_PREFIX + orgId + ":*"; + } + public static final class Fields { private Fields() { } diff --git a/src/main/resources/application-nocache.yml b/src/main/resources/application-nocache.yml new file mode 100644 index 0000000..68c9a6a --- /dev/null +++ b/src/main/resources/application-nocache.yml @@ -0,0 +1,3 @@ +app: + cache: + enabled: false