부하 테스트를 해보자! Artillery 부하 테스트 가이드

이름부터가 포병, 내 서버에 request를 미친듯이 쏴주는 Artillery 부하테스트. 설치부터 실전 시나리오 구현, 트러블슈팅까지의 상세 기록.

Jun Noh

나는 gemini의 Gem으로 나의 선배가 되어줄 시니어 개발자 성격을 만들어서 자주 코드 관련 조언을 얻거나 지금 현실적으로 내가 뭘 해야 하는지 같은 걸 물어본다.

이번에는 그냥 내 서비스의 마무리를 앞두고 그 동안 내가 작업했던 Jira 캘린더를 보니까 정말 9월 말부터 쉬지 않고 달려온게 보여서 뿌듯한 마음에 혹시나 칭찬을 좀 해줄까 싶어서 자랑하려고 질문을 올렸다.

근데 이 김시니어가 하는 말이 내 가슴에 비수를 꽂았다.

운영 전에 부하 테스트는 했어? 동접자 몇 명까지 견딜 수 있는데? 오픈 첫 날 사과문 올리려는 거 아니지?

부하 테스트, 알파 테스트 이후에 하려고 하긴 했는데… 생각해보니 지금 알파 테스트 서버도 얼마나 버틸 수 있을 지 전혀 모르고 있었다.

단위 테스트나 통합 테스트는 로직의 정합성을 검증해주지만, 서버의 Performance을 검증해주지는 않는다.

사용자가 10명일 때는 잘 돌아가던 코드가, 1,000명이 동시에 접속하면 DB 커넥션 풀이 말라버리거나 타임아웃이 발생하며 뻗어버릴 수 있기 때문이다.

오늘은 Artillery라는 강력하고 트렌디한 도구를 사용해서, 내 서버의 한계를 명확히 측정하고 최적화하는 과정을 아주 상세하게 공유하려고 한다.


왜 Artillery인가?

사실 부하테스트를 안해봤다. JMeter를 들어보기만 했지, 현업에서 딱히 써볼 일이 없었다. 그도 그럴 것이 항상 제한된 유저를 위한 시스템을 만들었으니, 애초에 부하테스트의 필요성을 못느끼고 있었다.

그래서 이 쪽에 대한 지식이 전혀 없는 상태로 좀 찾아봤는데, JavaScript/TypeScript 생태계에 익숙한 Node.js 개발자에게는 Artillery가 압도적으로 편리하다는 평이 많았다.

특징은 아래와 같다.

  • Node.js 기반: npm으로 설치하고 실행한다. (사실 여기서 끝이지 뭐.)
  • YAML 설정: 복잡한 코딩 없이 YAML 파일 하나로 시나리오를 정의한다.
  • Scenario-based: 단순히 url을 때리는 게 아니라, “로그인 -> 검색 -> 상세 조회” 같은 사용자 흐름을 테스트한다.

설치 및 기본 설정

설치는 아주 간단하다. 전역으로 설치해도 되고, 프로젝트의 devDependencies로 관리해도 된다.

npm install -g artillery
# 또는
npm install --save-dev artillery

Artillery 스크립트 한 줄 한 줄 뜯어보기 (.yml)

Artillery의 핵심은 YAML 파일이다. 가장 기본이 되는 피드 조회 시나리오와 Upload, 상세보기 등 여러 동작을 합쳐놓은 (mixed-scenario.yml)를 예시로, 각 설정이 무엇을 의미하는지 상세하게 설명한다.

# mixed-scenario.yml

config:
  # 1. 타겟 서버 설정
  target: "https://api.my-service.com"
  
  # 2. 부하 단계(Phases) 설정
  phases:
    # 1단계: Warm-up (준비 운동)
    - duration: 60 #60초 동안
      arrivalRate: 1 # 초당 1명으로 시작해서
      rampTo: 1 # 초당 1명까지 서서히 늘린다.
      name: "Warm up"
    
    # 2단계: Peak Load (최대 부하)
    # 실제 서버가 견딜 수 있는지 확인할 목표치다.
    # 여기서는 초당 2명의 유저 진입(약 100~150명 동접 예상)을 설정했다.
    - duration: 100
      arrivalRate: 2
      rampTo: 2
      name: "Peak load"

  # 3. 플러그인 설정
  plugins:
    expect: {} # 응답 코드 검증을 위한 플러그인

scenarios:
  # 4. 가상 사용자 시나리오 정의
  - name: "User Journey - Active User"
    weight: 30 # 전체 유저 중 30% 확률로 실행
    flow:
      # (1) 리스트 조회
      - get:
          url: "/recipes/feed"
          expect:
            - statusCode: 200
      
      # (2) Think Time (중요!)
      # 사용자는 기계가 아니다. 3초간 고민한다.
      - think: 3 # 유저가 머무르는 시간

      # (3) 상세 조회 및 좋아요
      - get:
          url: "/recipes/123"
      - post:
           url: "/recipes/123/like"

핵심 포인트

  • arrivalRate: 초당 진입하는 사용자 수다. 만약 평균 2분(120초) 머무르는 시나리오에서 arrivalRate: 2라면, 동시 접속자는 240명(2 * 120)에 육박하게 된다. 이 개념을 잘 잡아야 한다.
  • think: 실제 사용자와 유사한 패턴을 만들기 위해 필수적인 대기 시간이다.

파일 구조 및 실행 방법

나는 아래와 같이 파일 구조를 구성하고 package.json에 실행 스크립트를 짜서 구동했다.

디렉터리 구조

dev/artillery-load-test/
├── .env                      # 환경 변수 (BASE_URL, AUTH_TOKEN)
├── package.json              # NPM 설정
├── scenarios/                # 테스트 시나리오들
│   ├── feed.yml
│   ├── recipe-detail.yml
│   ├── recipe-upload.yml
│   └── mixed-scenario.yml
└── scripts/
    ├── get-token.ps1         # 인증 토큰 발급
    └── load-env.js           # .env 파일 파싱 (Artillery Processor)

package.json

{
  "name": "babple-artillery-load-test",
  "version": "1.0.0",
  "description": "Babple Backend 원격 서버 부하 테스트 (Windows)",
  "scripts": {
    "test:feed": "artillery run scenarios/feed.yml --dotenv .env --output results/feed.json",
    "test:detail": "artillery run scenarios/recipe-detail.yml --dotenv .env --output results/detail.json",
    "test:upload": "artillery run scenarios/recipe-upload.yml --dotenv .env --output results/upload.json",
    "test:mixed": "artillery run scenarios/mixed-scenario.yml --dotenv .env --output results/mixed.json",
    "test:all": "npm run test:feed && npm run test:detail && npm run test:mixed",
    "report": "artillery report results/*.json",
    "token": "powershell -ExecutionPolicy Bypass -File scripts/get-token.ps1",
    "token:update": "powershell -ExecutionPolicy Bypass -File scripts/update-scenarios.ps1",
  },
  "keywords": [
    "load-test",
    "artillery",
    "performance",
    "babple"
  ],
  "author": "Babple Team",
  "license": "MIT",
  "devDependencies": {
    "artillery": "^2.0.0"
  }
}

로그인 후 토큰 업데이트 스크립트

**scripts/get-token.ps1**이 다음 작업을 자동으로 수행한다.

  1. 서버에 로그인하여 최신 JWT 토큰 받기
  2. .env 파일에 토큰 저장
  3. 모든 시나리오 파일 (.yml)의 authToken 값 자동 업데이트
# 토큰 발급 + 모든 시나리오 파일 자동 업데이트
npm run token

get-token.ps1의 핵심 로직:

# 1. 서버 로그인
$response = Invoke-RestMethod -Uri "$BASE_URL/api/auth/login" `
    -Method Post `
    -Body (@{
        email = $TEST_EMAIL
        password = $TEST_PASSWORD
    } | ConvertTo-Json) `
    -ContentType "application/json"

$token = $response.data.token

# 2. .env 파일 업데이트
Set-Content -Path ".env" -Value "AUTH_TOKEN=$token"

# 3. 모든 시나리오 파일 업데이트 (정규식으로 authToken 값 교체)
$scenarioFiles = @("feed.yml", "recipe-detail.yml", "recipe-upload.yml", "mixed-scenario.yml")

foreach ($file in $scenarioFiles) {
    $content = Get-Content "scenarios/$file" -Raw
    $pattern = '(authToken:\s*")[^"]*(")'
    $replacement = "`$1$token`$2"
    $newContent = $content -replace $pattern, $replacement
    Set-Content -Path "scenarios/$file" -Value $newContent -NoNewline
}

테스트 시작

테스트 시작은 별 거 없다. 위 package.json에서 작성한 스크립트를 그대로 실행하면 끝이다.

npm run test:mixed

트러블 슈팅: 실패에서 성공까지

1차 시도: 의욕이 앞선 설정 (실패)

처음에는 arrivalRate: 10 (초당 10명 진입)으로 설정했다. “이 정도는 버티겠지?”라고 생각했다. 결과는 처참했다.

  • Error: Timeout awaiting 'request' for 30000ms (30초 타임아웃)
  • Failures: 700건 이상의 에러 발생
  • Response Time: 평균 27초

서버가 죽지는 않았지만, 요청이 큐에 쌓여 처리되지 못하는 서비스 불능 상태가 되었다.

리틀의 법칙으로 계산해보면 무려 동시 접속자 1,200명 수준의 부하였다.

단일 노드 서버로는 무리였다.

2차 시도: 현실적인 목표 수정 (성공)

목표를 수정했다. “동접 1,000명”이라는 막연한 목표 대신, **“동접 100명에서 안정적인가?”**를 먼저 검증하기로 했다. arrivalRate를 1~2 수준으로 낮추고 다시 돌렸다.

Summary report @ 23:41:25(+0900)
--------------------------------
http.codes.200: 1455
http.codes.201: 135
vusers.failed: 0 (실패 없음!)
http.response_time:
  median: 67.4 ms
  p95: 671.9 ms
  p99: 1085.9 ms

결과 분석:

  • 성공률 100%: 단 한 건의 에러도 없이 모든 시나리오를 완주했다.
  • 쾌적한 속도: 중앙값(Median)이 67ms로 매우 빠르다.
  • 안정성: 하위 95%(p95)의 요청도 0.67초 안에 처리되었다.

이제 우리 서버는 **“동시 접속자 100~150명 수준에서는 0.1초 내외의 빠른 응답을 보장한다”**고 자신 있게 말할 수 있게 되었다.


6. 결론

부하 테스트를 통해 얻은 것은 단순히 “서버가 튼튼하다/아니다”가 아니었다.

  1. 숫자로 된 기준점: 막연한 불안감이 “동접 150명까지 OK”라는 데이터로 바뀌었다.
  2. 한계 파악: 무리하게 부하를 주면 어디서부터(DB인지, 앱인지) 병목이 오는지 경험했다.

이번에 부하 테스트를 하면서 기능 구현이 끝났다고 끝난 게 아니다. 라는 것을 다시 한 번 체감했다.

물론, 내 서비스가 오픈 하자마자 지금 부하테스트를 한 것처럼 관심을 받을 확률은 거의 제로에 가깝지만, 이젠 서버가 죽어가는 것을 봤을 때 어느 정도로 서버를 증설해야 하는 지 알 수 있어졌다.

Artilley, 앞으로도 아주 많이 애용하게 될 거 같다.

끝!

다른 글 보기