scenario-b
테스트는 load와 pressure로 나누어 2차례에 걸쳐 실시했다.
load test에서는 안정적인 baseline을 확인한다.
stress test에서는 headroom(버티는 한계)를 확인한다.
가상 스레드 적용 후에는 baseline 성능과 headroom 개선 정도를 볼 것이다.
테스트 시나리오 코드
import http from 'k6/http';
import {check} from 'k6';
import {authHeaders, loginUsers} from './common.js';
const BASE_URL = __ENV.BASE_URL || 'http://host.docker.internal:8080';
const MAX_VUS = parseInt(__ENV.MAX_VUS || '500');
const stages = {
smoke: [
{target: 5, duration: '30s'},
],
load: [
{target: 10, duration: '1m'},
{target: 20, duration: '1m'},
{target: 30, duration: '1m'},
{target: 0, duration: '30s'},
],
pressure: [
{ target: 30, duration: '1m' },
{ target: 50, duration: '1m' },
{ target: 70, duration: '1m' },
{ target: 100, duration: '1m' },
{ target: 0, duration: '30s' },
],
stress: [
{target: 15, duration: '1m'},
{target: 60, duration: '1m'},
{target: 150, duration: '1m'},
{target: 300, duration: '1m'},
{target: 600, duration: '1m'},
{target: 0, duration: '30s'},
],
};
export const options = {
setupTimeout: '3m', // setup timeout 방지. 500명 로그인에 필요한 충분한 시간 확보
scenarios: {
purchase_flow: {
executor: 'ramping-arrival-rate', // RPS 고정 방식
startRate: 1,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: MAX_VUS,
stages: stages[__ENV.TEST_TYPE || 'smoke'],
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{phase:scenario,name:cart_add}': ['p(95)<800'],
'http_req_duration{phase:scenario,name:order_create}': ['p(95)<1000'],
'http_req_duration{phase:scenario,name:payment_create}': ['p(95)<1500'],
'http_req_failed{phase:scenario}': ['rate<0.01'],
},
};
export function setup() {
// 1. 상품 ID 수집 (인증 불필요)
const productRes = http.get(`${BASE_URL}/products?page=0&size=20`,
{
tags: {
phase: 'setup',
name: 'product_list'
},
}
);
check(productRes, {
'product fetch success': (r) => r.status === 200,
});
if (productRes.status !== 200) {
throw new Error(`Product fetch failed: status=${productRes.status}`);
}
const products = productRes.json('data.content');
if (!Array.isArray(products) || products.length === 0) {
throw new Error('No products found. Seed products before running test.');
}
const productIds = products.map(p => p.id);
// 2. 로그인 후 배송지 ID 수집
const {tokens} = loginUsers(MAX_VUS);
const users = tokens.map(token => {
const addrRes = http.get(`${BASE_URL}/addresses`, {
...authHeaders(token),
tags: {
phase: 'setup',
name: 'address_fetch'
}
}
);
check(addrRes, {
'address fetch success': (r) => r.status === 200,
});
if (addrRes.status !== 200) {
throw new Error(`Address fetch failed: status=${addrRes.status}`);
}
const addresses = addrRes.json('data');
if (!addresses || addresses.length === 0) {
throw new Error('Address missing. Seed addresses first');
}
const addressId = addresses[0].addressId;
if (!addressId) {
throw new Error('Address missing for token. Seed addresses first.');
}
return {token, addressId};
});
return {users, productIds};
}
// GET에는 authHeaders(token) 그대로 사용,
// POST에는 Content-Type을 추가로 병합
function jsonAuth(token) {
return {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
};
}
export default function (data) {
const user = data.users[(__VU - 1) % data.users.length];
const productId = data.productIds[Math.floor(Math.random() * data.productIds.length)];
// 1. 장바구니에 상품 추가
// sort=createdAt,desc&size=1 → 응답에서 방금 추가한 항목의 cartItemId를 바로 꺼냄
const cartRes = http.post(
`${BASE_URL}/carts/items?sort=createdAt,desc&size=1`,
JSON.stringify({productId, quantity: 1}),
{
...jsonAuth(user.token),
tags: {
phase: 'scenario',
name: 'cart_add'
},
}
);
check(cartRes, {'cart item added 201': (r) => r.status === 201});
if (cartRes.status !== 201) {
const bodyPreview = (cartRes.body || '').slice(0, 300);
console.error(`CART FAILED: status = ${cartRes.status}, bodyPreview = ${bodyPreview}`);
return;
}
const body = cartRes.json();
const cartItemId = body.data.items.content[0].id;
// 2. 주문 생성
const orderRes = http.post(
`${BASE_URL}/orders`,
JSON.stringify({cartItemIds: [cartItemId], addressId: user.addressId}),
{
...jsonAuth(user.token),
tags: {
phase: 'scenario',
name: 'order_create'
},
}
);
check(orderRes, {'order created 201': (r) => r.status === 201});
if (orderRes.status !== 201) {
const bodyPreview = (orderRes.body || '').slice(0, 300);
console.error(`ORDER FAILED: status = ${orderRes.status}, bodyPreview = ${bodyPreview}`)
return;
}
const orderId = orderRes.json('data.orderId');
// 3. 결제
const paymentPayload = JSON.stringify({
orderId: orderId,
method: 'MOCK'
});
const paymentRes = http.post(
`${BASE_URL}/payments`,
paymentPayload,
{
...jsonAuth(user.token),
tags: {
phase: 'scenario',
name: 'payment_create'
},
}
);
check(paymentRes, {'payment processed 201': (r) => r.status === 201});
if (paymentRes.status !== 201) {
const bodyPreview = (paymentRes.body || '').slice(0, 300);
console.error(`PAYMENT FAILED: status = ${paymentRes.status}, bodyPreview = ${bodyPreview}`);
}
}
TEST-1
- test type: load
- 목적: baseline 성능 측정
- test detail
load: [
{target: 10, duration: '1m'}, // 워밍업: 10 RPS까지 증가
{target: 20, duration: '1m'},
{target: 30, duration: '1m'},
{target: 0, duration: '30s'}, // 쿨다운
],
test result(console)
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: /k6/scenario-b.js
output: InfluxDBv1 (http://k6-influxdb:8086)
scenarios: (100.00%) 1 scenario, 500 max VUs, 4m0s max duration (incl. graceful stop):
* purchase_flow: Up to 30.00 iterations/s for 3m30s over 4 stages (maxVUs: 50-500, gracefulStop: 30s)
█ THRESHOLDS
http_req_duration{phase:scenario,name:cart_add}
✓ 'p(95)<800' p(95)=67.58ms
http_req_duration{phase:scenario,name:order_create}
✓ 'p(95)<1000' p(95)=15.38ms
http_req_duration{phase:scenario,name:payment_create}
✓ 'p(95)<1500' p(95)=37.46ms
http_req_failed
✓ 'rate<0.01' rate=0.00%
{phase:scenario}
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......: 10541 36.718645/s
checks_succeeded...: 100.00% 10541 out of 10541
checks_failed......: 0.00% 0 out of 10541
✓ product fetch success
✓ login success
✓ address fetch success
✓ cart item added 201
✓ order created 201
✓ payment processed 201
HTTP
http_req_duration..........................: avg=32.54ms min=6.03ms med=23.57ms max=305.26ms p(90)=55.61ms p(95)=134.92ms
{ expected_response:true }...............: avg=32.54ms min=6.03ms med=23.57ms max=305.26ms p(90)=55.61ms p(95)=134.92ms
{ phase:scenario,name:cart_add }.........: avg=46.24ms min=31.81ms med=40.65ms max=193.57ms p(90)=57.26ms p(95)=67.58ms
{ phase:scenario,name:order_create }.....: avg=10.84ms min=6.03ms med=9.64ms max=142.58ms p(90)=12.48ms p(95)=15.38ms
{ phase:scenario,name:payment_create }...: avg=26.65ms min=18.25ms med=22.49ms max=192.29ms p(90)=32.66ms p(95)=37.46ms
http_req_failed............................: 0.00% 0 out of 10541
{ phase:scenario }.......................: 0.00% 0 out of 9540
http_reqs..................................: 10541 36.718645/s
EXECUTION
iteration_duration.........................: avg=84.35ms min=57.45ms med=73.27ms max=434.46ms p(90)=100.97ms p(95)=121.8ms
iterations.................................: 3180 11.07725/s
vus........................................: 1 min=0 max=9
vus_max....................................: 50 min=50 max=50
NETWORK
data_received..............................: 6.7 MB 24 kB/s
data_sent..................................: 3.6 MB 13 kB/s
test result(graph)




TEST-2
- test type: pressure
- 목적: 실패 지점 관찰
- test detail
pressure: [
{ target: 30, duration: '1m' },
{ target: 50, duration: '1m' },
{ target: 70, duration: '1m' },
{ target: 100, duration: '1m' },
{ target: 0, duration: '30s' },
]
test result(console)
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: /k6/scenario-b.js
output: InfluxDBv1 (http://k6-influxdb:8086)
scenarios: (100.00%) 1 scenario, 500 max VUs, 5m0s max duration (incl. graceful stop):
* purchase_flow: Up to 100.00 iterations/s for 4m30s over 5 stages (maxVUs: 50-500, gracefulStop: 30s)
WARN[0220] Insufficient VUs, reached 500 active VUs and cannot initialize more executor=ramping-arrival-rate scenario=purchase_flow
█ THRESHOLDS
http_req_duration{phase:scenario,name:cart_add}
✗ 'p(95)<800' p(95)=5.06s
http_req_duration{phase:scenario,name:order_create}
✗ 'p(95)<1000' p(95)=4.99s
http_req_duration{phase:scenario,name:payment_create}
✗ 'p(95)<1500' p(95)=5.03s
http_req_failed
✓ 'rate<0.01' rate=0.00%
{phase:scenario}
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......: 26198 74.710287/s
checks_succeeded...: 100.00% 26198 out of 26198
checks_failed......: 0.00% 0 out of 26198
✓ product fetch success
✓ login success
✓ address fetch success
✓ cart item added 201
✓ order created 201
✓ payment processed 201
HTTP
http_req_duration..........................: avg=2.89s min=5.83ms med=4.31s max=7.02s p(90)=4.93s p(95)=5.03s
{ expected_response:true }...............: avg=2.89s min=5.83ms med=4.31s max=7.02s p(90)=4.93s p(95)=5.03s
{ phase:scenario,name:cart_add }.........: avg=3.02s min=31.65ms med=4.39s max=7.02s p(90)=4.97s p(95)=5.06s
{ phase:scenario,name:order_create }.....: avg=2.98s min=5.83ms med=4.34s max=6.84s p(90)=4.9s p(95)=4.99s
{ phase:scenario,name:payment_create }...: avg=3.01s min=18.13ms med=4.37s max=6.84s p(90)=4.94s p(95)=5.03s
http_req_failed............................: 0.00% 0 out of 26198
{ phase:scenario }.......................: 0.00% 0 out of 25197
http_reqs..................................: 26198 74.710287/s
EXECUTION
dropped_iterations.........................: 5130 14.629505/s
iteration_duration.........................: avg=9.01s min=57.34ms med=13.16s max=17.56s p(90)=14.25s p(95)=14.43s
iterations.................................: 8399 23.951893/s
vus........................................: 66 min=0 max=500
vus_max....................................: 500 min=50 max=500
NETWORK
data_received..............................: 17 MB 48 kB/s
data_sent..................................: 9.2 MB 26 kB/s
running (5m50.7s), 000/500 VUs, 8399 complete and 0 interrupted iterations
purchase_flow ✓ [======================================] 000/500 VUs 4m30s 001.29 iters/s
ERRO[0350] thresholds on metrics 'http_req_duration{phase:scenario,name:cart_add}, http_req_duration{phase:scenario,name:order_create}, http_req_duration{phase:scenario,name:payment_create}' have been crossed
test result(graph)




'Projects > [Final] Shopping Mall Project' 카테고리의 다른 글
| 가상 스레드를 적용했는데 왜 성능이 그대로인가 (0) | 2026.05.08 |
|---|---|
| 가상 스레드 적용 후 테스트 결과 (결제 생성) (0) | 2026.05.08 |
| [트러블슈팅?] .setAllowedOrigins("")에 특정 도메인만 허용하기 (0) | 2026.05.07 |
| [트러블슈팅] BeanDefinitionOverrideException 해결하기 (0) | 2026.05.07 |
| Request Body 재사용 불가로 Internal API 테스트 400 오류 (0) | 2026.05.05 |