2주만에 TIL 을 쓰네요... 어떻게 쓰는지 다 까먹은 것만 같음
처음 개인 프로젝트를 했을 때는 그냥 내가 접속해서 콘솔로 잘 돌아가는지만 확인하면 됐고 점점 화면이 출력되어야 했는데 이젠 사용자가 몰렸을 때 어떻게 대처해야할지에 대한 부분까지 추가로 생각을 하고 코드를 짜야한다구 함 막막하네요
오랜만에 징징대보기
1. K6 사용 이유
단건 요청은 빠른데 동시 접속이 올라가니까 DB 커넥션 풀이 바닥나고, 65명 넘어가면 실패율이 급증하는 포화지점도 생김
근데 내가 65개의 요청을 동시에 보낼 수 없는 상황이니 테스트 전용 툴을 사용합니다
K6 를 사용해야하는 이유를 정리하면
1. 포화지점 찾을 수 있음
2. 효과를 수치로 비교 - test code 를 돌려서 알 수 있는 부분 이상을 알 수 있음
인덱스를 걸고 포화테스트를 시키니까 조금 더 버틴다거나, 쿼리 최적화를 하고 난 뒤에 하면 포화지점이 올라가는 등의 차이
3. 배포 전에 알 수 있음.. 아직 내가 서비스를 내본 게 아니라 이 부분의 장점은 잘 와닿지 않지만 ㅜ 언젠가 내야한다면 대량 사용자에 대한 설계를 해야하는건 알 것 같음
2. K6 실행 방법
https://drcode-devblog.tistory.com/204
[솔루션] K6 부하테스트 사용법
www.k6.io/ Load testing for engineering teams | k6 k6 is an open-source load testing tool and cloud service providing the best developer experience for API performance testing. k6.io K6란? - 웹 사이트에 부하를 가하여 부하를 얼마나 견디
drcode-devblog.tistory.com
다운로드는 위 블로그를 참고했습니다
다운로드 시킨 다음에 경로 확인해두고
꼭 Powershell 관리자 모드로 실행시키기...

나는 테스트 할 경로를 따로 만들어두고 거기 코드를 넣은다음 powershell 내부에서 cd 로 이동한 후에 실행시켰다
PS C:\Users\user\Documents\project\test> k6 run k6_test.js
PS C:\Users\user\Documents\project\test> k6 run -e VERSION=v3 -e VUS=50 k6_test.js
환경 변수를 넘기고 싶을 때는 앞에 -e 를 붙이고 써줬다 (이번엔 version 이나... vus 를 명시하는데 사용했음)
k6 를 설치 할 때 choco install k6 를 사용하면 자동으로 PATH 에 등록해주기 때문에 앞에 k6 를 바로 써도 되지만
만약에 다른 방법을 사용하게 된다면 그 땐 k6 의 경로를 같이 명시해주면 된다
PS C:\Users\user\Documents\project\test> C:\programfiles\k6\k6.exe run k6_test.js
으 불편해
3. 프롬프트 작성
거의 필수가 되어버린 AI.... 스크립트 작성해달라고 할 때 참고 하기
보통 k6 스크립트가 아래처럼 생겼기 때문에
import http from 'k6/http';
import { check, sleep } from 'k6';
const VERSION = __ENV.VERSION; // -e 로 전달받은 애들: 형변환 해줘야하고 형식은 __ENV.XXX
const VUS = parseInt(__ENV.VUS) || 50 ; // || 연산자를 넣으면 -e 가 없다면 기본값 설정
const JWT = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyxxx';
const HEADERS = {
Authorization: `Bearer ${JWT}`,
'Content-Type': 'application/json',
};
// 1. 옵션 - 몇 명이 얼마나 보낼지
export const options = {
scenarios: {
constant_load: {
executor: 'constant-vus',
vus: VUS, // 이렇게 사용
duration: '30s',
},
},
};
// 2. 기본 함수 - VU 한 명이 반복 실행하는 로직
export default function () {
// API url, method 이름 정확히
const res = http.get('http://localhost:8080/api/v1/events/search',
{ headers: HEADERS} );
check(res, { 'status 200': (r) => r.status === 200 }); // 성공 기준
check(res, . . .}) // response body 에 특정 field 가 있다거나 하는지도 확인
sleep(0.3);
}
//3. (선택) 결과 요약 커스텀
export function handleSummary(data) { ... }
프롬프트를 작성할 때
| 테스트 대상 API의 정확한 URL과 HTTP 메서드 인증 방식 요청 파라미터 변형 VU 수와 시간 성공 기준 |
을 명시해주면 잘 짜준다
const SEARCH_PARAMS = [
{ area: 'SEOUL,GYEONGGI', category: 'MUSICAL', endPrice: '80000' },
{ area: 'SEOUL', category: 'CONCERT', startDate: '2025-06-01T00:00:00'},
{ area: 'BUSAN,DAEGU', category: 'PLAY', endPrice: '50000' },
{ category: 'MUSICAL', startDate: '2025-01-01T00:00:00', endDate: '2027-01-01T00:00:00' },
];
export default function () {
// 매 요청마다 랜덤으로 하나 골라서 사용
const params = SEARCH_PARAMS[Math.floor(Math.random() * SEARCH_PARAMS.length)];
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
const url = `${BASE_URL}/api/events/${VERSION}?${queryString}&page=0&size=10`;
const res = http.get(url, { headers: HEADERS });
}
요청 파라미터 변형은 이런식으로 해주는데 ... ... 캐시만 테스트 할 게 아니라면 무조건 조건을 섞어줘야한다
하나하나 적을 필요는 없고 어차피 짜둔 시나리오가 여러개일테니까 그걸 보내주자

클로드한테 이런식으로 말해주기
4. 시나리오
시나리오는 사용자를 어떤 패턴으로 보낼 건지 정하는 것

이번 과제에서는 constant-vus (고정인원 즉시 투입), ramp-up- vus (점진 투입) 을 모두 테스트했다
추천하는 단계는 먼저 constant-vus로 VU별로 따로 돌려서 포화지점 찾고 전체 흐름 한 번 확인용도 로 ramp -up 쓰기
constant
executor: 'constant-vus',
vus: 50,
duration: '30s',
ramp-up
executor: 'ramping-vus',
stages: [
{ duration: '30s', target: 50 },
{ duration: '30s', target: 100 },
{ duration: '30s', target: 200 },
],
ramp up 은 0명부터 점점 올라가는 방식 : 실제 트래픽이 몰리는 상황이랑 비슷
하지만 어디부터 터졌는지 잡기 힘들었음 -> 이번 트러블 슈팅에서 해결 예정
추가로 시나리오에서 sleep(0.3) 을 넣었는데 실제 사용자는 응답 받고 바로 또 요청을 보내지 않기 때문에 sleep 을 넣어서 실제 상황과 비슷하게 만들어주는 용도 ... 안 넣으면 결과가 좀 과장됨
5. 성능 지표
| http_req_duration (응답 시간) |
avg (평균) | 전체 요청의 평균 응답시간 소수의 느린 요청이 평균을 올릴 수 있으니 주의 |
| med (중간값) | 전체 요청을 빠른 순으로 줄 세웠을 때 딱 가운데 값 대부분의 사용자 체감 속도 라고 생각하면 편 |
|
| p95 | 상위 5% 느린 요청의 기준선 == 100명 중 95명은 이 시간 안에 응답을 받았다 |
|
| p99 | 상위 1% -> 극단적으로 느린 요청 |
| http_reqs (처리량) |
count | 총 요청 수 |
| rate (TPS) | 초당 처리한 요청 수. 이게 높을수록 서버가 많이 소화함 | |
| http_req_failed (실패율) | 요청이 실패한 비율 status 200이 아니거나 응답 자체가 안 온 경우 실패율이 높아지는 지점을 포화지점으로 생각하면 됨 |
|
결과를 볼 때 실패율 -> p95 -> avg vs med -> TPS 순서로 보면 된다
실패율부터 확인 — 0%가 아니면 일단 문제 있는 거
p95 확인 — 대부분의 사용자 체감 속도
avg랑 med 비교 — 차이가 크면 일부 요청이 극단적으로 느린 거
TPS — 처리량이 VU를 올렸는데 오히려 떨어지면 서버가 한계에 온 거
6. 성능테스트할 때 고민하면 좋은 점들
테스트 데이터 규모를 실제와 비슷하게 맞추기
- 5만건과 50만건 사이에서도 결과가 다르게 나옴
캐시 상태를 의식
캐시 없는 최악의 상황"을 측정하고 싶으면 매 테스트 전에 캐시를 비우기
DB 자체의 cache 도 생각하기
일반적인 운영 상황 측정하려면 warm 상태에서 돌리기
같은 조건에서 여러 번 돌리기
테스트 하나에서 반복문을 돌리더라도 ... 여러번 하기 그러고 또 excel 등으로 결과 평균값 내기
같은 스크립트, 같은 VU로 돌려도 결과가 매번 조금씩 달라. OS 스케줄링, GC 타이밍, 네트워크 지터 등이 영향을 줌
그리고 돌릴때 절대..... 다른거 하지말기 (유튜브,,,, 등등)
서버 리소스도 같이 모니터링
CPU가 100% 찍은 건지, DB 커넥션 풀이 바닥난 건지, 메모리가 부족한 건지. tixy에서 HikariCP 커넥션 풀 설정을 튜닝한 것도 서버 로그에서 커넥션 대기 시간을 확인했던것
# dead lock 방지 코드 추가
hikari:
max-lifetime: 30000
connection-timeout: 5000
leak-detection-threshold: 10000
부하 테스트 돌릴 때 서버 쪽 모니터링(CPU, 메모리, DB 커넥션 수 등)을 같이 보는 습관을 들이기
-> Grafana, Prometheus 사용
spring-boot-starter-actuator
micrometer-registry-prometheus
위 의존성을 추가하면 /actuator/prometheus 엔드포인트가 생겨. 거기서 HikariCP 커넥션 풀 상태, JVM 메모리, HTTP 요청 수 같은 걸 Prometheus가 긁어가고, Grafana에서 실시간 그래프로 확인 가능
이번 프로젝트에서도 썼지만 내가.... 모르는 부분이라 공부 더 해야 할 듯 함
sleep 값을 조정
아까 시나리오 설계에서도 말했지만, sleep이 없으면 비현실적으로 빡빡한 부하가 생김
sleep을 너무 길게 주면 부하가 약해서 포화지점을 못 찾을 수 있음
7. 프로젝트에서 K6를 언제 쓰면 좋은지
코드 인덱스 캐시 설정 하나라도 바뀌면 돌리기
진짜
매번 돌리기.....
테스트 데이터 바뀌더라도 새로 돌리기 그리고 메모하기...정리하기 습관들이기
8. 트러블슈팅 (1)

실행시키려고 하니까 애플리케이션 제어 정책에서 이 파일을 차단했습니다 오류가 발생
해결 과정

차단해제 -> 안 됨

cmd 로 올라와서 실행시켜보기 -> 안 됨
choco upgrade k6 -y
choco update 하기 -> 안 됨
결국 튜터님께 가져갔다......
오류 이름: 조직의 Device Guard 정책에 의해 차단되었습니다.
Windows가 백그라운드로 자동으로,,, SmartScreen 정책을 업데이트해주는듯
internet 으로 다운받은 파일에 대한 필터가 작동하는 듯 함 ...
설정 / 개인 정보 및 보안 / Windows 보안 / 앱 및 브라우저 컨트롤

평판 기반 보호가 꺼져있다면 켜고, 켜져있다면 평판 기반 보호 설정으로 들어간다


Microsoft Edge 용 SmartScreen 을 꺼준다

잘 돌아감
9. 트러블 슈팅 (2)
Ramp up 구간별 출력 문제가 있었음 -> 시나리오를 아예 10 vus, 20 vus 이렇게 분리해보려고 했지만 잘 안 됨 ...
그냥 어떤 지점까지 쭉 끌어올렸을 때의 평균 실패율과 응답시간을 출력해준다

이 전 코드를 보면 k6가 시나리오별로 메트릭을 자동 분리해줄 거라고 생각하고
handleSummary에서 아래 키로 찾아서 표에 넣으려고 했었음
// k6가 알아서 만들어줄 거라 기대한 키
data.metrics['http_req_duration{scenario:vu_50}']
하지만 실제로 키가 없어서 - 가 출력
해결: Custom Metrics를 직접 선언해서 요청할 때마다 수동으로 기록
// 1단계: 시나리오별 메트릭을 직접 만들어둠
durations[50] = new Trend('duration_vu_50');
reqCounts[50] = new Counter('reqs_vu_50');
failRates[50] = new Rate('fails_vu_50');
// 2단계: 요청할 때마다 직접 기록
durations[50].add(res.timings.duration);
reqCounts[50].add(1);
failRates[50].add(!success);
// 3단계: handleSummary에서 내가 만든 키로 꺼냄
data.metrics['duration_vu_50'] // ← 내가 만든 거니까 확실히 있음
// ----------------------
// 이전: 모든 시나리오가 같은 default 함수 실행 → 어떤 구간인지 구분 불가
export default function () { ... }
// 이번: 시나리오마다 다른 함수 실행 → 각 함수가 자기 VU 수를 알고 기록
export function run_vu_50() { runTest(50); }
export function run_vu_100() { runTest(100); }
k6는 시나리오별로 scenario: 태그를 자동으로 붙여주지만, handleSummary에서 이 태그별 메트릭을 꺼내려면 해당 sub-metric에 threshold가 정의돼 있어야 한다.
threshold 를 하나하나 만들어주는건 번거롭기 때문에 아래처럼 Custom Metrics 만들어주기
https://community.grafana.com/t/multiple-scenarios-metrics-per-each/97687/3

이런식으로 scenarios 를 지정해줘야한다 (이건 k6 community 에 있는 내용이고)
// ─── VU 단계 정의 ───────────────────────────────────────
const stages = [10, 30, 50, 65, 80, 100, 150, 200, 250, 300, 350, 400];
const STAGE_DURATION = '30s';
const STAGE_SEC = 30;
const GAP = 5;
// ─── 각 시나리오의 실행 함수 ────────────────────────────
// k6는 exec에 지정된 함수명을 export해야 실행함
export function run_vu_10() { runTest(10); }
export function run_vu_30() { runTest(30); }
// . . .
export function run_vu_300() { runTest(300); }
export function run_vu_350() { runTest(350); }
export function run_vu_400() { runTest(400); }
// ─── 시나리오별 Custom Metrics 생성 ─────────────────────
// k6가 시나리오별 자동 분리를 안 해주므로, 직접 Trend/Counter/Rate를 만들어서 기록
const durations = {};
const reqCounts = {};
const failRates = {};
stages.forEach((vus) => {
durations[vus] = new Trend(`duration_vu_${vus}`);
reqCounts[vus] = new Counter(`reqs_vu_${vus}`);
failRates[vus] = new Rate(`fails_vu_${vus}`);
});
// ─── 시나리오 정의 ──────────────────────────────────────
const scenarios = {};
stages.forEach((vus, i) => {
scenarios[`vu_${vus}`] = {
executor: 'constant-vus',
vus: vus,
duration: STAGE_DURATION,
startTime: `${i * (STAGE_SEC + GAP)}s`,
exec: `run_vu_${vus}`,
};
});
내 코드에서는 반복문을 돌려서 개별 시나리오를 생성한다
결과

잘 나온다
테스트를. v3로 해버려서 redis cache 를 타서 실패율이 다 0이 뜨긴 했지만
지금 확인해보려고 한건 VUs ramp up 이 잘 되나 확인이었으니까! 넘어가기
끝
'SPARTA 과제 > SPRING' 카테고리의 다른 글
| Distributed Lock 트러블슈팅 (0) | 2026.05.07 |
|---|---|
| RedisTemplate StackOverFlow Error (1) | 2026.05.03 |
| JOOQ 코드 생성 트러블슈팅 (1) | 2026.04.13 |
| Plus spring 도전과제 (1) | 2026.04.06 |
| Plus spring 필수 과제 (0) | 2026.04.03 |