API Performance Testing
- API 성능 테스트(API Performance Testing)는 API가 높은 부하에서도 안정적으로 동작하는지 확인하는 과정이다.
- 주요 목표는 응답 시간(Response Time), 처리량(Throughput), 동시 사용자 수(Concurrency), 그리고 리소스 사용률(CPU, 메모리 등)을 평가하는 것이다.
- 주요 테스트 유형으로는 부하 테스트(Load Testing), 스트레스 테스트(Stress Testing), 스파이크 테스트(Spike Testing), 지속 테스트(Soak Testing) 가 있다.
nGrinder
nGrinder는 네이버에서 제공하는 The Grinder 기반의 오픈소스 부하 테스트 도구이며, 분산 환경에서 대규모 부하 테스트를 쉽게 수행할 수 있도록 기능을 제공한다.
구성요소
- Controller
- 테스트를 관리하고 실행하는 중앙 역할
- 테스트 스크립트 작성 및 실행 제어
- Agent에게 테스트 명령을 전달하고 결과 수집
- Agent
- Controller의 명령을 받아 부하(Load)를 발생시키는 역할
- 여러 Agent를 사용하여 분산 부하 테스트 가능
- 테스트 실행 중 Target Server로 요청을 전송
- Target Server
- 테스트 대상 서버(API, 웹 애플리케이션 등)
- Agent에서 보내는 요청을 처리하고 성능을 평가
- User
- nGrinder를 사용하는 테스트 관리자(테스터)
- Controller에서 테스트를 설정하고 결과를 분석
Architecture
사용자가 요청 → Controller가 Agent와 Target Server 설정 → Agent가 부하 테스트 실행 → 성능 데이터 수집 및 분석 흐름으로 진행된다.
자세한 내용을 nGrinder Wiki 에서 확인 가능하다. (https://github.com/naver/ngrinder/wiki/Architecture)
설치 및 환경 구성 - nGrinder Docker
nGrinder 를 설치 및 운영할 수 있는 방법은 Controller와 Agent 소스코드(https://github.com/naver/ngrinder)를 내려 받아 직접 설치하는 방법과 Docker Image 를 내려받아 container 를 생성하여 운영하는 방법 두 가지가 있다.
본 글에서는 Controller, Agent Docker Image로 Container 를 생성하여 설치 및 운영하는 방법에 대해 기술한다. 아래 docker-compose.yml 파일은 하나의 Controller 와 하나의 Agent 를 정의하였다.
# docker-compose.yml
version: '3.8'
services:
ngrinder_controller:
image: ngrinder/controller
container_name: ngrinder_controller
restart: unless-stopped
ports:
- "80:80"
- "16001:16001"
- "12000-12009:12000-12009"
volumes:
- ~/ngrinder-controller:/opt/ngrinder-controller
ngrinder_agent:
image: ngrinder/agent
container_name: ngrinder_agent_1
restart: unless-stopped
depends_on:
- ngrinder_controller
links:
- "ngrinder_controller:controller"
Controller 와 Agent 에 설정된 옵션의 의미는 아래와 같다.
- controller - ports
- 80 : web service 포트
- 9010 - 9019 : Agent들이 Controller 클러스터로 연결되는 포트
- 16001 : 테스트를 하지 않는 유휴 상태의 Agent가 Controller에게 테스트 가능 메시지를 전달하는 포트
- 12000-12009 : 테스트 실행 및 종료 등 컨트롤러 명령어와 에이전트별 테스트 실행 통계를 초별로 수집하는 포트
- agent - links
- ngrinder_controller:controller : Controller Docker 컨테이너 간의 네트워크 연결
- controller - volumes
- Docker 컨테이너 내부의 데이터를 유지(영속화, Persistent Storage)
$ docker compose up -d 을 통해 Controller 와 Agent 컨테이너를 실행한다.
컨테이너 실행 후 http://{WEB_URL}:{PORT} 으로 접속하고 초기 ID / PW 는 admin / admin 이다.
Agent Management 탭으로 이동해서 Agent 가 정상적으로 동작하고 있음을 확인한다.
Script 작성 - Groovy
테스트에 사용할 스크립트를 작성한다. 스크립트는 Groovy, Jython으로 작성할 수 있다. Groovy는 자바와 유사하고 Jython은 파이썬과 유사하다. (본 글에서는 Groovy 기준으로 설명)
스크립트 유형 선택 후 파일을 생성하면 Sample Code가 작성되어 있다. 테스트할 API, Request Header, Request Body 등을 스크립트에서 설정할 수 있다.
인증 기능이 없는 스크립트는 예제 파일에서 일부분 수정하여 테스트가 가능하지만 인증 또는 인가 기능이 포함되어 있다면 별도로 해당 로직을 구현해야 한다.
테스트에 사용할 라이브러리와 데이터 파일을 각각 /lib, /resources 에 업로드한다. (/lib 에 추가한 파일은 자동 import)
아래 코드는 스크립트를 생성하면 자동으로 작성되어 있는 샘플 코드이다.
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager
/**
* A simple example using the HTTP plugin that shows the retrieval of a single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static Map<String, String> headers = [:]
public static Map<String, Object> params = [:]
public static List<Cookie> cookies = []
@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "Test1")
request = new HTTPRequest()
grinder.logger.info("before process.")
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
grinder.logger.info("before thread.")
}
@Before
public void before() {
request.setHeaders(headers)
CookieManager.addCookies(cookies)
grinder.logger.info("before. init headers and cookies")
}
@Test
public void test() {
HTTPResponse response = request.GET("http://please_modify_this.com", params)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(response.statusCode, is(200))
}
}
}
샘플 코드를 바탕으로 각각의 어노테이션이 의미하는 바는 아래와 같다.
- @RunWith() : JUnit 테스트 실행기를 지정하는 역할
- @BeforeProcess : 프로세스가 생성될때 실행해야 하는 동작 정의
- @AfterProcess : 프로세스가 종료하기 직전에 실행해야 하는 동작 정의
- @BeforeThread : 각 쓰레드가 실행된 전에 실행해야 하는 동작 정의
- @AfterThread : 각 쓰레드가 종료하기 직전에 실행해야 하는 동작 정의
- @Before : 모든 @Test 메소드가 실행되기 전에 실행해야 하는 동작 정의
- @After : 모든 @Test 메소드가 종료된 이후 실행해야 하는 동작 정의
- @Test : 테스트 동작 정의
작성한 스크립트 파일은 아래 절차를 통해 실행된다.
자세한 내용을 nGrinder Wiki 에서 확인 가능하다. (https://github.com/naver/ngrinder/wiki/Groovy-Script-Structure)
테스트 생성 및 실행
테스트에 적용하는 설정 항목은 아래와 같다.
Agent
- 테스트를 실행할 가상 사용자(VUser)를 실행하는 물리적 또는 가상 서버
- nGrinder에서 부하를 발생시키는 역할을 하며, 여러 개의 Agent 사용 가능함.
Vuser per agent
- 각 Agent에서 실행할 가상 사용자(Virtual User, VUser)의 수
- VUser는 실제 사용자를 시뮬레이션하여 요청을 보냄.
- VUser = Agent * (Processes * Threads)
- Processes
- Agent에서 실행할 프로세스 수
- 한 개의 Agent 내에서 여러 개의 프로세스를 실행하여 성능 테스트를 병렬로 수행함.
- Threads
- 각 프로세스에서 실행할 스레드(쓰레드) 수
- 하나의 프로세스 안에서 여러 개의 스레드를 실행하여 부하를 증가시킬 수 있음.
Script
- 성능 테스트를 수행할 테스트 스크립트(Groovy, Jython 등)
- 실제 HTTP 요청을 보내거나, 데이터베이스에 쿼리를 실행하는 등의 테스트 로직을 정의함.
Script Resources
- 테스트에 필요한 추가적인 파일(JAR, CSV, JSON 등)
Target Host
- 부하 테스트를 수행할 대상 서버의 URL 또는 IP 주소
Duration
- 테스트의 실행 시간, 5분으로 설정하면 5분 동안 지속적으로 요청 수행
Run Count
- 특정 횟수만큼 테스트를 실행, Run Count = 1000이면 총 1000번의 요청 수행
Enable Ramp-Up
- VUser가 일정한 간격으로 증가하면서 실행됨.
- Initial Count (초기 사용자 수)
- 테스트 시작 시 처음 실행될 VUser 수를 설정함.
- 예: Initial Count = 10 → 처음에 10명의 VUser가 실행됨.
- Initial Sleep Time (초기 대기 시간 / ms)
- 테스트가 시작된 후, VUser를 실행하기 전에 대기 시간(밀리초 단위)
- 예: Initial Sleep Time = 5000ms → 테스트 시작 후 5초 대기 후 VUser 실행
- Incremental Step (증가 단계)
- 일정한 간격(Interval)마다 몇 명의 VUser를 추가할 것인지 설정함.
- 예: Incremental Step = 5 → 매번 5명의 VUser가 추가됨.
- Interval (증가 간격, 초 단위)
- VUser가 증가하는 시간 간격(초 단위)을 설정함.
- 예: Interval = 10이면 10초마다 Incremental Step 만큼 VUser를 추가함.
Performance Report
Total Vusers
- 실행된 총 가상 사용자(VUser)의 수
- 부하 테스트에서 최대 동시 실행된 사용자 수를 의
TPS(Transactions Per Second, 초당 트랜잭션 수)
- 초당 수행된 요청 수(트랜잭션 수)
- TPS가 높을수록 더 많은 요청을 처리할 수 있다는 의미 (성능이 좋다.)
- 일반적으로 Executed Tests / Run time(초)
Peak TPS (최대 TPS, 최고 초당 트랜잭션 수)
- 테스트 중에서 가장 높은 TPS(초당 요청 수)
- 부하가 가장 많았을 때 서버가 처리할 수 있는 최대 성능
Mean Test Time (평균 응답 시간, ms)
- 각 요청(트랜잭션)이 처리되는 데 걸린 평균 시간(밀리초)
- 클라이언트가 요청을 보낸 후 응답을 받을 때까지의 평균 지연 시간
- 값이 너무 크면 서버의 응답 속도가 느리다는 뜻
Executed Tests(총 실행된 테스트 수)
- 테스트 동안 실행된 총 요청(트랜잭션) 개수
- 모든 VUser가 수행한 테스트의 횟수
Successful Tests(성공한 테스트 수)
- 총 요청(트랜잭션) 중에서 HTTP 200 등의 정상 응답을 받은 경우 성공으로 카운트
Errors(에러 수)
- HTTP 500(서버 오류) 또는 404(찾을 수 없음) 같은 에러 요청(실패한 트랜잭션) 수
Run time (테스트 실행 시간, 초 단위)
- 테스트가 실행된 총 시간(초 단위)
TPS Graph
- X : 시간
- Y : TPS
Results analysis
앞선 테스트를 통해 여러 API의 성능 데이터를 수집했다. TPS(초당 트랜잭션 수)와 MTT(평균 테스트 시간)를 통해 대략적인 성능을 짐작할 수 있지만, 이를 전체 API 대비 비교하여 분석하는 것이 중요하다.
API 성능을 평가하는 주요 지표는 다음과 같다:
- Throughput (TPS, 초당 트랜잭션 수): 시스템이 처리할 수 있는 최대 요청량을 나타냄.
- Latency (MTT, Mean Test Time): 하나의 요청을 처리하는 데 걸리는 평균 시간.
Saturation Point 분석
시스템의 Saturation Point (포화점) 을 파악하기 위해 Enable Ramp-Up 기능을 사용하여 vUser를 점진적으로 증가시키며 테스트를 진행한다. 포화점이란, 요청이 증가하더라도 TPS가 특정 값에 수렴하는 지점을 의미하며, 이를 통해 시스템의 임계점을 판단하고 향후 성능 개선 계획을 수립할 수 있다.
Saturation Point: 요청이 증가해도 TPS가 특정 값으로 수렴하는 지점.
TPS와 MTT를 활용한 성능 평가
TPS가 낮고 MTT가 높은 경우
- 시스템이 한 번에 많은 요청을 처리하지 못함 → 병목(Bottleneck) 존재 가능성
- 응답 시간이 길어지고 성능 저하 우려가 있음
TPS가 높고 MTT가 낮은 경우
- API가 빠르게 많은 요청을 처리 가능 → 현재 시스템 성능 양호
- 추가적인 최적화 없이도 현재 부하를 원활히 감당 가능
데이터 기반 성능 평가 (이상치 탐지)
API의 TPS와 MTT 데이터를 활용하여 다음과 같은 통계 지표를 구하여 성능을 평가한다:
- 중위값 (Median): API 성능의 대표값
- 사분위수 (Q1, Q3): 데이터의 분포를 분석하기 위한 기준
- IQR (Interquartile Range): Q3 - Q1로 계산되며, 데이터의 변동 범위를 나타냄
- Lower Outlier (이상치 기준): X < Q1 - 1.5 × IQR
이러한 지표를 통해 API 성능이 전반적으로 균일한지 평가하고, 특정 API가 비정상적으로 낮은 성능을 보이는지를 분석한다.
Future Challenges
본 글을 통해 nGrinder 를 설치하고 테스트 스크립트를 통해 성능 테스트를 진행하였다. 하지만 현재 학습한 내용으로는 웹에 접속하여 수동으로 테스트를 진행해야 하는 번거로움이 존재한다. 향후 업무 효율성 및 성과를 증대 시키기 위해서는 스크립트 일괄 업로드, 테스트 원격 실행, 테스트 결과 원격지 발송 등 자동화를 구현해야 할 것이다. 자동화를 달성하기 위해 아래의 레퍼런스를 참고한다.
스크립트 파일 대량 업로드
- 최초 스크립트가 세팅된 docker image 를 생성하여 관리한다.
- svn 저장소를 이용하여 관리한다.
테스트 원격 실행
- nGrinder 에서 제공하고 있는 REST API 를 이용하여 구현한다.
- Ref. https://github.com/naver/ngrinder/wiki/REST-API-QuickStart
테스트 결과 원격지 발송
- nGrinder 에서 제공하고 있는 Webhook을 이용하여 구현한다.