Artillery를 활용한 부하 테스트 시작하기 🚀

성종호·2025년 3월 2일
2

Artillery를 활용한 부하 테스트 시작하기 🚀

테스트 환경 구성

  • 로컬 테스트

    • 환경: Docker 컨테이너에서 어플리케이션 실행
    • 특징: 동일 네트워크 내에서 통신하여 레이턴시가 낮게 측정됨
    • 주의: 실제 사용자 환경과는 차이가 있을 수 있음
  • 실제 서비스 환경

    • 부하 테스트 요청을 보내는 인스턴스와 WAS(어플리케이션 서버)를 별도의 클라우드 인스턴스에 배치
    • 실제 사용자가 접속하는 환경과 유사하게 네트워크 레이턴시와 처리량을 측정할 수 있음

요약:
이번 테스트는 초기 검증 및 병목 현상 탐지를 위해 로컬 환경에서 진행했지만,
실제 서비스 환경과 유사하게 테스트하려면 클라우드에서 별도의 인스턴스를 사용해야 합니다.
(참고로 로컬 테스트에서는 레이턴시가 낮게 측정되어 신빙성이 떨어집니다. ) 😊


1. 왜 부하 테스트를 하나요? 🤔

프로젝트가 어느 정도의 트래픽을 안정적으로 처리할 수 있는지,
그리고 성능 병목 현상을 빠르게 발견해 개선할 수 있도록 하기 위해서라고 생각합니다.


2. 왜 Artillery인가? 💡

여러 부하 테스트 도구 중 Artillery를 선택한 이유는 다음과 같습니다:

  • 접근성: YAML 파일로 테스트 시나리오를 간단하게 작성할 수 있어 초보자도 쉽게 접근할 수 있습니다.
  • 유연성: 램업, 안정, 램다운 등 다양한 부하 패턴을 지원하며, 사용자 정의 스크립트를 통해 복잡한 로직도 구현할 수 있습니다.
  • 확장성: 기본 HTTP 요청뿐만 아니라 WebSocket, MQTT 등 다양한 프로토콜을 지원하여 다양한 테스트가 가능합니다.

3. 성능 테스트 주요 용어 📚

  • TPS (Transactions Per Second)
    초당 처리되는 트랜잭션 수를 의미하며, 시스템의 전체 처리량을 평가합니다.

  • RPS (Requests Per Second)
    초당 발생하는 요청 수를 의미합니다. HTTP 요청 기반 테스트에서 주로 사용됩니다.

  • 레이턴시 (Latency)
    요청을 보내고 응답을 받기까지 걸리는 시간입니다.

  • 퍼센타일 (Percentile)
    예를 들어, p90은 전체 요청 중 90%가 해당 시간 이하로 응답받았음을, p99는 99%가 그 이하의 응답시간을 경험했음을 의미합니다.

  • vUser (Virtual User)
    부하 테스트에서 시뮬레이션 되는 가상의 사용자를 의미합니다.
    하나의 vUser는 미리 정의된 시나리오(예: 로그인, 데이터 요청, 로그아웃)를 수행합니다.


4. Artillery YAML 구성 및 실행 🎯

아래는 실제 테스트에 사용한 YAML 파일 예시입니다.
이 예시는 로컬에서 Docker로 어플리케이션을 띄우고 부하 테스트를 진행한 환경을 기반으로 작성되었습니다.

4.1 YAML 파일 구성

config:
  target: "http://localhost:8080"  # 실제 API 서버 주소로 변경
  phases:
    # 램업: 2분 동안 5에서 25까지 가상 사용자를 점진적으로 증가
    - duration: 120
      arrivalRate: 5
      rampTo: 25
    # 안정: 1분 동안 초당 25건 유지
    - duration: 60
      arrivalRate: 25
    # 램다운: 2분 동안 25에서 5까지 감소
    - duration: 120
      arrivalRate: 25
      rampTo: 5
  processor: "./processor.js"
  defaults:
    headers:
      Content-Type: "application/json"
  variables:
    userId: 1  # 테스트할 사용자 ID (필요에 맞게 수정)
  payload:
    path: "./data.csv"   # CSV 파일 경로 (예: 스크립트와 동일한 디렉토리 내)
    fields:
      - email
      - password

before:
  flow:
    - post:
        url: "/api/v1/users/sign-in"
        json:
          email: "{{ email }}"
          password: "{{ password }}"
        capture:
          - json: "$.data.accessToken"
            as: "accessToken"
          - header: "set-cookie"
            as: "refreshToken"
  afterResponse: "combineTokens"  # processor.js 내 함수로 캡처된 토큰들을 조합

scenarios:
  - name: "로그인 후 투자 일기 생성, 투자 자산 추가 및 삭제 플로우"
    flow:
      # 1. 투자 일기 생성
      - post:
          url: "/api/v1/users/{{ userId }}/diaries"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"
          json:
            title: "Investment Diary Title"
            content: "Investment diary content"
            date: "2025-03-01T14:55:35.478Z"
          capture:
            - json: "$.data.id"
              as: "diaryId"

      # 2. 자산 목록 조회
      - get:
          url: "/api/v1/assets?offset=0"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"

      # 3. 투자 일기 자산 추가
      - post:
          url: "/api/v1/users/{{ userId }}/diaries/{{ diaryId }}/diary-assets"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"
          json:
            diaryId: "{{ diaryId }}"
            assetId: "{{ diaryId }}"    # 예시: diaryId를 assetId로 사용 (실제 테스트에 맞게 수정)
            amount: 100
            buyPrice: 50
          capture:
            - json: "$.data.id"
              as: "diaryAssetId"

      # 4. 투자 일기 자산 삭제
      - delete:
          url: "/api/v1/users/{{ userId }}/diaries/{{ diaryId }}/diary-assets/{{ diaryAssetId }}"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"

      # 5. 투자 일기 목록 조회
      - get:
          url: "/api/v1/users/{{ userId }}/diaries"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"

      # 6. 투자 일기 삭제
      - delete:
          url: "/api/v1/users/{{ userId }}/diaries/{{ diaryId }}"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"

      # 7. 자산 목록 재조회
      - get:
          url: "/api/v1/assets?offset=0"
          headers:
            Authorization: "Bearer {{ tokens.accessToken }}"
          cookie:
            refreshToken: "{{ tokens.refreshToken }}"

4.2 Artillery 설치 및 실행

Artillery 설치

npm install -g artillery

부하 테스트 실행

artillery run test.yaml --key yourkey --record

테스트 실행 후 Artillery 대시보드에 접속하면 아래와 같이 결과를 확인할 수 있습니다.

  • Average req/s 118: 초당 평균 118회의 요청이 발생했음을 의미합니다.
  • Peak req/s 175: 테스트 중 최고 초당 175회의 요청이 발생했습니다.
  • 5100 vusers created: 총 5100회의 시나리오가 실행되었습니다.
  • 5분간 5100회의 시나리오를 실행한 결과, 약 17 TPS (5100/300)가 측정되었습니다.

또한, 아래와 같이 각 요청의 퍼센타일 정보를 통해 성능 병목을 확인할 수 있습니다.

전체 시나리오는 아래와 같이 p99는 433ms, p90은 408ms로 나타나 하나의 트랜잭션의 레이턴시도 확인할 수 있습니다.


후기 💬

테스트 환경에 맞춰 YAML 파일을 작성하는 동안 여러 시행착오를 겪었지만,
이번 테스트를 통해 내 프로젝트가 어느 정도의 트래픽을 견딜 수 있는지, 그리고
어떤 부분에서 성능 병목이 발생하는지 파악할 수 있었습니다.

추가적으로, 더 많은 부하를 주었을 때 아래와 같이 연결이 끊기거나
컨테이너 CPU 사용률이 급증하는 문제도 확인했습니다.

다음 포스트에서는 Grafana + Prometheus를 활용해 모니터링을 구축하고,
병목 현상을 분석하여 개선하는 사례를 다룰 예정입니다. 👍

profile
아자

0개의 댓글

관련 채용 정보