Bldev's Blog

[소프트웨어] 성능 테스트

2024. 4. 2.

성능 테스트

성능 테스트(performance test)란 시스템(또는 소프트웨어)의 성능이 요구사항을 충족하는지 확인하기 위해 시스템의 속도, 안정성, 확장성 등 다양한 지표를 측정하고 평가하는 테스트이다. 성능 테스트의 목적은 시스템의 최대 성능 및 처리량을 파악하기 위한 것만은 아니며 다양한 시나리오에 따른 시스템의 처리 능력과 장애 복구 능력 등 다양한 지표를 파악하는데 있다. 성능 테스트는 다음 하위 테스트로 구성된다.

  • 부하 테스트 (load test): 부하 테스트는 임계값(threshold) 한계에 도달할 때까지 부하를 지속적으로 꾸준히 증가시키는 테스트이다. 특정 한계 지점에 도달했을 때 성능 저하나 응답 시간 지연이 발생하는지 확인한다. 장기간 지속적으로 부하를 가하여 테스트하는 부하 테스트를 내구성 테스트(indurance test)라고 한다.
  • 스트레스 테스트 (stress test): 스트레스 테스트는 평균값 또는 임계값 이상의 많은 부하를 가하여 시스템이 어떻게 동작하는지 확인하는 테스트이다. 부하 테스트는 임계값까지 부하를 가하는 반면 스트레스 테스트는 임계값 이상의 부하를 가한다. 사용자 수가 갑자기 증가하는 상황에서 어떻게 작동하는지 확인하는 스트레스 테스트를 스파이크 테스트(spike test)라고 한다.

성능 테스트 관련 용어

  • 램프 업 (ramp-up): 테스트를 시작한 후 점진적으로 부하를 증가시키는 것을 말한다. 램프 업 구간에서는 시스템의 초기 성능과 부하 증가에 따른 상태를 모니터링한다.
  • 플래토 (plateau): 부하를 일정 수준으로 유지하는 것을 말한다. 플래토 구간에서는 일정 수준의 부하(낮은 수준의 부하 또는 높은 수준의 부하)가 유지되는 동안 성능과 상태를 모니터링 한다.
  • 램프 다운 (ramp-down): 테스트를 종료하기 전까지 점진적으로 부하를 감소시키는 것을 말한다. 램프 다운 구간에서는 시스템의 부하 감소에 따른 상태를 모니터링한다.
  • 가상 유저 (VU, virtual user): 실제 사용자를 시뮬레이션하는 가상 사용자를 말한다. 기본적으로 가상 유저 한 명이 하나의 요청을 수행한다고 가정한다. 가상 유저 수를 높게 설정하는 것은 높은 부하를 발생시키는 것이다. 테스트 도구는 가상 유저 수 만큼 요청을 병렬로 수행하도록 하여 테스트를 시뮬레이션 한다.
  • 처리량 (throughput): 시스템이 일정 시간 동안 처리할 수 있는 작업의 수나 데이터의 양을 나타내는 성능 지표이다. 처리량의 단위는 보통 시간 당 요청을 처리할 수 있는 최대 작업 수(request per second, req/s) 또는 시간 당 트랜잭션 수(transaction per second, tps)를 말한다. 성능 테스트 시 높은 가상 유저의 수를 설정하여 테스트를 수행한다는 것은 시스템이 높은 처리량을 얼마나 유지할 수 있는지 확인하는 것을 의미한다.
  • 시나리오 (scenario): 테스트 시나리오란 테스트가 수행될 환경과 조건을 설정하여 테스트를 어떻게 수행할 것인지 미리 시뮬레이션하는 계획을 의미한다. 가상 유저의 수 설정, 부하 스케줄링(부하 횟수 및 부하 지속 기간, 부하 간 시간 간격 등) 설정 등을 통해 시나리오를 구성한다. 보다 현실적인 실제 사용자의 행동을 시뮬레이션하기 위해 요청 조건 및 반복 수 설정, 세션 설정 등의 과정이 포함될 수 있다.

테스트 시나리오 예

  1. 부하 테스트
    • 최대 사용자: 1000명
    • 램프 업: 1000명의 사용자(스레드)에 도달할 때까지 30초마다 100명의 사용자 추가
    • 플래토: 1000명의 사용자에 도달하면 부하를 300초 동안 유지
    • 램프 다운: 3초마다 10명의 사용자를 중지
  2. 스트레스 테스트
    • 최대 사용자: 10000명
    • 램프 업: 테스트 시작 후 5초를 기다린 후 곧바로 7000명의 사용자를 추가하고 10000명의 사용자에 도달할 때까지 30초마다 500명의 사용자 추가
    • 플래토: 10000명의 사용자에 도달하면 부하를 300초 동안 유지
    • 램프 다운: 1초마다 10명의 사용자를 중지

성능 테스트 도구와 시나리오 설정

JMeter

JMeter는 아파치(Apache) 소프트웨어 재단에서 개발한 순수 자바 기반의 오픈소스 성능 테스트 도구이다. GUI 환경에서 시나리오 작성이 가능하고 웹 뿐만 아니라 데이터베이스, 메시지 큐 등 다양한 프로토콜을 지원, 클라우드 플랫폼의 분산 테스트 규격화 등 성능 테스트 관련 방대한 생태계를 제공하고 있다.

JMeter는 가상 사용자 1명당 OS 스레드 1개를 할당하는 스레드 당 사용자 모델(동기 논블로킹 모델)을 기반으로 설계되어 동시 접속자 수가 수천~수만 명으로 늘어나면 리소스 오버헤드(스레드 생성 비용, 스택 메모리, CPU 컨텍스트 스위칭 오버헤드)가 급증하여 테스트 도구 자체가 병목이 될 수 있다는 단점이 있다.

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Stepped Load Test Plan" enabled="true">
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
    </TestPlan>
    <hashTree>
      <!-- Stepped Load Thread Group -->
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Stepped Load Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <!-- 1000명 사용자 설정 -->
        <stringProp name="ThreadGroup.num_threads">1000</stringProp>
        <!-- 300초 동안 램프업 (30초마다 100명 유입) -->
        <stringProp name="ThreadGroup.ramp_time">300</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <!-- 총 유지 시간 900초 (300초 램프업 + 300초 유지 + 300초 램프다운) -->
        <stringProp name="ThreadGroup.duration">900</stringProp>
        <stringProp name="ThreadGroup.delay">0</stringProp>
      </ThreadGroup>
      <hashTree>
        <!-- HTTP Sampler (요청 대상) -->
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
          <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
          <stringProp name="HTTPSampler.port">8080</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.path">/api/health</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
        </HTTPSamplerProxy>
        <hashTree>
          <!-- 200 OK 검증 Assertion -->
          <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
            <collectionProp name="Asserion.test_strings">
              <stringProp name="49586">200</stringProp>
            </collectionProp>
            <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
            <intProp name="Assertion.test_type">8</intProp>
          </ResponseAssertion>
          <hashTree/>
          <!-- 1초 Think Time 타이머 -->
          <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true">
            <stringProp name="ConstantTimer.delay">1000</stringProp>
          </ConstantTimer>
          <hashTree/>
        </hashTree>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
$ jmeter -n -t stepped_load_test.jmx -l results.jtl

Creating summariser <summary>
Created the tree successfully using stepped_load_test.jmx
Starting standalone test @ 2026 Aug 25 19:38:59 KST
summary +    150 in 00:00:05 =   30.0/s Avg:    18 Min:    10 Max:    35 Err:     0 (0.00%)
Tidying up ...    @ 2026 Aug 25 19:39:04 KST
... end of run

게틀링

게틀링(Gatling)은 고성능 웹 서비스의 부하, 스트레스 등을 테스트하기 위한 비동기 성능 테스트 프레임워크이다. GUI 환경 지원 대신 코드로 시나리오를 작성하는 것을 기본 기능으로 제공하고 있다. 코드 기반 시나리오 작성을 통해 시나리오의 형상 관리, CI/CD 파이프라인 연속적 통합 기능을 제공하여 인프라 환경에서 보다 유연하게 테스트할 수 있는 장점을 제공한다.

게틀링의 큰 특징은 비동기 논블로킹 모델을 기반으로 설계되어 적은 수의 작업 스레드로 수천~수만 개의 동시 접속자를 처리하는 높은 부하를 테스트할 수 있다는 점에 있다. 게틀링은 GUI 기반이 아닌 코드 작성 기반이므로 사용성 측면에서 진입 장벽은 비교적 높다고 볼 수 있다.

package com.example;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

import java.time.Duration;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class SteppedLoadSimulation extends Simulation {

    // 1. 프로토콜 공통 환경 설정
    private final HttpProtocolBuilder httpProtocol = http
        .baseUrl("http://127.0.0.1:8080")
        .acceptHeader("application/json")
        .contentTypeHeader("application/json");

    // 2. 가상 사용자 시나리오 정의
    private final ScenarioBuilder scn = scenario("Stepped Load Scenario")
        .exec(
            http("Health Check Request")
                .get("/api/health")
                .check(status().is(200))
        )
        .pause(Duration.ofSeconds(1));

    // 3. 부하 주입 프로파일 설정
    {
        setUp(
            scn.injectClosed(
                // ==========================================
                // 1. [Ramp-up] 1000명 도달할 때까지 30초마다 100명씩 추가 (10회 = 300초)
                // ==========================================
                incrementConcurrentUsers(100)
                    .times(10)
                    .eachLevelLasting(Duration.ofSeconds(30))
                    .separatedByRampsLasting(Duration.ofSeconds(0)),

                // ==========================================
                // 2. [Plateau] 1000명 도달 후 300초 동안 유지
                // ==========================================
                constantConcurrentUsers(1000)
                    .during(Duration.ofSeconds(300)),

                // ==========================================
                // 3. [Ramp-down] 300초 동안 1000명 -> 0명으로 종료 (3초마다 10명 감속)
                // ==========================================
                rampConcurrentUsers(1000)
                    .to(0)
                    .during(Duration.ofSeconds(300))
            )
        )
        .protocols(httpProtocol)
        .assertions(
            global().responseTime().percentile3().lt(500),
            global().successfulRequests().percent().gt(99.0)
        );
    }
}
$ mvn test-compile gatling:test -Dgatling.simulationClass=com.example.SteppedLoadSimulation

Simulation com.example.SteppedLoadSimulation started...
================================================================================
---- Global Information --------------------------------------------------------
> request count                                      13291 (OK=13291  KO=0     )
> min response time                                      0 (OK=0      KO=-     )
> max response time                                      7 (OK=7      KO=-     )
> response time 95th percentile                          1 (OK=1      KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                         13291 (100%)
> failed                                                 0 (  0%)
================================================================================
Global: percentage of successful events is greater than 99.0 : true (actual : 100.0)
[INFO] BUILD SUCCESS

K6

K6는 그라파나 랩스(Grafana Labs)에서 제공하는 성능 테스트 도구이다. K6는 내장된 자바스크립트 엔진을 사용하며 자바스크립트 언어로 시나리오에 대한 스크립트를 작성할 수 있고 스크립트 작성 시 다양한 자바스크립트 라이브러리를 사용할 수 있다는 장점이 있다. K6는 비동기 기반 고(Go) 언어로 개발되어 높은 성능을 제공한다.

게틀링과 유사하게 가상 사용자 1명당 OS 스레드를 할당하는 대신 고 언어의 코루틴(Goroutine)을 사용하여 비동기 논블로킹 모델 기반으로 적은 수의 작업 스레드로 수천~수만 개의 가상 사용자를 처리한다.

K6는 프로메테우스(Prometheus) 및 그라파나(Grafana) 통합 지원이 뛰어나며, 자바스크립트 문법을 지원하므로 웹 개발자에게 가장 친숙한 도구라고 볼 수 있다. 가장 큰 장점은 노드나 자바 같은 런타임 의존성이 전혀 필요 없는 고 언어 단일 바이너리이므로 설치 즉시 스크립트를 실행할 수 있어 간편하다는 것이다.

쿠버네티스 환경에서 k6 오퍼레이터를 사용하여 클러스터 내부에서 대규모 분산 테스트를 CRD(Custom Resource Definition)로 배포하고 관리할 수 있다는 측면에서 K6는 격리된 환경에서의 테스트 방법으로 좋은 선택이라 할 수 있다.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    stepped_load_profile: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        // ==========================================
        // 1. [Ramp-up] 30초마다 100명씩 추가 (총 10단계 = 300초)
        // ==========================================
        { duration: '30s', target: 100 },
        { duration: '30s', target: 200 },
        { duration: '30s', target: 300 },
        { duration: '30s', target: 400 },
        { duration: '30s', target: 500 },
        { duration: '30s', target: 600 },
        { duration: '30s', target: 700 },
        { duration: '30s', target: 800 },
        { duration: '30s', target: 900 },
        { duration: '30s', target: 1000 },

        // ==========================================
        // 2. [Plateau] 1000명 도달 후 300초 동안 부하 유지
        // ==========================================
        { duration: '300s', target: 1000 },

        // ==========================================
        // 3. [Ramp-down] 3초마다 10명씩 중지 (300초 동안 1000 -> 0)
        // ==========================================
        { duration: '300s', target: 0 },
      ],
      gracefulRampDown: '10s',
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% 응답 시간 500ms 미만
    http_req_failed: ['rate<0.01'],    // 실패율 1% 미만
  },
};

export default function () {
  const res = http.get('http://127.0.0.1:8080/api/health');

  check(res, {
    'status is 200': (r) => r.status === 200,
  });

  sleep(1); // 1초 대기 (Think Time)
}
$ k6 run k6_stepped_test.js

          /\      Grafana   /‾‾/  
     /\  /  \     |\  __   /  /   
    /  \/    \    | |/ /  /   ‾‾\ 
   /          \   |   (  |  (‾)  |
  / __________ \  |_|\_\  \_____/ 

  █ THRESHOLDS 
    http_req_duration: ✓ 'p(95)<500' p(95)=6.89ms
    http_req_failed..: ✓ 'rate<0.01' rate=0.00%

  █ TOTAL RESULTS 
    checks_succeeded...: 100.00% (10 out of 10)
    checks_failed......: 0.00%
    ✓ status is 200

참고