해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.
Day 87 - Hands-on Performance Testing with k6
성능 테스트의 궁극적인 목표는 테스트 대상 시스템의 실생활에서의 신뢰성을 검증하는 것이다.
단순히 기능이 작동하는지를 넘어, 실제 사용 환경과 유사한 부하 상황에서 시스템이 어떻게 반응하는지를 확인해야 한다.
이를 위해 주로 다음 세 가지 측면을 중점적으로 살펴본다.
지연 시간 (응답 속도) : 시스템이 사용자 요청에 얼마나 빠르게 응답하는가?
오류 : 부하가 가해졌을 때 시스템이 에러를 반환하거나 내부적인 오류가 발생하지 않는가?
기능적 무결성 : 부하 상태에서도 통합 테스트나 End-to-End 사용자 흐름이 정상적으로 작동하는가?
성능 테스트와 부하 테스트는 매우 밀접한 관계가 있다.
사실상 부하 테스트는 부하(Load)를 동반한 성능 테스트라고 정의할 수 있다.
부하 테스트는 크게 테스트 지속 시간(Duration)과 트래픽 부하량(Load)이라는 두 가지 변수에 따라 다음과 같이 세분화된다.
평균 부하 테스트 (Average Load Test):
평소 운영 환경에서 발생하는 평균적인 트래픽 수준을 시뮬레이션
일상적인 성능 기준을 잡는 데 사용
스트레스 테스트 (Stress Test):
스파이크 테스트 (Spike Test):
특정 이벤트(예: 티켓 예매 오픈) 시점처럼, 트래픽이 매우 낮은 상태에서 갑자기 매우 높은 상태(Peak)로 급증하는 상황을 시뮬레이션
시스템의 급격한 확장에 대한 대응력을 테스트
스모크 테스트 (Smoke Test):
최소한의 부하로 아주 짧은 시간 동안 수행
본격적인 테스트 전에 스크립트 오류 여부와 시스템의 기본 가용성을 빠르게 점검하는 것이 목적
소크 테스트 (Soak Test):
장시간(2시간, 4시간, 혹은 그 이상) 동안 부하를 지속
메모리 누수(Memory Leak), 리소스 고갈, 디스크 공간 부족 등 장기 운영 시에만 드러나는 문제를 찾아내는 테스트
브레이크포인트 테스트 (Breakpoint Test):
시스템이 과부하로 멈추거나 오류를 낼 때까지 부하를 지속적으로 증가시킴.
시스템의 물리적 한계점을 파악하여 용량 계획을 세우는 데 도움을 줌.
모든 성능 테스트가 동시 접속 부하를 필요로 하는 것은 아니다.
프로파일링 (Profiling): 단일 사용자/스레드로 특정 함수나 로직의 실행 시간을 분석한다.
브라우저 성능 테스트: 백엔드 부하보다는 프론트엔드 최적화(렌더링 속도, 에셋 로딩 시간 등)에 집중한다. 이 경우 백엔드에 부하를 주입할 필요가 없다.
신세틱 모니터링 (Synthetic Monitoring): 운영 중인 시스템의 가용성을 확인하기 위해 주기적으로 가상의 트랜잭션을 발생시켜 모니터링한다.
성능 테스트 도구는 매우 다양하며, 사용 방식과 목적에 따라 크게 4가지 카테고리로 분류할 수 있다.
벤치마킹 도구
대표 도구: ab (Apache Bench), wrk
특징: 단일 엔드포인트나 URL 리스트를 대상으로 단순한 부하를 발생시킨다.
장단점: 사용이 매우 간편하고 결과가 터미널에 바로 출력되지만, 복잡한 시나리오 구성은 어렵다.
프로토콜 특화 도구
gRPC, MQTT, Kafka 등 특정 프로토콜 테스트에 최적화된 도구들이 존재한다.GUI 기반 도구
대표 도구: JMeter
특징: 가장 오래되고 널리 쓰이는 도구 중 하나다. GUI를 통해 테스트 단계를 클릭으로 구성할 수 있다.
장단점: 코딩 없이 접근 가능하고 커뮤니티가 크지만, 스크립트 관리가 어렵고 리소스를 많이 차지할 수 있다.
코드 기반(Scripting) 도구
대표 도구: k6 (JavaScript/Go), Locust (Python), Gatling (Scala/Java)
특징: 개발자 친화적이며, 코드로 테스트 시나리오를 작성하므로 버전 관리(Git)가 용이하고 유연성이 매우 높다.
참고: Artillery와 같이 YAML 설정을 사용하는 도구도 있지만, 스크립팅 언어만큼의 완벽한 유연성을 제공하지는 못할 수 있다.
k6는 개발자 중심의 오픈 소스 부하 테스트 도구다.
k6는 단순히 부하를 일으키는 도구를 넘어, 개발자 경험(DX, Developer Experience)을 최우선으로 설계된 오픈 소스 성능 테스트 프레임워크이며, Grafana Labs에 인수된 이후, 관측성(Observability) 생태계와 결합하며 더욱 강력해졌다.

k6의 가장 큰 기술적 특징은 "작성은 쉬운 JavaScript로, 실행은 강력한 Go로" 수행한다는 점이다.
스크립팅의 용이성:
개발자에게 익숙한 ES6 자바스크립트 문법을 사용하여 테스트 시나리오를 작성한다.
덕분에 별도의 도구 사용법을 익힐 필요 없이, 복잡한 비즈니스 로직을 코드로 유연하게 구현할 수 있다.
고성능 실행 엔진:
작성된 스크립트는 내부적으로 내장된 Go 기반의 JS 인터프리터(goja)를 통해 실행된다.
이는 Node.js나 브라우저 엔진 위에서 구동되는 것이 아니기 때문에 오버헤드가 극도로 낮다.
스레드 대신 가상 사용자(VU):
기존의 JMeter가 OS Thread를 사용하여 메모리 소비가 컸던 반면, k6는 Go의 Goroutine을 활용하여 가상 사용자(Virtual Users)를 구현한다.
덕분에 단일 인스턴스에서 수만 명의 동시 접속자를 시뮬레이션하더라도 메모리 사용량이 현저히 적다.

첨부된 이미지가 보여주듯, k6는 소프트웨어 개발 수명 주기(SDLC) 전반에 걸쳐 성능을 관리할 수 있도록 설계되었다.
Pre-production (Proactive): 배포 전 단계에서 가상 사용자 트래픽(Virtual User Traffic)을 통해 잠재적인 병목 구간을 사전에 발견하고 해결한다.
Production (Reactive): 실제 운영 환경의 모니터링 데이터(Real User Traffic)와 테스트 결과를 비교 분석한다.
Grafana와의 결합:
또한, k6의 가장 강력한 장점은 하나의 테스트 스크립트를 수정 없이 다양한 환경에서 실행할 수 있다는 점이다.
Local (로컬): 개발자 개인 PC나 단일 서버에서 실행한다. k6 run 명령어를 사용한다.
Distributed (분산): Kubernetes 클러스터 등을 활용하여 여러 파드(Pod)에 부하를 분산시켜 대규모 테스트를 수행한다.
Cloud (클라우드): k6의 상용 클라우드 서비스(Grafana Cloud k6)를 이용하여 인프라 관리 없이 대규모 테스트를 수행하고 결과를 저장/분석한다.
이는 로컬 개발부터 대규모 프로덕션 테스트까지 매끄러운 확장을 가능하게 한다.
추가적으로, k6를 사용할 때 반드시 이해해야 할 점은 k6는 브라우저가 아니라는 것이다.
k6는 프로토콜 레벨(HTTP 등)에서 요청을 보내고 응답을 측정하는 도구다.
웹 페이지의 HTML을 가져오지만, 브라우저처럼 화면을 렌더링하거나 페이지 내의 JavaScript를 실행하지는 않는다.
따라서 정확한 페이지 로딩 속도(렌더링 포함)를 측정하고 싶다면, 최근 k6에 실험적으로 추가된 k6 Browser 모듈(Chrome DevTools Protocol 기반)을 활용하거나 다른 도구를 병행해야 한다.
하지만 백엔드 API의 성능을 극한으로 테스트하는 목적이라면 k6의 기본 모드가 가장 효율적이고 강력한 선택지가 된다.
해당 프레젠테이션의 데모는 피자 주문 및 추천 서비스인 'Quick Pizza' 애플리케이션을 대상으로 진행된다.
Docker Compose를 사용하여 애플리케이션, Grafana, Prometheus를 로컬 환경에 구성했다.
k6 스크립트는 자바스크립트(ES6) 기반이며, 크게 Imports, Options, Default Function 세 부분으로 구성된다.
Imports: k6/http와 같은 내장 모듈이나 외부 라이브러리를 불러온다.
Options: 가상 사용자 수(VUs), 지속 시간(Duration) 등 테스트 실행 설정을 정의한다.
Default Function: 가상 사용자가 반복해서 수행할 실제 비즈니스 로직을 작성한다.
import http from 'k6/http';
import { sleep, check } from 'k6';
// 1. 설정 (Options): 부하 수준 정의
export const options = {
vus: 5, // 가상 사용자 5명
duration: '10s', // 10초 동안 실행
};
// 2. 메인 로직 (Default Function): VU가 반복 실행할 시나리오
export default function () {
const res = http.post('http://localhost:3333/pizza'); // HTTP 요청 (POST)
// 응답 확인 및 로깅 (선택 사항)
console.log(`Response status: ${res.status}`);
sleep(1); // 1초 대기 (Think Time)
}
k6는 매우 정교한 부하 제어 기능을 제공한다. 상황에 따라 다양한 옵션을 적용할 수 있다.
A. Iterations 기반 실행
기본적으로 시간(Duration) 기반으로 실행하지만, iterations 옵션을 사용하여 총 실행 횟수를 지정할 수도 있다. CLI에서 플래그로 덮어쓰는 것도 가능하다.
# 5명의 VU가 협력하여 총 20번의 시나리오를 수행하고 종료
k6 run --iterations 20 --vus 5 script.js
B. 스테이지(Stages)를 이용한 램핑(Ramping)
export const options = {
stages: [
{ duration: '5s', target: 5 }, // 0명에서 5명으로 5초 동안 증가 (Ramp-up)
{ duration: '10s', target: 5 }, // 5명 유지 (Steady State - 안정성 확인)
{ duration: '5s', target: 0 }, // 5명에서 0명으로 감소 (Ramp-down)
],
};C. 초당 요청 수(RPS) 제어: Constant Arrival Rate
scenarios의 constant-arrival-rate 실행기를 사용한다.export const options = {
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: 30, // 초당 30회 반복 목표
timeUnit: '1s', // 시간 단위
duration: '30s', // 30초 동안 실행
preAllocatedVUs: 50, // 미리 할당할 VU 수
maxVUs: 100, // 최대 허용 VU 수
},
},
};k6는 테스트 중 발생하는 데이터를 4가지 유형의 메트릭으로 수집한다.
Counter: 누적 합계 (예: http_reqs)
Gauge: 특정 시점의 값 (예: vus)
Rate: 성공/실패 비율 (예: checks, http_req_failed)
Trend: 통계적 분포 (예: http_req_duration - p95, p99 등)
커스텀 메트릭 (Custom Metrics) 이란?
추가적으로, 기본 메트릭 외에 비즈니스 로직에 맞는 데이터를 수집할 수 있다.
import http from 'k6/http';
import { Trend, Counter } from 'k6/metrics';
// 커스텀 메트릭 정의
const ingredientsTrend = new Trend('quickpizza_ingredients');
const pizzaCounter = new Counter('quickpizza_number_of_pizzas');
export default function () {
const res = http.get('http://localhost:3333/pizza');
// 응답받은 JSON 데이터 파싱
const pizza = res.json();
// 메트릭에 데이터 추가
ingredientsTrend.add(pizza.ingredients.length); // 재료 개수 통계
pizzaCounter.add(1); // 피자 개수 카운트
}
터미널 요약 및 파일 내보내기
테스트 종료 후 터미널에 출력되는 요약 리포트는 집계된 데이터다.
상세 분석을 위해 handleSummary() 함수를 이용해 JSON 파일로 내보낼 수 있다.
실시간 데이터 스트리밍
가장 강력한 기능은 --out 플래그를 사용하여 Raw Data를 시계열 데이터베이스로 실시간 전송하는 것이다.
# Prometheus로 데이터 실시간 전송 (Remote Write 기능 사용)
k6 run --out experimental-prometheus-rw script.js
위 명령어를 실행하면 k6가 데이터를 Prometheus로 전송하고, 이를 Grafana 대시보드에서 실시간 그래프로 확인할 수 있다.
스파이크 발생 지점이나 지연 시간 변화 추이를 시각적으로 파악하는 데 필수적이다.
k6는 테스트 성공 여부를 판단하고 검증하기 위해 두 가지 방법을 제공한다.
1. Checks
단순 확인용(Boolean) 검증으로, 실패해도 테스트가 중단되거나 실패로 기록되지 않는다.
import { check } from 'k6';
export default function (res) {
// 응답 코드가 200인지 확인 (실패해도 테스트는 계속 진행됨)
check(res, { 'is status 200': (r) => r.status === 200 });
}
2. Thresholds (임계값)
테스트의 성공/실패 기준을 정의하며, 이를 위반하면 k6는 실패(Non-zero) 종료 코드를 반환하므로 CI/CD 파이프라인에서 매우 중요하다.
export const options = {
thresholds: {
// 에러율(http_req_failed)이 1% 미만이어야 함
'http_req_failed': ['rate<0.01'],
// 응답 시간(http_req_duration)의 95%가 500ms 이내여야 함
'http_req_duration': ['p95<500'],
// 커스텀 메트릭 평균이 2 미만이어야 함
'quickpizza_ingredients': ['avg<2'],
},
};
성능 테스트는 시스템의 신뢰성을 보장하기 위한 선택이 아닌 필수 과정이다.
k6는 강력한 스크립팅 기능과 확장성, 그리고 개발자 친화적인 경험(DX)을 제공하여 이를 효율적으로 지원한다.