0Docker Compose 환경 설정
nGrinder는 Controller(웹 UI + 결과 수집)와 Agent(실제 부하 발생)로 구성됩니다. Docker Compose로 즉시 환경을 구축할 수 있습니다.
docker-compose.yml — Controller + Agent 3대 구성
Controller 1대와 Agent 3대로 구성된 분산 테스트 환경.
docker-compose up -d 하나로 완성.yamlversion: '3.8'
services:
ngrinder-controller:
image: ngrinder/controller:3.5.9
container_name: ngrinder-controller
ports:
- "8080:8080" # 웹 UI
- "9010:9010" # Agent 연결 포트
- "9011:9011"
- "9012:9012"
volumes:
- ngrinder-data:/opt/ngrinder-controller
networks:
- ngrinder-net
ngrinder-agent1:
image: ngrinder/agent:3.5.9
container_name: ngrinder-agent1
environment:
- CONTROLLER_ADDR=ngrinder-controller:9010
depends_on:
- ngrinder-controller
networks:
- ngrinder-net
ngrinder-agent2:
image: ngrinder/agent:3.5.9
container_name: ngrinder-agent2
environment:
- CONTROLLER_ADDR=ngrinder-controller:9010
depends_on:
- ngrinder-controller
networks:
- ngrinder-net
ngrinder-agent3:
image: ngrinder/agent:3.5.9
container_name: ngrinder-agent3
environment:
- CONTROLLER_ADDR=ngrinder-controller:9010
depends_on:
- ngrinder-controller
networks:
- ngrinder-net
volumes:
ngrinder-data:
networks:
ngrinder-net:
driver: bridge
1
시작 —
docker-compose up -d2
웹 UI 접속 —
http://localhost:8080 (기본 계정: admin / admin)3
Agent 확인 — 상단 메뉴 Agent Management에서 3개 Agent가 Approved 상태인지 확인
4
스크립트 업로드 — Script 메뉴에서 아래 예제 코드를 새 스크립트로 등록
1Groovy 기본 GET 스크립트
nGrinder의 기본 스크립트 구조입니다. @RunWith와 @Test를 사용한 JUnit 스타일로, 각 가상 유저가 test()를 반복 실행합니다.
BasicGetTest.groovy — HTTP GET 기본 테스트
nGrinder 스크립트의 최소 구조. GET 요청 후 응답 코드와 본문을 검증하는 기본 패턴.
groovyimport static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import net.grinder.plugin.http.HTTPPluginControl
import net.grinder.plugin.http.HTTPRequest
import net.grinder.script.GTest
import org.junit.Before
import org.junit.Test
// 테스트 통계 수집 대상 등록
def test1 = new GTest(1, "GET /public/crocodiles")
// HTTP Request 객체 (스레드 공유)
def request = new HTTPRequest()
test1.record(request)
class BasicGetTest {
def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
@Before
void setUp() {
grinder.statistics.delayReports = true
}
@Test
void test() {
def response = request.GET("https://test-api.k6.io/public/crocodiles/")
// 응답 코드 검증
assertThat(response.statusCode, is(200))
// 응답 본문 검증
assertNotNull(response.text)
assertTrue(response.text.contains("Bert"))
// TPS 카운터 수동 증가
grinder.statistics.forLastTest.success = 1
}
}
스크립트 등록: nGrinder 웹 UI → Script 메뉴 → Create Script → Groovy 선택 후 위 코드를 붙여넣고 Validate Script로 문법 오류를 먼저 확인하세요.
2POST 인증 + 세션 유지 시나리오
로그인 → 세션 쿠키 저장 → 인증 API 호출의 실무 패턴입니다. nGrinder는 Cookie를 자동으로 유지하므로 로그인 후 별도 설정 없이 세션 기반 인증을 사용할 수 있습니다.
AuthFlowTest.groovy — 로그인 → API 호출 시나리오
로그인 POST로 토큰을 발급받아 Header에 설정하고, 이후 여러 API를 순서대로 호출하는 사용자 여정 시뮬레이션.
groovyimport static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import net.grinder.plugin.http.HTTPPluginControl
import net.grinder.plugin.http.HTTPRequest
import net.grinder.script.GTest
import HTTPClient.NVPair
import org.junit.Before
import org.junit.Test
def loginTest = new GTest(1, "POST /auth/token/login")
def profileTest = new GTest(2, "GET /my/crocodiles")
def request = new HTTPRequest()
loginTest.record(request)
// 스레드별 토큰 저장
def threadToken = new ThreadLocal()
class AuthFlowTest {
@Before
void setUp() {
grinder.statistics.delayReports = true
}
@Test
void test() {
// Step 1: 로그인 → 토큰 발급
def loginBody = '{"username":"test_case","password":"1234"}'.bytes
def loginHeaders = [new NVPair("Content-Type", "application/json")] as NVPair[]
def loginRes = request.POST(
"https://test-api.k6.io/auth/token/login/",
loginBody,
loginHeaders
)
assertEquals(200, loginRes.statusCode)
// 응답에서 access token 파싱
def jsonText = loginRes.text
def matcher = jsonText =~ /"access"\s*:\s*"([^"]+)"/
assertTrue("access token이 없습니다", matcher.find())
def token = matcher.group(1)
// Step 2: 인증된 API 호출
def authHeaders = [
new NVPair("Authorization", "Bearer ${token}"),
new NVPair("Content-Type", "application/json")
] as NVPair[]
def profileRes = request.GET(
"https://test-api.k6.io/my/crocodiles/",
[] as NVPair[],
authHeaders
)
assertEquals(200, profileRes.statusCode)
// Step 3: 새 리소스 생성
def createBody = '{"name":"grinder_test","sex":"M","date_of_birth":"2020-01-01"}'.bytes
def createRes = request.POST(
"https://test-api.k6.io/my/crocodiles/",
createBody,
authHeaders
)
assertEquals(201, createRes.statusCode)
grinder.statistics.forLastTest.success = 1
}
}
3beforeTest / afterTest — 준비·정리 로직
테스트 시작 전 데이터 준비, 종료 후 정리 작업을 @BeforeProcess / @AfterProcess 어노테이션으로 처리합니다.
LifecycleTest.groovy — 프로세스·스레드 라이프사이클
Process 레벨(전체 1회)과 Thread 레벨(스레드별 1회) 초기화/정리 로직을 분리하는 nGrinder 표준 패턴.
groovyimport static net.grinder.script.Grinder.grinder
import net.grinder.plugin.http.HTTPRequest
import net.grinder.script.GTest
import org.junit.BeforeClass
import org.junit.AfterClass
import org.junit.Before
import org.junit.After
import org.junit.Test
def test1 = new GTest(1, "GET /api/data")
def request = new HTTPRequest()
test1.record(request)
// 공유 데이터 (Process 레벨)
def sharedData = []
class LifecycleTest {
// ── Process 레벨 (Agent 프로세스 당 1회) ─────────────────
@BeforeClass
static void beforeProcess() {
// 테스트 데이터 파일 로드, DB 커넥션 풀 초기화 등
grinder.logger.info("=== Process 시작: 공유 리소스 초기화 ===")
def file = new File("test_data.txt")
if (file.exists()) {
sharedData.addAll(file.readLines())
}
}
@AfterClass
static void afterProcess() {
grinder.logger.info("=== Process 종료: 공유 리소스 정리 ===")
// DB 커넥션 종료, 임시 파일 삭제 등
}
// ── Thread 레벨 (VU 스레드 당 1회) ──────────────────────
@Before
void setUp() {
// 스레드별 쿠키 초기화, 로그인 등
grinder.statistics.delayReports = true
grinder.logger.info("Thread ${grinder.threadNumber} 시작")
}
@After
void tearDown() {
// 스레드별 세션 종료, 로그아웃 등
grinder.logger.info("Thread ${grinder.threadNumber} 종료")
}
// ── 반복 실행 구간 ────────────────────────────────────────
@Test
void test() {
// sharedData에서 데이터 가져오기 (스레드 번호로 순환)
def idx = grinder.threadNumber % Math.max(sharedData.size(), 1)
def testParam = sharedData.isEmpty() ? "default" : sharedData[idx]
def response = request.GET(
"https://httpbin.org/get?param=${testParam}"
)
if (response.statusCode == 200) {
grinder.statistics.forLastTest.success = 1
} else {
grinder.logger.warn("Unexpected status: ${response.statusCode}")
}
}
}
4TPS 기반 성능 분석 — 목표 TPS 설정
nGrinder는 VU(가상 유저 수)와 Think Time을 조합해 목표 TPS를 달성합니다. TPS = VU ÷ (응답시간 + Think Time) 공식으로 적정 VU를 계산합니다.
TpsTargetTest.groovy — 목표 TPS 100 달성 설정
목표 TPS 100을 위해 응답시간·Think Time으로 필요한 VU를 계산하고, 커스텀 TPS 메트릭을 기록하는 예제.
groovyimport static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.script.GTest
import org.junit.Before
import org.junit.Test
/*
목표 TPS 계산:
- 목표 TPS = 100
- 예상 평균 응답시간 = 200ms
- Think Time = 800ms
- 필요 VU = TPS × (응답시간 + Think Time) / 1000
= 100 × (200 + 800) / 1000 = 100 VU
nGrinder 웹 UI 설정:
- Vusers per Agent: 34 (에이전트 3대 × 34 = 102 VU)
- Duration: 10분
- Think Time: 800ms (Throttle을 사용하지 않는 경우)
*/
def test = new GTest(1, "API Load Test")
def request = new HTTPRequest()
test.record(request)
// 응답시간 측정용 변수
def responseTimes = []
class TpsTargetTest {
@Before
void setUp() {
grinder.statistics.delayReports = true
}
@Test
void test() {
def start = System.currentTimeMillis()
def response = request.GET("https://httpbin.org/get")
def elapsed = System.currentTimeMillis() - start
// 응답 검증
assertEquals(200, response.statusCode)
// 응답시간 로깅 (일정 비율만)
if (grinder.runNumber % 100 == 0) {
grinder.logger.info(
"VU:${grinder.threadNumber} " +
"Iter:${grinder.runNumber} " +
"ResponseTime:${elapsed}ms"
)
}
// TPS 목표 달성 여부 판단
if (elapsed > 500) {
grinder.logger.warn("응답 지연 감지: ${elapsed}ms — TPS 목표 미달 가능성")
}
grinder.statistics.forLastTest.success = 1
// Think Time: 목표 TPS에 맞춘 대기
grinder.sleep(800)
}
}
결과 분석: 테스트 완료 후 nGrinder 웹 UI에서 TPS 그래프, 응답시간 분포, 에러율을 확인하세요. CSV 리포트를 다운로드해 Excel에서 분석하거나 nGrinder 완전 가이드의 Grafana 연동 섹션을 참고하세요.