성능 분석 실패기 : Grafana K6, N+1 문제를 폭로하다

2025. 7. 24. 00:22·Jungle

배경: 성능 개선의 필요성을 느끼다

나만의 무기 만들기 폴리싱 주간을 맞아, 현재 서비스의 성능을 한 단계 끌어올려야 했습니다. To-Do 리스트는 아래와 같이 산더미처럼 쌓여 있었죠.

  • FastAPI 비동기 처리 구현 (try-on)
  • 검색엔진 구현
  • try-on 알고리즘 구현
  • 개인화 추천상품 알고리즘 구현
  • TIO 애플리케이션 성능개선

저는 이 중에서 성능 개선을 맡았습니다. 왜 성능 개선이 필요하다고 생각했을까요? 이전에 간단히 진행했던 부하 테스트에서 문제의 징후를 발견했기 때문입니다.


징후: nGrinder 테스트에서 발견된 이상 신호

이전에 nGrinder를 이용해 메인 페이지, 상세 페이지, 검색 페이지에 대한 간단한 GET 요청 테스트를 진행한 적이 있습니다. 가상 사용자(VUser) 수를 늘려가며 5분간 테스트한 결과는 충격적이었습니다.

API VUser 200명 VUser 400명 VUser 1,000명 VUser 2,000명
메인 페이지 3-100ms 26-128ms 42-108ms 1-317ms
상세 페이지 357-2,099ms 310-2,973ms 4,099-16,685ms 최대 17,265ms
검색 페이지 (테스트 안됨) (테스트 안됨) 937-1,434ms 1,396-8,146ms

메인 페이지는 사용자가 늘어도 안정적인 반면, 상세 페이지는 사용자가 200명만 되어도 응답시간이 2초를 넘기 시작하더니, 1,000명에 이르자 최대 17초까지 폭증했습니다.

단순 페이지 호출만으로도 이런 병목이 발생한다는 것은, 상세 페이지를 불러오는 과정에 **복잡하고 비효율적인 쿼리, 즉 N+1 문제**가 숨어있음을 직감하게 했습니다.


계획: 무엇을 어떻게 개선할 것인가?

"감"만으로는 부족합니다. 정확한 데이터를 기반으로 개선 작업을 진행하기 위해, 먼저 성능 테스트를 통해 현재 상태(Before)의 지표를 명확히 측정하기로 했습니다.

개선 대상 목록

  1. DB 최적화
    • N+1 문제 해결 (가장 시급)
    • 커넥션 풀 최적화
    • 읽기 전용(Read Replica) RDS 도입
  2. Spring 애플리케이션 최적화
    • try-on 기능 비동기 처리
    • JVM 최적화
  3. 프론트엔드 최적화
    • Next.js 이미지 최적화
    • Lazy Loading 적용
  4. 캐싱
    • Redis 캐시 도입

테스트 도구: Grafana K6

nGrinder, JMeter, Locust 등 다양한 도구가 있지만, 이번에는 Grafana K6를 선택했습니다.

  • Go 언어로 개발되어 성능이 우수합니다.
  • 테스트 스크립트를 JavaScript로 작성하여 개발자에게 친숙합니다.
  • CI/CD 파이프라인에 통합하기 쉽습니다.
  • Grafana, Datadog 등 다양한 모니터링 도구와 연동이 뛰어납니다.
K6 설치 (macOS 기준)
brew install k6

설치 후 k6 version 명령어로 설치를 확인합니다.

image.png


실험: 실제 사용자 여정 시나리오 테스트

단순 API 호출이 아닌, 실제 사용자의 행동 패턴을 모방한 복합적인 시나리오를 설계했습니다.

  • 테스트 환경: 동시 사용자 10명 → 20명 → 40명(피크)으로 점진적 증가 (총 22분)
  • 테스트 방식: 실제 계정으로 로그인하여 인증 토큰(JWT) 기반으로 진행

🎭 단계별 사용자 여정

  1. 🔐 로그인: 이메일/비밀번호로 로그인하여 JWT 토큰 획득
  2. 📍 배송지 관리: 기존 배송지 확인 후, 없으면 신규 추가
  3. 📱 개인화 추천 조회: 로그인 유저 기반의 메인 페이지 추천/랭킹 상품 조회
  4. 👀 상품 상세 조회 + 옵션 선택 (핵심): 추천 상품 3개를 둘러보며, 실제 사용자처럼 랜덤 사이즈/색상/수량을 선택하고 3~8초간 머무름
  5. 🏠 메인페이지 재방문: 상품을 본 뒤 메인으로 돌아오는 행동
  6. 👗 가상 피팅: 선택한 상품과 랜덤 신체 정보로 가상 피팅 시도
  7. 💖 찜하기: 마음에 드는 상품 1개를 찜 목록에 추가
  8. 👔 옷장 조회: 개인 옷장 기능 확인
  9. 🛒 장바구니 추가: 상세 조회 시 선택했던 옵션(사이즈, 색상 등)을 그대로 장바구니에 추가
  10. 📝 주문서 작성: 등록된 배송지를 선택하여 주문서 작성
  11. 💳 결제: 실제 결제는 안전을 위해 제외

K6 테스트 스크립트

위 시나리오를 바탕으로 작성된 JavaScript 테스트 스크립트입니다. (전체 코드는 글의 마지막에 첨부)

// k6-authenticated-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
// ...

export let options = {
  stages: [
    { duration: '2m', target: 10 },   // 10명으로 시작
    { duration: '5m', target: 20 },   // 20명으로 증가
    { duration: '10m', target: 20 },  // 20명 유지
    { duration: '3m', target: 40 },   // 40명으로 피크
    { duration: '2m', target: 0 },    // 종료
  ],
  // ...
};

// ... (전체 시나리오 구현)

결과: 모든 것이 터져나갔다

테스트를 시작하자 처음에는 순조로워 보였습니다.

image1.png image2.png

하지만 그것도 잠시, 터미널은 곧 에러 메시지로 뒤덮이기 시작했습니다.

image3.png

급히 테스트를 종료하고 AWS 모니터링 대시보드를 확인했습니다. 인스턴스가 죽었다 살아나기를 반복하고 있었습니다.

image4.png image5.png

최종 결과는 처참했습니다.

image6.png image7.png

  • API 성공률: 12.90% (목표: 90%)
  • 로그인 성공률: 12.98% (목표: 95%)
  • 사용자 여정 완료율: 0.00% (목표: 85%)
  • HTTP 요청 실패율: 81.06%
  • 평균 응답시간: 27.21초 (목표: 3초 이하)

모든 지표가 박살 났습니다. CPU 사용률은 테스트가 진행된 10분 남짓한 시간에 100%를 찍었습니다.

image8.png


분석: 범인은 역시 N+1

1. N+1 쿼리의 증폭 효과

문제의 원인은 ProductService 코드에 있었습니다. 비회원 메인 페이지를 구성하기 위해 모든 카테고리를 조회(categoryRepository.findAll())한 뒤, 각 카테고리별로 상품을 또 조회하는 로직이었습니다.

image9.png

이로 인해 발생하는 쿼리 증폭은 이렇습니다.

  • 사용자 1명의 메인페이지 요청 → findAll 1번 + 카테고리 수(N)만큼 추가 쿼리 → 약 101번의 쿼리 발생
  • 동시 사용자 20명 → 20명 x 101번 = 2,020번의 쿼리 발생

테스트 중 ALB에는 최대 63,000건 이상의 요청이 기록되었습니다. 이를 N+1 쿼리 수와 곱하면,

63,000 요청 × 101 쿼리/요청 = 약 636만 번의 DB 쿼리!

이 엄청난 쿼리 폭풍이 DB 커넥션 풀을 고갈시키고, 결국 시스템 전체를 마비시킨 것입니다. RDS의 DBLoad와 EC2의 네트워크 패킷 수가 순간적으로 치솟은 것이 그 증거입니다.

image10.png image11.png image12.png

2. 미스터리: 왜 이전 테스트는 멀쩡했을까?

이전의 단순 nGrinder 테스트는 인증 없이 페이지만 호출했습니다. 이 요청들은 대부분 CDN(CloudFront)에서 캐싱된 정적 HTML 페이지를 반환하며 끝났습니다. 즉, 백엔드 API 서버까지 요청이 도달하지 않아 N+1 문제가 발생할 일이 없었던 것입니다.

  • 단순 테스트: CDN 성능 테스트 (DB 부하 없음)
  • 이번 테스트: 실제 사용자 시나리오 테스트 (DB 부하 폭증)

결국 이번 실패는 CDN 뒤에 숨어있던 백엔드 API의 취약점을 정확히 드러낸, 값진 실패였습니다.

결론: 당장 N+1 문제부터 해결하자

성능 개선의 첫걸음은 가장 큰 병목 지점인 N+1 문제 해결이 되어야 함을 명확히 알 수 있었습니다. 다음 편에서는 이 문제를 해결하고, 다시 K6 테스트를 통해 성능이 얼마나 개선되었는지 측정해보겠습니다.

728x90

'Jungle' 카테고리의 다른 글

TIO 개선 소요 파악 - 성능 최적화  (0) 2025.07.29
상세이미지 버킷 링크 노출  (0) 2025.07.24
17일? 못 기다려! 크롤러 성능 개선 삽질기 (Selenium → Requests)  (0) 2025.07.23
nGrinder 설치 및 실행 가이드: 트러블슈팅 포함  (0) 2025.07.23
이미지 트러블슈팅 완전정복: S3 리전 오류, Mixed Content, Next.js 최적화  (0) 2025.07.23
'Jungle' 카테고리의 다른 글
  • TIO 개선 소요 파악 - 성능 최적화
  • 상세이미지 버킷 링크 노출
  • 17일? 못 기다려! 크롤러 성능 개선 삽질기 (Selenium → Requests)
  • nGrinder 설치 및 실행 가이드: 트러블슈팅 포함
ahpicl64
ahpicl64
in the clouds
  • ahpicl64
    구름
    ahpicl64
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • WIL (4)
      • Jungle (36)
      • AWS (2)
      • SQL (2)
      • CS:APP (17)
      • Algorithm (10)
      • K8s (7)
      • 자료 구조 (10)
      • Spring (4)
      • React (0)
      • 운영체제 (1)
      • 기타등등 (2)
      • 이야기 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    부하테스트
    Spring
    queue
    github actions
    DevOps
    자료구조
    CSAPP
    컴퓨터시스템
    DB
    AWS
    k8s
    트러블슈팅
    Spring boot
    S3
    python
    어셈블리
    IAM
    CloudFront
    알고리즘
    EC2
  • 02-21 08:19
  • hELLO· Designed By정상우.v4.10.3
ahpicl64
성능 분석 실패기 : Grafana K6, N+1 문제를 폭로하다
상단으로

티스토리툴바