k6로 시작하는 부하 테스트 - 설치부터 첫 테스트까지

2025년 12월 09일

devops

# k6# Load Testing# Performance Testing# DevOps

들어가며

서비스를 배포하기 전에 “우리 서버가 트래픽을 얼마나 견딜 수 있을까?”, “갑자기 사용자가 몰리면 어떻게 될까?”라는 질문에 답할 수 있어야 합니다. k6는 현대적인 부하 테스트 도구로, JavaScript로 테스트 시나리오를 작성하고 간편하게 성능을 검증할 수 있습니다.

k6란?

k6는 Grafana Labs에서 개발한 오픈소스 부하 테스트 도구입니다. Go로 작성되어 높은 성능을 자랑하며, JavaScript(ES6+)로 테스트 스크립트를 작성할 수 있어 접근성이 뛰어납니다.

k6의 주요 특징

  • 개발자 친화적: JavaScript로 테스트 시나리오 작성
  • CLI 기반: 가볍고 빠르며 CI/CD 통합이 쉬움
  • 확장성: 대규모 부하 생성 가능
  • 다양한 프로토콜: HTTP/1.1, HTTP/2, WebSocket, gRPC 지원
  • 실시간 모니터링: 테스트 중 실시간으로 메트릭 확인 가능

다른 도구와의 비교

도구 언어 UI 특징
k6 JavaScript CLI 가볍고 빠름, 코드 기반
JMeter Java GUI 기능이 많지만 무거움
Gatling Scala GUI/CLI Scala DSL 사용
Locust Python Web UI Python 코드, 분산 테스트

설치하기

macOS

Homebrew를 사용하면 간단하게 설치할 수 있습니다:

brew install k6

Linux

APT (Debian/Ubuntu):

sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
  --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | \
  sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

YUM (CentOS/RHEL):

sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6

Docker

Docker로 실행할 수도 있습니다:

docker pull grafana/k6

설치 확인

k6 version
k6 v1.4.2 (commit/devel, go1.25.4, darwin/arm64)

k6 테스트 사이트 - QuickPizza

k6는 테스트 연습을 위해 quickpizza.grafana.com을 제공합니다. 이 사이트는 Grafana에서 만든 데모 앱으로, k6 성능 테스트와 관찰성(observability) 학습을 위해 설계되었습니다.

QuickPizza는 기존 test.k6.io, httpbin.test.k6.io 등의 레거시 테스트 사이트들을 대체합니다. HTTP/REST API는 물론 gRPC 테스트도 지원하며, 실제 애플리케이션처럼 로그, 메트릭, 트레이스를 생성합니다.

주의: 공개된 공유 환경이므로 고부하 테스트는 피하고, 학습과 간단한 테스트 용도로만 사용하세요.

첫 번째 테스트 스크립트

가장 간단한 HTTP GET 요청 테스트를 작성해 보겠습니다.

기본 스크립트 구조

k6 스크립트는 크게 세 부분으로 구성됩니다:

// test.js

// 1. 초기화 (init) - 한 번만 실행
import http from 'k6/http';
import { sleep } from 'k6';

// 2. 옵션 설정
export const options = {
  vus: 10,          // Virtual Users (동시 사용자 수)
  duration: '30s',  // 테스트 지속 시간
};

// 3. 메인 함수 - 각 VU가 반복 실행
export default function() {
  http.get('https://quickpizza.grafana.com');
  sleep(1);  // 1초 대기
}

스크립트 실행

k6 run test.js

결과 해석

테스트를 실행하면 다음과 같은 출력을 볼 수 있습니다:

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: test.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
              * default: 10 looping VUs for 10s (gracefulStop: 30s)



  █ TOTAL RESULTS

    HTTP
    http_req_duration..............: avg=92.75ms min=4.8ms med=96.47ms max=191.42ms p(90)=185.23ms p(95)=186.13ms
      { expected_response:true }...: avg=92.75ms min=4.8ms med=96.47ms max=191.42ms p(90)=185.23ms p(95)=186.13ms
    http_req_failed................: 0.00%  0 out of 178
    http_reqs......................: 178    15.881114/s

    EXECUTION
    iteration_duration.............: avg=1.23s   min=1.17s med=1.18s   max=1.64s    p(90)=1.6s     p(95)=1.63s
    iterations.....................: 89     7.940557/s
    vus............................: 9      min=9        max=10
    vus_max........................: 10     min=10       max=10

    NETWORK
    data_received..................: 415 kB 37 kB/s
    data_sent......................: 43 kB  3.8 kB/s




running (11.2s), 00/10 VUs, 89 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  10s

주요 메트릭 설명

k6 v1.4+에서는 결과가 HTTP, EXECUTION, NETWORK 세 가지 카테고리로 구분되어 표시됩니다.

HTTP 메트릭

  • http_req_duration: 요청 소요 시간 (평균 92.75ms, P95 186.13ms)
    • 이 값이 낮을수록 서버가 빠르게 응답합니다
  • http_req_failed: 실패한 요청 비율 (0%)
    • 5xx 오류나 네트워크 실패를 의미합니다
  • http_reqs: 총 HTTP 요청 수 (178회, 초당 15.88개)
    • 서버가 처리한 총 트래픽량입니다

EXECUTION 메트릭

  • iteration_duration: 한 번의 반복(iteration) 소요 시간 (평균 1.23초)
    • sleep(1) 포함한 전체 시나리오 실행 시간입니다
  • iterations: 완료된 반복 횟수 (89회, 초당 7.94개)
    • 각 VU가 시나리오를 몇 번 실행했는지 보여줍니다
  • vus: 동시 사용자 수 (최소 9명, 최대 10명)
    • 동시에 실행 중인 Virtual User 수입니다

NETWORK 메트릭

  • data_received: 수신한 데이터량 (415 kB, 초당 37 kB/s)
  • data_sent: 전송한 데이터량 (43 kB, 초당 3.8 kB/s)

백분위수 (Percentile)

  • p(90): 90%의 요청이 이 시간 이내에 완료
  • p(95): 95%의 요청이 이 시간 이내에 완료

평균보다 백분위수가 중요한 이유: 극단값의 영향을 받지 않기 때문입니다. P95가 186ms라면 사용자의 95%가 186ms 이내에 응답을 받았다는 뜻입니다. 평균은 하나의 매우 느린 요청 때문에 왜곡될 수 있지만, 백분위수는 실제 사용자 경험을 더 정확하게 반영합니다.

다양한 실행 옵션

CLI 옵션으로 실행

스크립트를 수정하지 않고 CLI 옵션으로 설정을 변경할 수 있습니다:

# 사용자 수와 지속 시간 지정
k6 run --vus 50 --duration 1m test.js

# 반복 횟수 지정
k6 run --vus 10 --iterations 100 test.js

# HTTP/2 강제
k6 run --http2 test.js

옵션 객체로 상세 설정

export const options = {
  // 기본 부하 설정
  vus: 10,
  duration: '30s',

  // 또는 단계별 부하 증가 (ramping)
  stages: [
    { duration: '1m', target: 20 },  // 1분 동안 20명으로 증가
    { duration: '3m', target: 20 },  // 3분 동안 20명 유지
    { duration: '1m', target: 0 },   // 1분 동안 0명으로 감소
  ],

  // 임계값 설정 (테스트 통과 조건)
  thresholds: {
    'http_req_duration': ['p(95)<500'],  // 95%의 요청이 500ms 이내
    'http_req_failed': ['rate<0.01'],    // 실패율 1% 이하
  },

  // 기타 옵션
  noConnectionReuse: false,  // 연결 재사용 허용
  userAgent: 'MyK6Test/1.0',
};

실전 예제

1. POST 요청 테스트

import http from 'k6/http';
import { check } from 'k6';

export default function() {
  const url = 'https://httpbin.org/post';
  const payload = JSON.stringify({
    username: 'testuser',
    password: 'testpass123',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post(url, payload, params);

  // 응답 검증
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
}

2. 인증이 필요한 API 테스트

import http from 'k6/http';
import { check } from 'k6';

export default function() {
  // 1. 로그인하여 토큰 획득
  const loginRes = http.post('https://api.example.com/login', JSON.stringify({
    username: 'test',
    password: 'test123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });

  const token = loginRes.json('token');

  // 2. 토큰으로 인증된 요청
  const authRes = http.get('https://api.example.com/profile', {
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  });

  check(authRes, {
    'authenticated successfully': (r) => r.status === 200,
  });
}

3. 단계적 부하 증가 테스트

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 10 },   // 워밍업: 10명으로 증가
    { duration: '1m', target: 50 },    // 부하 증가: 50명으로
    { duration: '2m', target: 50 },    // 유지
    { duration: '1m', target: 100 },   // 스파이크: 100명으로
    { duration: '2m', target: 100 },   // 유지
    { duration: '30s', target: 0 },    // 쿨다운: 0명으로
  ],
};

export default function() {
  http.get('https://quickpizza.grafana.com');
  sleep(1);
}

이 패턴은 실제 트래픽을 시뮬레이션합니다:

  1. 워밍업: 서버가 준비할 시간을 줍니다
  2. 부하 증가: 점진적으로 부하를 늘립니다
  3. 스파이크: 갑작스런 트래픽 증가를 테스트합니다
  4. 쿨다운: 서버가 정상화되는지 확인합니다

4. 체크(Checks)로 응답 검증

import http from 'k6/http';
import { check } from 'k6';

export default function() {
  const res = http.get('https://httpbin.org/json');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'has slideshow': (r) => r.json().hasOwnProperty('slideshow'),
    'response time < 200ms': (r) => r.timings.duration < 200,
    'content type is JSON': (r) => r.headers['Content-Type'].includes('application/json'),
  });
}

체크가 실패해도 테스트는 계속 진행되지만, 결과 리포트에 실패율이 표시됩니다:

     ✓ status is 200
     ✓ has slideshow
     ✗ response time < 200ms
     ✓ content type is JSON

     checks.........................: 75.00% ✓ 3    ✗ 1

5. 환경 변수 사용

import http from 'k6/http';

const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com';
const USERNAME = __ENV.USERNAME || 'defaultuser';

export default function() {
  http.get(`${BASE_URL}/api`);
}

실행 시 환경 변수 전달:

k6 run -e BASE_URL=https://api.example.com -e USERNAME=testuser test.js

결과 저장 및 시각화

JSON 파일로 저장

k6 run --out json=results.json test.js

CSV로 저장

k6 run --out csv=results.csv test.js

Grafana + InfluxDB 연동

실시간 대시보드를 구성하려면 InfluxDB와 Grafana를 사용할 수 있습니다:

# InfluxDB로 메트릭 전송
k6 run --out influxdb=http://localhost:8086/k6 test.js

Grafana에서 k6 대시보드를 import하면 실시간으로 테스트 결과를 시각화할 수 있습니다.

주의사항

1. 테스트 대상 확인

운영 환경에 부하 테스트를 실행하면 실제 사용자에게 영향을 줄 수 있습니다. 반드시 테스트 환경이나 스테이징 환경에서 실행하세요.

2. 네트워크 대역폭

대규모 부하 테스트는 많은 대역폭을 소비합니다. 특히 집에서 테스트할 때는 ISP의 트래픽 제한에 걸릴 수 있습니다.

3. 로컬 머신의 한계

단일 머신에서 생성할 수 있는 VU 수에는 한계가 있습니다. 매우 큰 부하가 필요하면 k6 Cloud나 분산 실행을 고려하세요.

4. Sleep 추가

sleep() 없이 요청을 연속으로 보내면 비현실적인 부하를 생성합니다. 실제 사용자는 페이지를 읽거나 생각하는 시간이 필요하므로 적절한 대기 시간을 추가하세요.

다음 단계

이제 k6의 기본을 익혔다면 다음 단계로 넘어갈 수 있습니다:

  1. 시나리오(Scenarios): 복잡한 부하 패턴 구성
  2. 메트릭(Metrics): 커스텀 메트릭 추가
  3. 체크와 임계값(Checks & Thresholds): 테스트 통과 기준 설정
  4. 모듈화: 재사용 가능한 함수와 헬퍼 작성
  5. CI/CD 통합: GitHub Actions, GitLab CI에서 자동 실행
  6. 분산 테스트: 여러 머신으로 부하 분산

정리

k6는 간단하면서도 강력한 부하 테스트 도구입니다. 핵심 포인트:

  1. JavaScript로 테스트 작성: 진입 장벽이 낮고 유연함
  2. CLI 기반: CI/CD 통합이 쉬움
  3. 실시간 피드백: 테스트 중 즉시 결과 확인
  4. 확장 가능: 간단한 테스트부터 복잡한 시나리오까지

부하 테스트는 서비스 출시 전 필수 단계입니다. k6로 미리 성능을 검증하고 병목을 찾아내면, 실제 트래픽 폭증 시에도 안정적인 서비스를 제공할 수 있습니다.


참고 자료

© 2025, 미나리와 함께 만들었음