성능 테스트 - Team-likelion-3rd-Project/likelion-devops-7th-3rd-team01 GitHub Wiki

성능 테스트

1주차(M1)에는 무엇을 어떻게 시험할지만 적는다. 실행 결과는 6주차(M6)에 채운다.

1. 목적

  • 서비스가 목표 응답 시간·오류율을 지키는지 확인한다.
  • 배포·장애 상황에서도 사용자 요청이 실패하지 않는지 확인한다.
  • 병목 1개를 찾아 개선하고, 개선 전→후 수치를 남긴다.

2. 도구와 측정 환경

항목 내용
부하 도구 k6 2.2.0 (Docker로 실행, 스크립트와 실행 방법은 백엔드 레포 perf/README.md)
실행 위치 클러스터 밖 PC에서 실행 (부하 도구가 서버 자원을 같이 쓰지 않도록)
대상 환경 온프렘(M4 기준값, 10/23 측정)과 AWS EKS(M5 이후). 온프렘은 Tailscale로 접속하므로 클라이언트 지표에 VPN 지연이 섞인다
클라이언트 지표 k6: 응답 시간 p95·p99, 실패율(http_req_failed), 초당 요청 수(RPS)
서버 지표 Grafana: Pod CPU·메모리, Pod 수(HPA), DB 커넥션 풀 사용량, Redis 적중률
화면 지표 Lighthouse (FE 박영찬, 7장)

3. mock AI와 실제 AI 구분

mock AI (AI_MODE=mock) 실제 AI (AI_MODE=bedrock)
언제 모든 k6 부하 테스트, 평소 개발·CI 점검일에만: 4주차 점검·6주차 리허설·발표 (각 최대 4시간, 총 12시간)
모델 - Bedrock Claude Haiku 5.5 (Global 추론 프로파일, 10/22 비교 시험으로 선택)
응답 고정된 가짜 요약·답변. 지연은 AI_MOCK_DELAY로 준다. 부하 테스트는 5s (10/22 실측: 질문 답변 3.6~10초) 실제 AI 호출
비용 0 토큰 비용 발생 (예상: 입력 약 160만·출력 약 29만 토큰, 질문 150건 포함)
측정 서버 처리 능력 (AI 외 구간) AI 응답 시간 1회 측정 (질문 5회 내외) → mock 지연값에 반영
  • 실제 AI로는 부하 테스트를 하지 않는다. 비용이 사용자 수에 비례해 늘고, AI 회사의 호출 제한에 먼저 걸리기 때문이다.
  • mock일 때 화면에는 "샘플 데이터" 띠가 보인다.

실제 AI 모델 선택 (10/22, M4-9)

Bedrock Playground에서 같은 지시문·실제 기사로 두 모델을 비교하고, 고른 모델로 앱에서 실제 호출했다.

모델 질문 답변 분야 요약 지시 준수 (해요체·출처·사건 구분)
Nova 2 Lite 1.3초 - 부족 (합니다체, 답에 쓴 기사를 출처에서 빠뜨림, 서로 다른 사건을 묶음)
Claude Haiku 5.5 (선택) 3.6~10초 분야당 15~19초 좋음
  • 앱 실제 호출: 요약 배치 6개 분야 110초(분야당 입력 약 1만 / 출력 약 3천 토큰), 질문 6개 정상 (관련 답변 3개, 범위 밖 거절 2개, 기사 없는 주제 1개)
  • 비용 추정 (Anthropic 공식가 기준, Bedrock 실제 가격은 확인 필요): 배치 1회 약 $0.016, 질문 1회 약 $0.001, 점검일 하루 약 $0.18, 프로젝트 전체 약 $0.55
  • Claude Haiku 5.5는 답하기 전에 생각하는 토큰이 출력에 포함돼 출력 상한을 넉넉히 둔다 (요약 8,000 / 답변 2,000)

4. k6 시나리오

공통 준비

  • 테스트 계정: 질문은 사용자당 하루 10회 제한이 있으므로 시나리오 2·4는 실행할 때마다 새 계정 100명(perf-<실행 시각>[email protected] …)을 만들어 질문을 고르게 나눈다. 고정 계정을 미리 만들어 두면 같은 날 두 번째 실행부터 429가 나기 때문이다.
  • 대상 주소는 BASE_URL, mock 지연은 MOCK_DELAY_MS(서버 AI_MOCK_DELAY와 같은 값)로 넣는다.
  • 요약 데이터: 테스트 전 요약 배치를 1회 이상 실행해 IT 6분야 요약이 있는 상태에서 시작한다.

시나리오 1. 조회 부하 — 홈·분야 페이지

비로그인 사용자가 홈을 보고 분야 페이지로 이동하는, 가장 많이 일어나는 흐름.

항목 내용
요청 GET /api/news/home → GET /api/news/categories/{임의 분야} (사용자당 1~3초 간격)
부하 가상 사용자 0 → 200명 (2분) → 200명 유지 (5분) → 0 (1분)
목표 p95 < 300ms, 실패율 < 1%
볼 것 Redis 캐시 적중 시 DB 조회가 거의 없는지, Pod CPU

시나리오 2. 질문 처리 — 접수부터 답변까지

로그인 사용자가 질문을 보내고 결과가 나올 때까지 2초 간격으로 확인하는 흐름. (mock AI)

항목 내용
요청 POST /api/auth/login → POST /api/questions → GET /api/questions/{id} 2초마다 반복 (완료·실패·거절까지)
부하 가상 사용자 0 → 50명 (1분) → 50명 유지 (5분)
목표 접수 p95 < 500ms, 접수→완료 p95 < mock 지연 + 5초, 60초 타임아웃(FAILED) 0건
볼 것 대기(PENDING) 질문이 쌓이는지, 레플리카 2개 이상에서 같은 질문이 두 번 처리되지 않는지

시나리오 3. 스파이크 — 갑작스러운 접속 폭증과 자동 확장

항목 내용
요청 시나리오 1과 같음
부하 20명 유지 (1분) → 10초 만에 500명 → 500명 유지 (3분) → 20명 (2분)
목표 폭증 구간 실패율 < 5%, HPA가 Pod를 늘린 뒤에는 p95가 시나리오 1 수준으로 회복
볼 것 HPA가 Pod를 늘리기까지 걸린 시간, 늘어나는 동안의 오류 (CL 김차니와 함께 관찰)

확장 기준 (M5-10, 10/26 검토)

대상 기준 설정안 상태
api CPU HPA minReplicas: 2, maxReplicas: 4, 평균 CPU 70% (요청 250m 기준 175m). 줄일 때 2분 대기 온프렘 측정에서 파드당 CPU 최대 약 0.17 core (조회 78 req/s). metrics-server 필요. EKS에서 적용·시나리오 3으로 확인 예정
worker Redis 대기열 길이 KEDA Redis 스케일러 (채택안). 대기열 questions:queue, 워커 1개(스레드 4개)당 대기 5건, 1~3개 KEDA 설치 필요. 미적용

워커 확장 방식 비교:

방식 장점 단점
KEDA Redis 스케일러 Redis 대기열 길이를 직접 읽어 앱 지표·어댑터가 필요 없다. 0개까지 줄일 수 있다 (PENDING 질문 재투입은 api가 하므로 워커가 0개여도 질문이 사라지지 않음) KEDA 설치 필요
HPA + prometheus-adapter 이미 있는 Prometheus와 앱 지표 newsbrief_questions_queue_length 사용 어댑터 설치·규칙 작성, 0개로는 못 줄인다

최소 개수는 1로 둔다. 0이면 첫 질문이 워커가 뜰 때까지(약 16초) 늦어진다.

시나리오 4. 롤링 배포 중 요청 실패율

항목 내용
요청 시나리오 1 + 2를 섞어 가상 사용자 50명으로 일정하게 유지
진행 부하 도중 새 이미지로 롤링 배포 (kubectl rollout restart 또는 develop/main push)
목표 배포 중 실패 요청 0건 (M5 완료 기준)
볼 것 실패가 있으면 readiness 통과 전 트래픽 유입, 종료 중 Pod로의 요청 등 원인을 기록하고 정상 종료(graceful shutdown) 설정으로 개선

5. 장애 중 동작 검증 (위키 "장애 시나리오"와 연결)

시나리오 1·2를 실행하는 도중에 의존 서비스를 멈춘다.

상황 방법 기대 동작
Redis 중단 Redis Pod 삭제 / ElastiCache 접근 차단 조회는 DB에서 계속 응답 (응답 시간은 늘어도 실패 0). 복구 후 캐시 다시 사용
AI 중단 AI_MODE=fail (mock이 항상 오류 반환) 질문은 FAILED로 끝나고 화면에 실패 안내. 요약은 직전 요약 계속 표시
뉴스 소스 중단 RSS 주소 접근 차단 요약 갱신만 실패, 직전 요약 계속 표시

6. 병목 개선 (개선 전→후)

시나리오 1~2 첫 측정에서 가장 느린 구간 1개를 골라 개선하고 같은 시나리오로 다시 측정한다. 예상 후보:

  • 요약 캐시 적중률 (캐시 TTL, 배치 후 캐시를 미리 채우기)
  • DB 커넥션 풀 크기
  • 질문 기록·관련 기사 검색 쿼리 인덱스

온프렘 기준값에서 찾은 병목 후보 (10/23, M4-8)

후보 근거 개선 방향
1. 파드 시작 직후 느림 (워밍업) 파드가 뜬 직후 첫 측정은 시나리오 1 p95 1.29초, 같은 부하로 예열 뒤 다시 재면 70ms. 첫 측정 중 Grafana에서 api 파드 CPU는 최대 약 0.17 core로 여유가 있었고, p95가 높은 건 시작 몇 분뿐이었다 (최대 부하 구간은 정상) readiness 전에 주요 API를 미리 호출해 예열, startupProbe. 롤링 배포(시나리오 4)·HPA 확장(시나리오 3) 때 새 파드에도 같은 문제가 생길 수 있다
2. Tailscale(VPN) 경로 예열 뒤 측정에서도 k6 실패율 1.42%(441건), 최대 52초. 같은 구간 Grafana(Traefik)는 최대 약 78 req/s에서도 서버 p95 0.1초 아래, 응답 코드 전부 200 → 서버가 아니라 클라이언트~서버 사이 경로에서 끊기거나 지연된다 VPN 없이 같은 시나리오 재측정(HyeobSang 예정), AWS 측정과 비교. 온프렘 k6 수치는 VPN 지연이 섞인 값으로 본다
시나리오 지표 개선 전 개선 후 개선 내용
1. 조회 200명, 파드 시작 직후 (온프렘) p95 1.29초 (10/23) 332ms (10/26) 병목 후보 1 개선: api 파드가 readiness UP 전에 조회 API를 140회 미리 호출 (NewsWarmUp, 1.7~2.5초 걸림)
〃 p99 - 2초 〃
〃 실패율 0.10% 0% (37,118건) 〃
  • 측정 조건: 둘 다 클러스터 밖 PC에서 Tailscale로, api 파드 2개가 막 뜬 상태에서 시나리오 1(0→200명 2분, 200명 5분)을 바로 시작. 10/26은 kubectl rollout restart deployment/api 완료 직후
  • p95는 74% 줄었지만 목표 300ms를 조금 넘는다. 예열을 끝낸 파드(10/23 두 번째 측정)는 70ms라 남은 차이는 예열 횟수 부족(JIT가 200명 부하에서 더 컴파일)이나 Tailscale 경로로 본다. WARMUP_ROUNDS(기본 20)를 늘려 재측정하거나 EKS에서 비교한다

7. 프론트엔드 Lighthouse 측정 (박영찬)

항목 내용
목적 화면 로딩 속도와 접근성을 숫자로 확인하고, 6주차에 개선 전후를 비교
도구 Chrome DevTools의 Lighthouse (Performance, Accessibility, Best Practices, SEO)
측정 화면 홈(/), 분야 페이지(/category/AI)
측정 환경 모바일 기준, 시크릿 창, 확장 프로그램 끄기. 같은 조건으로 3회 측정해 중앙값 기록
측정 시점 4주차(운영 도메인 배포 후) 기준값 → 5주차 개선 후 → 6주차 최종 비교
기록 항목 Performance 점수, LCP(가장 큰 요소 표시 시간), CLS(레이아웃 흔들림), TBT(입력 지연)
개선 후보 번들 크기 줄이기(코드 분할), CloudFront 캐시, 이미지·폰트 최적화
시점 화면 Performance LCP CLS TBT 개선 내용
10/23 온프레미스 홈 76 5.0s 0.001 40ms -
10/23 온프레미스 분야 76 5.2s 0.003 40ms -
10/23 AWS 홈 88 3.5s 0.001 0ms -
10/23 AWS 분야 86 3.6s 0.002 0ms -
5주차 개선 후 홈
5주차 개선 후 분야
6주차 최종 홈
6주차 최종 분야

10/23 온프레미스 측정

측정 조건: 크롬 시크릿 창(캐시 없음), Lighthouse 13.4.1 모바일(Moto G Power 에뮬레이션, Slow 4G), 비로그인. 홈과 분야(AI) 각각 3회, 중간값 기록. 서버는 온프레미스(VPN·HTTP), 데이터는 샘플. AWS 배포 전에 먼저 잰 참고 기준값.

결과: 홈 Performance 76, LCP 5.0s, CLS 0.001, TBT 40ms. 분야 Performance 76, LCP 5.2s, CLS 0.003, TBT 40ms. Accessibility 100, Best Practices 74, SEO 82.

해석: 홈과 분야 점수가 거의 같아 화면별 문제가 아니라 사이트 전체의 공통 원인임. LCP 요소는 기사 제목 텍스트이고 서버 응답(TTFB 10ms)은 빠르며, 지연은 JS를 받아 실행한 뒤 목록이 그려지기까지에서 생김. Best Practices 74는 HTTP라서 HTTPS 항목 2개가 실패한 것이고, 콘솔 에러 1건은 비로그인 상태의 /api/auth/me 401로 의도된 동작임. 샘플 데이터 기준이라 데이터 연동 후 재측정하고, AWS와는 네트워크 경로가 달라 같은 환경끼리 비교함.

개선 후보: 응답 압축(JS 약 293KiB, gzip 시 약 93KB), 정적 파일 캐시(JS·CSS에 Cache-Control 없음), 외부 글꼴(jsDelivr) 제거 또는 preload(렌더 차단, 약 0.45~0.95초), 화면별 코드 분할(미사용 JS 약 150KiB), SEO 기본 설정(meta description, robots.txt).

10/23 AWS 측정

측정 조건: 크롬 시크릿 창, Lighthouse 모바일, Navigation, 기본 쓰로틀링(Slow 4G 시뮬레이션), 비로그인. 홈과 분야(AI) 각각 3회, 중간값 기록. 서버는 AWS(CloudFront + S3, API는 ALB 경유), 데이터는 실제 뉴스와 실제 AI 기준.

결과: 홈 Performance 88, LCP 3.5s, CLS 0.001, TBT 0ms. 분야 Performance 86, LCP 3.6s, CLS 0.002, TBT 0ms. Accessibility 100, Best Practices 96, SEO 90.

해석: 온프렘 대비 홈은 76에서 88, LCP 5.0초에서 3.5초로 개선됨. Best Practices는 HTTPS 적용으로 74에서 96으로 오름. LCP는 여전히 2.5초를 넘어 개선 필요 구간임.

개선 후보: 렌더 차단 요청(폰트, 예상 절약 약 1.3초), 안 쓰는 JS 약 45KiB, meta description 추가(SEO 90의 유일한 지적), 비로그인 콘솔 오류 확인(Best Practices 감점 항목).

8. 결과 기록

온프렘 기준값은 10/23 M4-8에서 측정했다. 클러스터 밖 PC에서 Tailscale로 접속, 서버 AI_MODE=mock, AI_MOCK_DELAY=0s, api 파드 2개·worker 1개 (CPU limit 없음).

시나리오 환경 p95 p99 실패율 RPS 최대 Pod 수 비고
1. 조회 부하 온프렘 (10/23 첫 측정) 1.29초 ❌ - 0.10% 65 api 2 파드 시작 직후. 최대 1분(타임아웃)
1. 조회 부하 온프렘 (10/23 기준값) 70ms ✅ 376ms 1.42% ❌ 64 api 2 예열 뒤. 최대 52초
1. 조회 부하 EKS
2. 질문 처리 온프렘 (10/23 기준값) 접수 46ms ✅ / 전체 90ms 접수 58ms / 전체 105ms 0% ✅ 5 api 2, worker 1 접수→완료 p95 2.08초 ✅ (2초 확인 간격이 대부분), FAILED·타임아웃 0
2. 질문 처리 EKS
3. 스파이크 EKS
4. 롤링 배포 온프렘 (10/26, M5-9) 105ms ✅ 194ms 0% ✅ (7,579건 중 0) 19 api 2 (교체 중 3) 부하 50명 6분 중 kubectl rollout restart deployment/api, 롤아웃 41초. 질문 FAILED·타임아웃 0
4. 롤링 배포 EKS

서버 지표 (Grafana, 10/23 첫 측정 구간):

항목 값
Traefik 초당 요청 최대 약 78 req/s
5xx 비율 0%
api 파드 CPU 파드당 최대 약 0.17 core
메모리 api 파드 약 400MiB, MySQL 약 500MiB

시나리오 4 (10/26) 적용한 무중단 설정: 앱 graceful shutdown(처리 중 요청 최대 15초) + api preStop 10초 < terminationGracePeriodSeconds 30초, maxUnavailable: 0, 새 파드는 조회 API 예열 뒤 readiness UP, 워커는 처리 중 질문을 40초 기다리고 못 끝내면 대기열로 반환.

⚠️ **GitHub.com Fallback** ⚠️