
중요한 로직에 대해 처리량 및 정상 수행 여부를 확인하려면 e2e 테스트 혹은 통합 테스트가 필요하다.
또한 성능 저하 로직들은 시스템을 멈출 수 있다.
대규모 트래픽 서비스인지 여부를 떠나서 이러한 점들을 위해 테스트가 필요하다.
더불어 사내에서 정부과제를 위한 성능지표로 부하 테스트 및 스모크 테스트 지표를 요구하고 있다.
이를 위해 테스트 기법과 k6 사용법에 대해 간략히 소개하고자 한다.
☝
대부분의 테스트 기법들이 영문인데, 일부러 안 바꿨다.
나름 쉽게 한답시고 용어 자체를 바꾸면 읽는 독자로 하여금 헷갈리기도하고,
다른 개발자들 간에 소통이 어려울 것 같다.
(나 또한 추후에 이 포스트를 보고 테스트를 할 수 있으니,,,)
용어 자체는 의미 그대로이니 그대로 따라가자
목표
테스트 방법

목표
테스트 방법

목표
테스트 방법

목표
테스트 방법
목표
테스트 방법

목표
테스트 방법
가장 기본적으로 어떻게 호출하는지 알아보고, 옵션 활용법에 대해 알아보도록 하겠다.
우선 테스트 대상을 만들기 위해 아래와 같이 카운팅 Go 모듈을 작성해보았다.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
)
var count int = 0
func main() {
/** POST : Increase count */
http.HandleFunc("/count", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var previousCount = count
count++
fmt.Fprintf(w, "Previous count : %d -> Current count : %d\n", previousCount, count)
return
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
/** PUT : Update count by input value */
http.HandleFunc("/count/update", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut:
var countUpdate struct {
Value *int `json:"value"`
}
err := json.NewDecoder(r.Body).Decode(&countUpdate)
if err != nil {
http.Error(w, "Invalid request body"+err.Error(), http.StatusBadRequest)
}
count = *countUpdate.Value
fmt.Fprintln(w, "Count has updated as : "+strconv.Itoa(count))
return
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
실제로 curl 을 통해 제대로 작동하는지 확인해보았다.
// POST : count 증가
curl -X POST localhost:8080/count
// PUT : count 값 수정
curl -d '{"value":32}' -X PUT -H "Content-Type: application/json" http://localhost:8080/count/update

이후에 k6 를 통해 아래와 같이 호출할 수 있다.
import http from 'k6/http';
import { sleep } from 'k6';
const domain = 'localhost';
const port = '8080';
const postUrl = `http://${domain}:${port}/count`;
const putUrl = `http://${domain}:${port}/count/update`;
export default function () {
// POST : count 증가
let postData = {};
let resPost = http.post(postUrl, JSON.stringify(postData), {
headers: { 'Content-Type': 'application/json' },
});
// Log the status code and body so you can confirm behavior:
console.log(`POST /count → status ${resPost.status}, body: ${resPost.body.trim()}`);
// PUT : count 값 수정
const putData = { value: 42 }; // for example, set “count” to 42
let resPut = http.put(putUrl, JSON.stringify(putData), {
headers: { 'Content-Type': 'application/json' },
});
console.log(`PUT /count/update → status ${resPut.status}, body: ${resPut.body.trim()}`);
sleep(1);
}

다른 http method 에 대해 호출하고자 한다면 아래 예제들을 따라서 호출할 수 있다.
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('https://test.k6.io');
sleep(1);
}import http from 'k6/http';
const url = 'https://quickpizza.grafana.com/api/json';
const logoBin = open('./logo.png', 'b');
export default function () {
let data = { name: 'Bert' };
// Using a JSON string as body
let res = http.post(url, JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
console.log(res.json().json.name); // Bert
// Using an object as body, the headers will automatically include
// 'Content-Type: application/x-www-form-urlencoded'.
res = http.post(url, data);
console.log(res.json().form.name); // Bert
// Using a binary array as body. Make sure to open() the file as binary
// (with the 'b' argument).
http.post(url, logoBin, { headers: { 'Content-Type': 'image/png' } });
// Using an ArrayBuffer as body. Make sure to pass the underlying ArrayBuffer
// instance to http.post(), and not the TypedArray view.
data = new Uint8Array([104, 101, 108, 108, 111]);
http.post(url, data.buffer, { headers: { 'Content-Type': 'image/png' } });
}import http from 'k6/http';
const url = 'https://quickpizza.grafana.com/api/delete';
export default function () {
const params = { headers: { 'X-MyHeader': 'k6test' } };
http.del(url, null, params);
}import http from 'k6/http';
const url = 'https://quickpizza.grafana.com/api/put';
export default function () {
const headers = { 'Content-Type': 'application/json' };
const data = { name: 'Bert' };
const res = http.put(url, JSON.stringify(data), { headers: headers });
console.log(JSON.parse(res.body).name);
}import http from 'k6/http';
const url = 'https://quickpizza.grafana.com/api/patch';
export default function () {
const headers = { 'Content-Type': 'application/json' };
const data = { name: 'Bert' };
const res = http.patch(url, JSON.stringify(data), { headers: headers });
console.log(JSON.parse(res.body).name);
}k6 는 옵션을 지정하여 어떻게 test 를 구성할 지 지정할 수 있다.
대표적인 설정은 아래와 같고, 부가적인 설정을 하고자 한다면 k6 options reference 를 참고해보자.
An integer value specifying the number of VUs to run concurrently, used together with the iterations or duration options. If you’d like more control look at the stages option or scenarios.
동시에 실행할 가상 사용자의 수를 지정
iteration 과 duration 과 함께 사용
Available in k6 run and k6 cloud commands.
export const options = {
vus: 10,
duration: '1h',
};
테스트 실행에서 실행할 기본 함수의 총 반복 횟수
여기서 횟수는 함수 호출 횟수의 총 합
각 vu 별로 반복횟수를 동일하게 가져가려면 per-vu iterations executor 를 지정
// 5명의 가상 사용자가 다른 실행횟수 호출 -> 10 번을 채움
export const options = {
vus: 5,
iterations: 10,
};
테스트 실행이 실행될 총 기간을 지정
export const options = {
vus: 100,
duration: '3m',
};
stages 는 테스트 부하가 주입되는 단계를 설정할 수 있다.
일반적으로 테스트는 RampUp --> Load --> RampBackDown 의 순서로 수행이 된다.
아래 예시는 램프업, 플래스, 램프다운을 실행
export const options = {
stages: [
// RampUp : 1->10 for 3m
{ duration: '3m', target: 10 },
// Flat : 10 for 5m
{ duration: '5m', target: 10 },
// RampDown : 35->1 for 3m
{ duration: '3m', target: 0 },
],
};
요청을 포기하고 오류를 발생시키기 전에 k6가 따를 최대 HTTP 리디렉션 횟수
export const options = {
maxRedirects: 10,
};
유스케이스 맞는 실행 환경을 조정
Scenarios 를 참조하면 더 자세하게 볼 수 있다.
export const options = {
scenarios: {
my_api_scenario: {
// arbitrary scenario name
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5s', target: 100 },
{ duration: '5s', target: 0 },
],
gracefulRampDown: '10s',
env: { MYVAR: 'example' },
tags: { my_tag: 'example' },
},
},
};
DNS 테이블을 선언하여 다양한 URL 들을 매핑
/etc/hosts 혹은 윈도우의 C:\Windows\System32\drivers\etc\hosts 와 유사"domain" : "ip:port" 형태로 호스트를 지정할 수 있다.
v0.42.0 부터 asterisk 사용 가능하다.
export const options = {
hosts: {
'test.k6.io': '1.2.3.4',
'test.k6.io:443': '1.2.3.4:8443',
'*.grafana.com': '1.2.3.4',
},
};
테스트 종료 조건을 지정하고, 해당 종료조건을 넘어서게 되면 테스트를 종료하게 된다.
export const options = {
thresholds: {
'http_req_duration': ['avg<100', 'p(95)<200'],
'http_req_connecting{cdnAsset:true}': ['p(95)<100'],
},
};
기본값은 false이며, true인경우 커넥션을 재사용하지 않으므로 keepalive를 유지하게 된다.
export const options = {
noConnectionReuse: true,
};
http 요청을 보낼때 사용자 agent 정보를 지정한다.
이렇게 지정하고 테스트를 수행하면 워크로드 요청시 agent를 실어서 전송하게 된다.
export const options = {
userAgent: 'MyK6UserAgentString/1.0',
};
이제 k6 옵션을 활용하여 각기 다른 테스트 기법을 적용해볼 수 있다.
시나리오와 같이 사용하면 토큰도 활용할 수 있고, 조금 더 자세한 유스케이스 테스트를 해볼 수 있으니
k6 를 활용하고자한다면 추가적으로 참고해보면 좋을 거 같다.
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{
duration: '10s',
target: 100
},
{
duration: '30s',
target: 100
},
{
duration: '10s',
target: 0
}
]
}
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{
duration: '10s',
target: 200
},
{
duration: '30s',
target: 200
},
{
duration: '10s',
target: 0
}
]
}
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{
duration: '1m',
target: 10000
},
{
duration: '1m',
target: 0
}
]
}
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{
duration: '2h',
target: 100000
}
]
}
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{
duration: '5m',
target: 1000
},
{
duration: '24h',
target: 1000
},
{
duration: '5m',
target: 0
}
]
}
export default function () {
http.get('http://192.168.68.108:3000');
sleep(1);
}
https://eltonminetto.dev/en/post/2024-01-05-load-test-types/
https://dev.to/eminetto/load-testing-using-k6-57ph
https://grafana.com/docs/k6/latest/examples/
https://medium.com/@ravipatel.it/step-by-step-guide-to-load-testing-with-k6-5afb625e231a
https://devocean.sk.com/blog/techBoardDetail.do?ID=164303
https://devocean.sk.com/blog/techBoardDetail.do?ID=164310
https://grafana.com/docs/k6/latest/using-k6/k6-options/how-to/
https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/