← Log

Artillery로 부하 테스트 시나리오 실행

성능 부하 테스트 기록· 2 / 2
  1. 1 — 부하 테스트의 개념과 지표
  2. 2 — Artillery로 부하 테스트 시나리오 실행

먼저, Nest.js 기반의 백엔드 앱을 테스트 해보자.

테스트 툴은 가벼우면서도 편한 Artillery을 사용할 것이다.

Artillery

Artillery는 API 및 웹 애플리케이션의 부하 테스트와 성능 모니터링을 위한 오픈 소스 툴이다. 특히 Node.js 환경에서 잘 통합되어, JavaScript나 TypeScript 백엔드 앱을 테스트하는 데 자주 사용된다.

<특징>

  • YAML와 JSON 형식으로 작성하며, 테스트 시나리오를 직관적으로 정의할 수 있음.
  • HTTP(S), Socket.io, Websocket 등 다양한 프로토콜 지원.
  • JavaScript로 로직을 작성해 동적 시나리오를 설정할 수 있음.
  • 리포트 페이지를 따로 제공하며 데이터는 JSON 형식으로도 출력할 수 있음.

빠른 테스트 맛보기

테스트를 본격적으로 진행하기에 앞서 Artillery를 로컬에 설치하고, 간단한 시도로 통해 체험해보자.

  • 도구 설치

    npm install -g artillery
    
    # v 2.0.21
  • artillery 실행

    CLI 명령어로 빠르게 간단한 부하 테스트를 실행할 수 있다. 준비된 채팅 백엔드 앱을 실행하고, 채팅방의 목록을 조회하는 API를 테스트에 이용해보았다.

    npx artillery quick --count 100 -n 50 http://localhost:3002/chat/rooms --output report.json

    Artillery quick 테스트 요약 리포트 터미널 출력

    위 테스트의 요청수는 가상 사용자 수(50) * 요청 수(100) = 총 5000회이다.

    http 계열은 서버가 처리하는 요청과 응답을 기준으로 서버의 성능(처리 속도, 성공률, 응답 시간)을 측정하며, vusers 계열은 가상 사용자의 세션 활동 기준으로 서비스의 성능(세션 시간, 완료 여부)을 측정한다.

  • 그래프 보고서 생성

    --output 명령어를 통해 추출한 결과 파일을 html로 변환해 결과 보고서 내용을 가시적으로 확인할 수 있다.

    npx artillery report report.json --output report.html

    Artillery HTML 리포트의 응답 시간 막대 그래프(min/max/median/p95/p99)

    응답 및 세션 시간을 평가할 때는, 모든 요청의 중간값인 median과 하위 5%와 1% 값인 p95, p99의 차이가 크지 않으면 성능이 안정적이고 고르게 분배되어 있다고 보면 된다.

    위 그래프로 봤을 때는 중앙값과 상위값의 차이가 꽤 나는 것으로 보이지만, 좀더 실제적인 실행 환경과 구체적인 시나리오를 통해 테스트한 후 다시 평가해볼 예정이다.

시나리오 테스트

좀더 구체적인 테스트를 위해 yml 파일로 사용자의 흐름을 토대로 시나리오를 구성해 테스트를 해보았다. 테스트할 시나리오는 아래와 같다.

  • [Scenario 1]. 채팅방 생성 > 채팅방 정보 조회

  • [Scenario 2]. 채팅방 입장 > 메세지 전송 > 채팅방 퇴장

  • [Scenario 1]. 채팅방 생성(HTTP)

    사용자가 채팅방을 생성하면 응답 데이터인 roomId를 이용해 브라우저 새탭에서 채팅방의 정보를 다시 요청한다. 이 두 API를 연결해 시나리오를 구성했다.

    config:
      target: 'http://localhost:3002'
      phases:
        - name: 'Baseline'
          duration: 30
          arrivalRate: 1
        - name: 'Ramp-Up'
          duration: 60
          arrivalRate: 2
          rampTo: 10
        - name: 'Steady-State'
          duration: 180
          arrivalRate: 10
        - name: 'Ramp-Down'
          duration: 30
          arrivalRate: 10
          rampTo: 0
    scenarios:
      - name: '채팅방 생성 및 정보 조회 시나리오'
        flow:
          - post:
              url: '/chat/rooms'
              json:
                namespace: 'anonymous'
                title: '채팅방 이름'
                capacity: 12
                isPassword: false
              capture:
                json: '$.data.id'
                as: 'roomId'
    
          - log: 'roomId: {{roomId}}'
    
          - get:
              url: '/chat/rooms/{{roomId}}'
              expect:
                statusCode: 200
              capture:
                json: '$.data.id'
                as: 'roomId'

    테스트는 <Baseline - Ramp-Up - Steady-State - Ramp-Down> 과정으로, 초기 설정 이후 점진적으로 요청 수을 늘리다가 높은 시점에서 지속한 후 다시 점진적으로 요청 수를 줄이는 방식으로 설정했다.

    사용자 경험에 따라 채팅방이 생성되면 해당 채팅방 정보에 접근해야하기 때문에, 생성 API가 완료되면 응답 데이터의 roomId를 추출해 캡처한 후, 조회 API 요청 시 파라미터로 사용하도록 해두었고, 데이터가 잘 캡처되는지 확인하기 위해 log로 실행 로그를 설정해두었다.

    capture 명령을 이용해 데이터를 캡처할 수 있으며, {{roomId}}와 같은 방식으로 캡처한 데이터를 불러올 수 있다.

    위 스크립트를 실행해보자.

    artillery run create.yml --record --key <Artillery Cloud API Key>

    채팅방 생성 시나리오 실행 로그와 roomId 캡처 확인 화면(터미널 2분할)

    약 5분 간 초당 1~10명의 가상 사용자, 총 4680번의 요청을 테스트한 결과다.

    시나리오에서 사용된 API는 2개로, 요청 횟수로 보면 채팅방 생성 POST API의 2340번(201 성공)과 정보 조회 GET API의 2340번(200 성공)으로 총 4680번의 요청을 받은 셈이다.

    오른쪽 터미널에는 시나리오 내의 API 요청 간 연계를 위한 roomId 캡처가 잘 작동하는지 확인하기 위해, 백엔드 앱의 API에 생성한 값과 전달 받은 값을 콘솔로 찍어 동일한지 확인했다.

    Artillery Cloud 대시보드 Overview 화면(부하 구간별 요청 수·응답 시간 그래프)

    동일한 테스트 결과가 Artillery Cloud에 전송된 화면이다.

    메인 그래프를 통해 구간별 요청 상황을 한눈에 볼 수 있다. http.request_rate(초당 요청수)vusers.created(가상 사용자 생성 수)의 모양이 점진적 증가 - 지속 - 점진적 감소로 의도대로 잘 처리되었음을 확인할 수 있다.

    http.response_time.p95(상위 95% 응답 속도)의 수치도 3~6ms 정도로 매우 양호한 수치를 보이고 있으며, 모든 요청이 에러없이 잘 응답되었다.

    위 테스트로 단기간의 정상 구동을 확인하였고, 지속성을 확인하기 위해 테스트 시간을 30분으로 늘려 추가 테스트를 진행하였는데, 결과는 아래 표로 정리해두었다.

    Smoke Test와 Load Test의 채팅방 생성 시나리오 결과 비교 표

    30분 간 테스트를 진행하면서 총 12,960명이라는 현재 서비스 상의 과도한 수의 가상 사용자가 채팅방 생성과 조회를 시도한 결과다.

    단기간 테스트와 비교하면 비교적 응답 시간이 늘어나긴 했지만, 사용자가 체감할 수 없는 수준으로 빠른 응답 속도이므로 문제가 없을 것이라 판단했다.

  • [Scenario 2]. 채팅 이용(WebSocket)

    두번째로는, 사용자가 생성된 채팅방에 입장해 메세지를 나누는 과정을 테스트해보고자 한다. 이 부분이 체류가 가장 긴 구간이라고 할 수 있다.

    채팅앱이 개발된 특성 상, HTTP 요청과 WebSocket 통신을 동시에 보낼 수 있도록 시나리오를 작성해야한다.

    config:
      target: 'http://localhost:3002'
      phases:
          - duration: 10
            arrivalCount: 75
      payload:
        path: './csv/roomIds.csv'
        order: 'sequence'
        fields:
          - 'namespace'
          - 'roomId'
    scenarios:
      - name: '사용자 채팅 프로세스 시나리오'
        engine: 'socketio'
        flow:
          - log: 'namespace: {{ namespace }} roomId: {{ roomId }}'
    
          - get:
              url: '/chat/rooms/{{ roomId }}'
    
          - think: 1
    
          - namespace: '{{ namespace }}'
            emit:
              channel: 'joinRoom'
              data:
                roomId: '{{ roomId }}'
    
          - think: 2
    
          - namespace: '{{ namespace }}'
            emit:
              channel: 'sendMessage'
              data:
                roomId: '{{ roomId }}'
                content: '안녕하세요! 😀'
          - think: 2
    
          - loop:
              - namespace: '{{ namespace }}'
                emit:
                  channel: 'sendMessage'
                  data:
                    roomId: '{{ roomId }}'
                    content: '{{ $randomString() }} [{{ $loopCount }}}번째 💬]'
              - think: '{{ $randomNumber(1, 10) }}'
            count: 10
    
          - namespace: '{{ namespace }}'
            emit:
              channel: 'sendMessage'
              data:
                roomId: '{{ roomId }}'
                content: '안녕히 계세요~  👋'

    30초 동안 75명의 사용자가 생성되어 시나리오대로 채팅을 이용하는 테스트 과정이다.

    config부터 보자면, 사용자를 75명으로 설정한 이유는 각 채팅방의 최대 인원수를 16명으로 제한했기 때문에, 테스트 모니터링 사용자 1명을 제외한 15명을 5개의 채팅방에 고루 분배했기 때문이다.

    채팅방에 접근하기 위해서는 고유한 roomId가 필요한데, 테스트를 위해 미리 5개의 데이터를 생성해 .csv 파일에 저장하고, order: 'sequence' 옵션을 사용해 순서대로 15명씩 다른 채팅방에 입장하도록 제한했다.

    scenarios는, 각 사용자가 총 14회의 요청을 보내도록 구성했다.(채팅방 정보 request 1회 + 입장 emit 1회 + 메세지 전송 emit 12회)

    요청 중간중간에 think 명령어를 두어 사용자의 지연 시간을 감안했고, 메세지 전송 시마다 1~10 초의 랜덤한 시간을 지연 시간으로 두어 좀더 자연스러운 행동을 반영했다.

    5개 채팅방에서 동시에 진행되는 채팅 테스트 화면

    실제로 5개의 채팅방에 미리 입장해 테스트를 모니터링하며 정상 구동되는 것을 확인했다.

    WebSocket 채팅 시나리오 실행 결과 요약 리포트(터미널)

    총 75명의 사용자를 1분 30초간 테스트한 결과, 전체적으로 빠른 응답 속도를 보였다.

    사용자의 채팅 사용감에 직결되는 WebSocket 통신은 통신 특성상 HTTP 요청보다 훨씬 빠른 속도로 측정되었다.

    vuser.session의 결과값은 사용자의 긴 체류 시간과 개별적으로 설정된 메세지 전송 후 지연 시간에 따라 달라지기 때문에, 사실상 큰 의미는 없을 것으로 간주된다.

    위 채팅 테스트에서 사용자 수와 메세지 전송 숫자를 높혀서 추가 테스트 진행해보았다.

    Smoke Test와 Load Test의 채팅 시나리오 결과 비교 표

    총 100개의 채팅방에 1600명의 사용자가 16명씩 고루 입장한 후, 100회 가량의 메세지를 전송했다.

    전체적으로 기능적인 큰 부하는 보이지 않았으며 일부 오히려 빠른 속도를 보이기도 했다.

마무리

전체적으로 현재 서비스 상황에 고려해 최대 사용자 유입을 예측해 테스트 해보았지만 기능적인 큰 부하를 보이진 않았다. 아직 백엔드 애플리케이션이 비교적 가볍고 복잡한 로직이 없기 때문으로 보인다.

프론트엔드의 경우, 실시간으로 대량의 메세지를 수신해 렌더링하는 과정에서 야간의 버벅임이 보이기는 했지만, 이 부분은 관련 파트의 개발자에게 조언을 구해 개선할 생각이다.

참고 자료

  • [1] Artillery Docs
  • [2] Artillery 부하 테스트 사용법
목차