소프트웨어를 최종 사용자에게 배포하기 전에 우리는 애플리케이션에 버그가 없고 비즈니스 요구 사항을 충족하는지 확인하기 위해 다양한 종류의 테스트를 수행합니다. 하지만 아무리 많은 테스트를 거쳐도 실제 사용자가 애플리케이션을 사용하기 전까지는 소프트웨어가 안정적이라고 확신할 수 없습니다. 사용자들이 애플리케이션을 사용하기 시작하면 다음과 같은 이유로 예상과 다르게 동작할 수 있습니다.
- 사용자 행동은 예측하기 어렵습니다.
- 사용자는 서로 다른 지역에 분산되어 있습니다.
- 대규모 사용자가 동시에 애플리케이션을 사용할 수 있습니다.
대규모 애플리케이션의 경우 이러한 요소들은 정식 출시 전에 반드시 파악해야 할 중요한 사항입니다. 애플리케이션이 기대한 대로 작동하는지 보장하려면 기능을 롤아웃할 때 다음 두 가지를 고려해야 합니다.
- 단계별 배포(Phased Rollout) — 모든 사용자에게 기능이 공개되기 전에 일부 사용자 그룹을 대상으로 먼저 테스트하는 방식입니다. 이를 통해 실제 사용자 행동 패턴을 파악할 수 있습니다.
- 부하 테스트(Load Testing) — 단계별 배포로 사용자 행동은 파악할 수 있지만, 여러 지역의 수많은 사용자가 동시에 애플리케이션을 사용할 때 플랫폼이 어떻게 작동하는지는 알 수 없습니다. 이를 검증하려면 부하 테스트가 필요합니다.
부하 테스트란 무엇이며, 성능 테스트와 어떻게 다를까?
다음 세 가지 용어는 비슷하게 들리지만 서로 다른 개념입니다.
- 성능 테스트(Performance Testing)
- 부하 테스트(Load Testing)
- 스트레스 테스트(Stress Testing)
성능 테스트는 주어진 입력 조건에서 애플리케이션이 어떻게 작동하는지 평가하는 포괄적인 테스트 방식입니다. 단일 사용자 환경에서도, 다중 사용자 환경에서도 수행할 수 있으며, 응답 시간이나 CPU/메모리 사용량 같은 특정 지표를 측정하는 것이 목적입니다. 부하 테스트와 스트레스 테스트는 성능 테스트의 하위 집합입니다.
부하 테스트는 특정 수의 사용자가 지정된 시간 동안 동시에 애플리케이션을 사용할 때 시스템이 기대한 대로 작동하는지 확인하기 위해 수행됩니다. 시스템이 감당할 수 있는 사용자 수를 파악하는 데 도움이 됩니다.
스트레스 테스트는 부하 테스트와 밀접한 관련이 있습니다. 유사한 방식으로 진행하지만 테스트의 목표가 다릅니다. 부하 테스트가 '지정된 사용자 수에서 애플리케이션이 정상 작동하는가'를 확인하는 것이라면, 스트레스 테스트는 부하 한계점을 넘었을 때 애플리케이션이 어떻게 동작하고 장애를 처리하는지 확인하는 것이 목적입니다.
또한 부하 상승(ramp-up) 속도에 따라 성능 테스트는 스파이크 테스트(spike test)와 소크 테스트(soak test)로 나눌 수 있습니다. 짧은 시간 안에 사용자가 급격히 몰리는 상황을 재현하는 것이 스파이크 테스트이고, 긴 시간에 걸쳐 서서히 사용자를 늘려가며 지속성을 확인하는 것이 소크 테스트입니다.
왜 성능 테스트가 중요한가?
실제 Rails 프로젝트에서 가까운 미래에 사용자가 크게 증가할 것으로 예상되는 경우가 있었습니다. 우리는 애플리케이션이 기대한 대로 작동하고, 사용자 수가 늘어나더라도 핵심 기능이 문제없이 유지되기를 원했습니다. 그렇다면 어떻게 이를 보장할 수 있을까요? 우리는 부하 테스트를 수행하여 주어진 사용자 증가분을 처리할 수 있는지 확인했습니다.
성능 테스트가 중요한 경우는 이 외에도 많습니다.
- 블랙프라이데이처럼 특정 날짜에 사용자가 급증할 것으로 예상된다면, 짧은 램프업 기간으로 스파이크 테스트를 수행해 잠재적인 시스템 문제를 미리 발견할 수 있습니다.
- 부하 테스트는 소수의 사용자만 접속할 때는 드러나지 않거나 미미했던 시스템 버그를 찾아내는 데 도움이 됩니다.
- 부하 증가가 플랫폼 속도에 어떤 영향을 주는지 평가할 수 있습니다. 애플리케이션이 느려지면 고객을 잃을 수 있습니다.
- 사용자가 10,000명일 때 높은 CPU나 메모리 사용량으로 시스템이 다운되는지 등, 증가된 부하 상황에서 시스템이 어떻게 작동하는지 평가할 수 있습니다.
- 특정 규모의 사용자를 지원하기 위해 애플리케이션 운영에 드는 비용을 산정할 수 있습니다.
실제로 부하 테스트를 수행하면서 Rails 앱의 버그를 발견한 적이 있습니다. 호텔 객실 예약 앱의 오픈 예약 프로세스에서, 소수의 사용자가 객실을 예약할 때는 아무 문제가 없었습니다. 그러나 여러 사용자가 동시에 같은 객실을 예약하려고 하면 서로 다른 두 사용자가 모두 예약에 성공하는 현상이 발생했습니다. 부하 테스트를 통해 이 문제를 조기에 발견하고, 기능 출시 전에 수정할 수 있었습니다.
Apache JMeter로 Rails 앱 부하 테스트하기
JMeter는 Apache 2.0 라이선스로 배포되는 오픈소스 부하 테스트 도구입니다. 스레드 기반 부하 테스트를 제공하므로, 수많은 사용자가 동시에 애플리케이션을 사용할 때 시스템이 받게 될 스트레스를 손쉽게 시뮬레이션할 수 있습니다. 또한 테스트 결과 리포팅 기능도 훌륭합니다.
이제 Apache JMeter를 사용해 다수의 사용자가 시스템을 이용하는 상황을 시뮬레이션함으로써, 잠재적인 시스템 문제와 애플리케이션의 응답 시간을 파악하는 방법을 살펴보겠습니다.
JMeter는 아래 링크에서 다운로드할 수 있습니다.
https://jmeter.apache.org/download_jmeter.cgi#binaries
알아두어야 할 JMeter 핵심 용어
- 테스트 플랜(Test Plan) — 최상위 개념으로, 부하 테스트 구성 요소들을 이 안에 정의합니다. 전역 설정과 변수도 여기서 정의합니다.
- 스레드 그룹(Thread Group) — 스레드 수, 램프업 기간, 스레드 간 지연 시간, 반복 횟수 등 스레드 관련 설정을 정의합니다. 부하 테스트를 실행할 병렬 사용자 수라고 생각하면 됩니다.
- 샘플러(Sampler) — 단일 스레드가 실행하는 작업 단위입니다. HTTP 요청, SMTP 요청, TCP 요청 등 다양한 종류가 있습니다.
- 전/후처리기(Pre/Post Processor) — 샘플러 실행 전후에 특정 작업을 수행합니다. 후처리기를 사용하면 한 API 호출의 응답 데이터를 추출해 다음 호출에 전달할 수 있습니다.
- 리스너(Listener) — 샘플러의 응답을 수집하고, 응답 시간이나 각 스레드의 응답에 대한 집계 리포트를 제공합니다.
- 어설션(Assertion) — 샘플러의 응답 데이터가 기대한 값인지 검증하는 데 유용합니다.
- 구성 요소(Config Element) — HTTP 헤더, HTTP 쿠키, CSV 데이터셋 설정 등 구성 정보를 정의합니다.
부하 테스트를 실행하려면 먼저 위에서 설명한 JMeter 구성 요소들을 정의한 JMX 파일을 생성해야 합니다.
부하 테스트용 JMX 파일 준비하기
JMX는 XML 형식으로 작성된 JMeter 프로젝트 파일입니다. 수동으로 작성하기는 어렵기 때문에, JMeter 인터페이스를 활용해 파일을 만들겠습니다.
JMeter 인터페이스를 열고 테스트 플랜을 찾습니다. 테스트 플랜 안에 스레드와 부하 테스트 설정을 추가합니다.
테스트 플랜을 생성하는 JMeter 인터페이스
테스트 플랜 이름은 부하 테스트 목적에 맞게 변경할 수 있습니다. 스레드 그룹(사용자)을 구성하고, 여기서는 기본 설정을 유지한 채 스레드 그룹을 생성합니다.
Add -> Threads(Users) -> Thread Group
스레드 그룹에는 기본적으로 스레드 1개가 지정되어 있습니다. 앱에 접속하는 사용자 수를 시뮬레이션할 수 있도록 필요한 숫자로 변경합니다.
웹 기반 Rails 앱을 부하 테스트할 것이므로 HTTP 샘플러를 추가합니다. 샘플러는 ThreadGroup 내부에 위치합니다. 다음 경로로 HTTP Sampler를 추가합니다.
Add -> Sampler -> HTTP Request
여기서 부하 테스트 대상 IP 또는 도메인, HTTP 메서드, 해당 엔드포인트에 필요한 요청 바디를 설정합니다.
마지막으로 부하 테스트 결과 리포트를 확인하기 위해 스레드 그룹에 리스너를 추가합니다.
Add -> Listener -> View Result Tree
View Result Tree는 각 스레드의 응답 시간을 표시합니다. 다른 종류의 리포트도 추가할 수 있습니다. 단, 'View Result Tree'는 디버깅 목적으로만 사용해야 하며 실제 테스트에는 사용하지 않는 것이 좋습니다.
이렇게 간단한 테스트 플랜을 만들고 실행할 수 있습니다. JMeter 상단 바의 재생 아이콘을 누르면 테스트가 실행됩니다.
Rails 앱 부하 테스트 전 고려사항
앞선 예제는 매우 단순한 단일 엔드포인트 HTTP 요청이었습니다. 하지만 실제 Rails 앱에서 테스트하려는 엔드포인트는 인증으로 보호되어 있는 경우가 대부분입니다. 따라서 다음 사항들이 준비되어 있어야 합니다.
- 웹 쿠키(Web Cookie) — 부하 테스트를 수행하려면 HTTP 엔드포인트에 쿠키 헤더가 포함되어야 합니다. JMeter는 사용자가 로그인한 후 쿠키를 추가하는 기능을 제공합니다. 다음 섹션에서 브라우저 요청을 녹화해 JMX 파일로 변환하는 방법과 함께 쿠키 녹화 방법도 다룹니다.
- Rails CSRF 토큰 — Rails는 CSRF 토큰을 통해 보안 취약점으로부터 앱을 보호합니다. 따라서 부하 테스트를 수행하기 전에 요청 헤더에 CSRF 인증 정보가 포함되어 있는지 확인해야 합니다. 이 CSRF 토큰은 HTML의
meta태그 안에 위치합니다.
Rails CSRF 토큰은 JMeter에서 후처리기를 사용해 추출할 수 있습니다. CSRF 토큰이 포함된 웹 페이지를 로드하는 HTTP Request를 마우스 오른쪽 버튼으로 클릭한 뒤 Add -> Post Processor -> Regular Expression Extractor를 선택합니다. 헤더의 meta 태그에서 CSRF 값을 읽어오기 위해 다음과 같이 정규식 추출기를 설정합니다.
- Reference Name:
csrf_value - Regular Expression:
name="csrfToken" content="(.+?)" - Template:
$1$ - Match No:
1
이제 csrf_value 변수를 사용해 요청을 보낼 수 있습니다.
브라우저 요청 녹화로 JMX 파일 자동 생성하기
HTTP 엔드포인트가 적다면 JMeter 인터페이스에서 직접 JMX 파일을 만드는 것도 간단합니다. 하지만 테스트 케이스가 커지면 작업이 어려워지고, 사용자가 앱을 사용하는 과정에서 발생하는 실제 요청을 놓칠 가능성도 있습니다. 브라우저에서 발생한 실제 요청을 녹화해서 JMX 파일을 자동으로 생성하는 것이 좋습니다.
JMeter를 Rails 앱과 브라우저 사이의 프록시로 설정할 수 있습니다. 이렇게 하면 모든 요청이 JMeter를 거쳐 Rails 서버로 전달됩니다. 흔히 MITM(Man In The Middle, 중간자) 공격이라고 불리는 방식과 동일한 원리입니다.
JMeter 녹화
JMeter에서 녹화를 생성하려면 file -> templates -> recording으로 이동한 뒤 create를 클릭합니다. 녹화할 호스트명을 지정하면 쿠키 매니저를 포함한 여러 설정이 자동으로 생성됩니다. 쿠키 매니저는 인증에 필요한 쿠키를 저장합니다.
JMeter SSL 인증서로 HTTPS 요청 검증하기
브라우저에서 보낸 요청은 JMeter로 전달되고, JMeter는 이를 웹 서비스로 전달하면서 요청을 녹화합니다. 이 녹화 결과를 바탕으로 JMeter에서 부하 테스트를 실행할 수 있습니다. 애플리케이션이 SSL 연결을 위해 https 프로토콜을 요구한다면 브라우저에 인증서를 추가해야 합니다. Firefox 또는 다른 브라우저를 열고, Firefox의 경우 settings > Privacy > Manage certificate로 이동해 JMeter 인증서를 추가하면 브라우저가 JMeter가 생성한 인증서를 신뢰하게 됩니다.
cmd + sht + g를 누르고 경로 /usr/local/Cellar/jmeter/5.2/libexec/bin/jmeter를 입력해 인증서를 추가합니다.
Firefox에서 JMeter를 프록시로 사용하도록 설정하기
다음으로 Firefox의 요청을 JMeter 녹화 스크립트로 전달해야 합니다. Firefox에서 프록시를 설정하면 됩니다. Firefox를 열고 Preferences -> Advanced -> Connection(settings)으로 이동합니다. 여기서 HTTP Proxy를 "localhost", 포트를 "8080"으로 설정하고 "Use this proxy server for all protocols"에 체크합니다.
이제 JMeter로 돌아가 앞서 선택한 템플릿의 Script recording 섹션으로 이동합니다. 시작 버튼을 누르면 JMeter가 들어오는 요청을 수신하기 시작합니다. Firefox에서 부하 테스트 대상 애플리케이션을 탐색하면 요청이 녹화되어 JMX 파일로 변환되고, 이 파일로 부하 테스트를 실행할 수 있습니다.
JMeter를 활용한 분산 부하 테스트
테스트 계획을 준비하는 단계에서는 로컬 머신 한 대에서 테스트를 수행해도 괜찮습니다. 하지만 실제 테스트를 실행할 때는 이를 변경해야 합니다. 단일 머신에서 부하 테스트를 수행하면 하드웨어 한계(CPU, 메모리)와 요청 위치의 한계에 부딪힙니다. 부하 테스트는 실제 사용자의 트래픽을 시뮬레이션하기 위한 것이므로, 테스트를 여러 서버에 분산하고 결과를 한곳에서 확인할 수 있어야 합니다.
JMeter는 테스트를 오케스트레이션하는 프라이머리 노드와 테스트를 실행하는 여러 세컨더리 노드를 제공합니다. 이를 통해 실제 사용자가 앱을 사용하는 상황을 더 사실적으로 재현할 수 있으며, 실제 사용자와 가까운 여러 리전에 테스트 서버를 분산 배치할 수도 있습니다.
JMeter 분산 테스트
분산 테스트를 수행하려면 먼저 프라이머리 서버와 세컨더리 서버 모두에 JMeter를 설치합니다.
세컨더리 서버에서 할 일:
jmeter/bin디렉터리로 이동해jmeter-server명령을 실행합니다. 이 명령이 테스트를 실행할 서버를 구동합니다.- 테스트에 CSV 입력이 필요하다면 해당 파일들을 이 서버에 추가합니다.
프라이머리 서버에서 할 일:
- jmeter/bin 디렉터리로 이동해
jmeter.properties파일을 엽니다. remote_hosts가 포함된 줄을 편집해 세컨더리 서버들의 IP를 쉼표로 구분해 추가합니다.remote_hosts=<s1_ip>,<s2_ip>- JMeter 테스트를 실행합니다.
세컨더리 서버가 실제 테스트를 담당하고, 프라이머리 서버가 리포트를 집계합니다.
부하 테스트를 수행할 때는 UI에서 테스트를 트리거하는 대신 항상 CLI 명령을 사용해야 합니다. UI 실행은 부하 테스트 서버에 성능 문제를 일으킬 수 있습니다. JMX 파일 경로를 지정해 다음과 같이 JMeter 명령을 사용합니다.
> jmeter -n -t path/to/test.jxm -r
또는
> jmeter -n -t path/to/test.jxm -R s1_ip,s2_ip,…
-r:jmeter.properties에 지정된 원격 서버를 사용합니다.-n: GUI 모드 없이 실행합니다.-t: JMX 파일 경로를 지정합니다.
Rails 서버, Puma vs Unicorn 선택은 어떻게 결정했나?
Puma와 Unicorn은 Rails에서 사용되는 서로 다른 웹 서버입니다. 둘 다 장점이 있으므로 어떤 것이 더 나은지는 애플리케이션에 따라 달라집니다. 어떤 앱은 Unicorn이 더 잘 맞고, 어떤 앱은 Puma가 더 잘 맞습니다. 우리는 하나의 Rails 앱에서 Unicorn과 Puma 중 선택해야 했고, 부하 테스트로 얻은 데이터를 기반으로 결정했습니다. 동일한 Rails 앱에 대해 한 번은 Unicorn으로, 또 한 번은 Puma로 부하 테스트를 수행했습니다. 변경한 것은 Rails 앱의 웹 서버뿐입니다.
테스트 결과는 다음과 같았습니다.
Puma와 Unicorn 간 응답 시간 차이
그 결과, 플랫폼에 대규모 사용자가 몰릴 때 Puma가 우리 Rails 앱에서 더 나은 성능을 보였습니다. 이는 더 적은 수의 애플리케이션 서버로 더 많은 사용자를 처리할 수 있다는 의미입니다.
참고: 이 결과는 사용 중인 서버 인스턴스 유형과 앱이 수행하는 비즈니스 로직의 성격에 따라 달라질 수 있습니다.
Rails 앱 스트레스 테스트에서 자주 발생하는 문제와 해결 방법
- 최적화되지 않은 데이터베이스 쿼리
- N+1 쿼리 문제를 제거합니다.
- 접근 패턴에 맞춰 인덱스를 추가합니다.
- 데이터베이스 앞단에 Redis 같은 캐싱 레이어를 도입합니다.
- 느린 Ruby 코드 성능
- 메모이제이션(Memoization)으로 코드를 최적화합니다.
- O(n²) 복잡도를 가진 코드를 찾아 더 효율적인 알고리즘으로 교체합니다.
- 마이크로서비스 아키텍처에서 서비스 간 HTTP 호출이 많은 경우 — 네트워크 호출은 느립니다.
- 마이크로서비스 간 통신에 메시징 시스템을 활용해 HTTP 호출 횟수를 줄입니다.
- 동시 생성 시 동일 레코드가 중복 생성되는 경우
- 데이터베이스 유니크 제약조건(unique constraint)을 추가합니다.
- FIFO 이벤트(큐) 기반 리소스 생성 방식을 활용합니다.
- 가능한 한 Sidekiq 같은 백그라운드 처리를 활용합니다.
- API 응답 시간에 대한 SLA를 정의하고, 성능 테스트를 개발 라이프사이클의 일부로 포함시킵니다.
부하 테스트는 어떤 환경에서 수행해야 할까?
운영(production) 환경에서 부하 테스트를 수행하는 것은 이상적이지 않습니다. 운영 환경에 문제를 일으키고 심하면 다운타임까지 초래할 수 있기 때문입니다. 하지만 동시에 테스트 리포트가 운영 환경과 유사한 실제 데이터를 반영하기를 원합니다. 부하/스트레스 테스트를 위해서는 운영 환경의 복제본(replica) 환경을 구축하는 것이 권장됩니다. 여기에는 다음과 같은 요소들이 포함됩니다.
- 애플리케이션 서버의 수
- 복제본을 포함한 데이터베이스 서버의 하드웨어 사양
- 테스트 데이터베이스에 운영 환경과 유사한 데이터 구축 — 운영 환경과 거의 동등한 데이터 볼륨을 포함해야 합니다.
운영 환경과 유사한 환경을 구축하는 것은 어렵고 비용도 많이 듭니다. 따라서 부하 테스트 대상 컴포넌트에 해당하는 인프라만 운영 수준으로 업그레이드하면 비용을 절감할 수 있습니다. 부하/스트레스 테스트는 3개월에 한 번씩 수행하는 것이 좋습니다. 반면, 단일 사용자 성능 테스트를 통해 응답 시간이 정의된 기준(예: 200ms) 이내인지 검증하는 작업은 개발 사이클에 상시 포함해야 합니다.
부하 테스트를 수행할 때는 대상 서버의 CPU/메모리 사용량 같은 데이터 포인트도 함께 수집해야 합니다. 메모리나 CPU 사용량이 급증하면 애플리케이션이 크래시될 수 있습니다. 하드웨어 KPI를 측정하려면 부하 테스트를 시작하기 전에 Prometheus 같은 모니터링 도구를 추가하세요.
Apache JMeter는 강력한 부하 테스트 도구입니다. 우리는 Rails 앱의 부하 테스트에 Apache JMeter를 사용했지만, 어떤 스택으로 빌드된 애플리케이션이든 부하/스트레스 테스트에 활용할 수 있습니다. 부하 테스트는 애플리케이션에 대한 데이터 기반 의사결정을 가능하게 해줍니다. 부하 테스트가 어렵게 느껴질 수 있지만, 초기에 조금만 투자하면 장기적으로 애플리케이션에 많은 안정성과 신뢰성을 더할 수 있습니다.