Projects/[Final] Shopping Mall Project

가상 스레드 적용 전 테스트 결과 (결제 생성)

montmer27 2026. 5. 7. 23:58

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)