테스트는 유지 보수가 악몽처럼 느껴지지 않고 새로운 기능이 꾸준히 추가되고 기존 기능이 개선되는, 잘 운영되는 대부분의 Rails 애플리케이션에서 빼놓을 수 없는 요소입니다. 그런데 아쉽게도 많은 애플리케이션이 무거운 작업 부하나 대량의 트래픽에 처음 노출되는 순간은 바로 프로덕션 환경입니다. 이런 테스트에는 비용이 상당히 들기 때문에 어찌 보면 당연한 일이기도 하죠.
다행히 Rails는 단위 테스트, 엔드투엔드(E2E) 테스트, 통합 테스트뿐만 아니라 성능 및 부하 관련 테스트도 폭넓게 지원합니다. 이 글에서는 이 모든 내용을 다루면서, 여러분의 애플리케이션 성능을 효율적으로 검증할 수 있도록 실질적인 예제도 함께 소개합니다.
이 글은 크게 두 부분으로 구성되어 있습니다:
- 이론 편 — 테스트가 왜 필요한지, 어떤 종류의 테스트가 있는지, 그리고 애플리케이션 테스트 시 반드시 확인해야 할 지표가 무엇인지 살펴봅니다.
- 실습 편 — 실제 애플리케이션에 테스트를 직접 작성하고 결과를 확인해 봅니다.
두 파트를 모두 읽고 나면 다양한 테스트 유형과 Rails 애플리케이션에 적용하는 방법을 더 깊이 이해하게 될 것입니다. 흥미로우신가요? 그럼 테스트에 관한 간단한 이론부터 시작해 보겠습니다.
이론으로 알아보는 테스트
테스트는 어떤 종류의 애플리케이션을 개발하더라도 항상 개발 과정의 일부여야 합니다. 아직 확신이 서지 않거나 테스트를 작성해 본 적이 없다면, 다음과 같은 이유들을 참고해 보세요.
- 무언가 망가뜨릴 걱정 없이 변경 사항 도입 가능 — 테스트가 필요한 가장 큰 이유입니다. 거대한 앱에서 사소한 변경 하나를 할 때마다 전체 화면을 클릭하며 문제가 없는지 확인한다고 상상해 보세요. 테스트가 있다면 명령어 한 번으로 자동적이고 빠르게 검증할 수 있습니다.
- 손쉬운 리팩토링 — 앞서 테스트가 신규 기능 추가나 변경 작업에 필수적이라고 언급했습니다. 테스트가 갖춰져 있으면 기존 코드를 개선할 때도 한결 마음이 편안해집니다.
- 테스트는 일종의 문서 — 잘 작성된 테스트는 애플리케이션의 여러 기능에 대한 문서 역할을 합니다. 해당 기능이 무엇인지뿐만 아니라 어떻게 동작해야 하는지까지 설명해 주죠.
- 구현을 다시 생각해 볼 기회 — 테스트를 작성하면서 의도한 구현 방식이 올바르고 합리적인지 다시 한번 점검할 수 있습니다. 또한 코드가 예상대로 동작하는지도 자연스럽게 확인하게 됩니다.
위의 이유들이 개발 과정에서 테스트를 활용해야 하는 계기가 되었기를 바랍니다. 코드를 왜 테스트해야 하는지 아는 것만큼이나, 다양한 테스트 유형을 아는 것도 중요합니다.
다양한 테스트 유형
Rails 애플리케이션의 성능이 올바른지, 그리고 인프라가 무거운 부하 속에서도 잘 동작하는지 확인하려면 세 가지 주요 테스트 유형을 활용할 수 있습니다.
- 부하 테스트(Load Testing) — "주어진 기간 동안 시스템이 몇 명의 동시 사용자를 처리할 수 있는가?"라는 질문에 답하는 테스트입니다. 웹사이트에서 인기 상품을 출시했는데 수천 명의 사용자가 동시에 주문을 시도하는 상황을 상상해 보세요. 제대로 된 부하 테스트가 없다면 가장 중요한 순간에 서비스가 다운될 위험이 있습니다.
- 스트레스 테스트(Stress Testing) — 이 테스트는 시스템이 처리할 수 있는 동시 사용자 수를 확인하는 것이 아니라, 사용자 한도에 도달했을 때 시스템이 어떻게 반응하는지에 초점을 맞춥니다.
- 성능 테스트(Performance Testing) — 성능 테스트는 스트레스 테스트와 부하 테스트의 상위 개념이라고 할 수 있습니다. 주요 목적은 특정 지표 세트를 확보하여, 이를 바탕으로 애플리케이션 코드를 개선할 조치를 취하는 것입니다. 관련 지표는 잠시 후 자세히 설명하겠습니다.
이제 이론 파트의 마지막 단계인, Rails 애플리케이션의 성능 테스트 시 핵심이 되는 지표에 대해 알아볼 준비가 되었습니다. 이 지표들을 제대로 이해하지 못하면 테스트 결과를 정확히 해석하지 못하고 코드를 수정해야 할지 판단할 수 없습니다.
핵심 지표
테스트 도구에 따라 얻을 수 있는 지표는 달라지지만, 일반적으로 다음과 같이 공통적인 지표들로 묶을 수 있습니다.
- 응답 시간(Response Time) — 요청이 발생한 시점부터 응답이 브라우저에 렌더링되기까지의 시간입니다. 사용자가 요청한 정보를 받기까지 얼마나 기다려야 하는지를 보여주는 지표로, 프로세스 타임(process time)이라고도 불립니다.
- 메모리 사용량(Memory Usage) — 특정 요청에서 소모되는 메모리의 양입니다. 코드를 개선해서 시스템이 더 빠르게 응답하고 리소스를 덜 사용하도록 만들 위치를 알려주는 중요한 정보입니다.
- 객체 할당(Object Allocation) — 메모리 할당이 많으면 메모리 사용량 증가와 긴 응답 시간으로 이어집니다. 이 지표를 통해 많은 객체가 할당되는 코드의 정확한 위치를 찾아 즉시 점검할 수 있습니다.
테스트 시 더 많은 지표를 확인할 수도 있지만, 위의 세 가지가 가장 중요하며 어떤 애플리케이션을 테스트하든 유효합니다. 이제 실제 테스트를 직접 작성해 볼 차례입니다.
실습
테스트할 대상이 없으면 테스트를 작성할 수 없습니다. 그래서 실습 파트의 첫 번째 단계는 테스트를 작성할 간단한 Rails 애플리케이션을 만드는 것입니다.
샘플 Rails 애플리케이션
저는 Ruby 3.0.1과 Rails 6.1.3.1을 사용하지만, 익숙한 버전을 자유롭게 사용하셔도 됩니다. Ruby와 Rails가 설치되어 있다면 다음 명령으로 애플리케이션 골격을 생성합니다.
rails new simpleapp -d=postgresql이 글을 위해 사용자 목록과 함께 각 사용자의 반려동물 이름을 보여주는 앱을 만들겠습니다. 이 구조 덕분에 N+1 쿼리를 손쉽게 만들어 낼 수 있고, 성능 테스트를 진행하며 속도와 기타 지표에 미치는 영향을 확인하는 재미가 더해집니다.
모델을 생성하기 전에 먼저 데이터베이스를 만듭니다.
cd simpleapp/
bin/rails db:create이제 모델을 생성합니다.
rails g model User name:string
rails g model Animal name:string user:references
bin/rails db:migrateUser 모델에 Animal 모델과의 관계를 반영하기 위해 간단한 수정만 해줍니다.
class User < ApplicationRecord
has_many :animals
end이제 db/seeds.rb 파일에 시드 데이터를 추가합니다.
people = {
'Tim' => ['Pinky', 'Rick'],
'Martha' => ['Rudolph'],
'Mark' => ['Niki', 'Miki', 'Bella'],
'Tina' => ['Tom', 'Luna']
}
people.each_pair do |name, pets|
user = User.create(name: name)
pets.each do |pet_name|
user.animals.create(name: pet_name)
end
end그리고 데이터를 데이터베이스에 로드합니다.
bin/rails db:seed사용자 정보를 담당하는 컨트롤러를 하나 만들고, 뷰에서는 모든 사용자와 그들의 반려동물 이름을 나열하겠습니다. 일부러 성능 문제를 유발하는 코드를 사용했으니, 나중에 개선 효과를 직접 측정해 볼 수 있습니다.
touch app/controllers/home_controller.rb
mkdir app/views/home
touch app/views/home/index.html.erb컨트롤러는 아주 간단합니다.
class HomeController < ApplicationController
def index
@users = User.all
end
end뷰도 마찬가지입니다.
<h1>List</h1>
<ul>
<% @users.each do |user| %>
<li><%= user.name %> (<%= user.animals.count %>)
<ul>
<% user.animals.each do |animal| %>
<li><%= animal.name %></li>
<% end %>
</ul>
</li>
<% end %>
</ul>마지막으로 config/routes.rb 파일을 수정해 메인 URL에 접속했을 때 무엇을 보여줄지 Rails에게 알려줍니다.
Rails.application.routes.draw do
root to: 'home#index'
endJMeter를 활용한 부하 테스트
JMeter는 Apache Software Foundation이 만든 오픈소스 소프트웨어로, 기능 동작에 대한 부하 테스트를 위해 설계되었습니다. Java로 만들어진 프로그램이기 때문에 어떤 운영체제에서든 설치할 수 있습니다. 파일은 https://jmeter.apache.org/download_jmeter.cgi 에서 다운로드할 수 있습니다.
macOS를 사용 중이라면 Homebrew로 간편하게 설치할 수 있습니다.
brew install jmeter설치 후에는 다음 명령으로 프로그램을 실행합니다.
jmeter테스트 설정하기
설정 과정은 다음 단계로 구성됩니다.
- 스레드 그룹(Thread Group) 추가 — 사용자 수와 각 사용자가 웹사이트에 머무는 시간을 지정
- HTTP 요청 설정 — JMeter가 호출할 엔드포인트를 지정
- 관심 있는 지표 설정
앞서 만든 간단한 앱의 메인 페이지에 단일 사용자 요청을 시뮬레이션하는 기본 테스트 설정을 단계별로 살펴보겠습니다.
스레드 그룹 추가
"Test Plan"을 우클릭해 열리는 메뉴에서 Add -> Threads (Users) -> Thread Group을 선택합니다.
그다음 사용자 수와 추가 속성들을 지정합니다.
HTTP 요청 설정
이전 단계에서 만든 스레드를 우클릭하고 Add -> Sampler -> HTTP Request를 선택합니다.
프로토콜, 서버 이름, 포트, 요청 경로를 설정합니다.
결과 뷰 지정
HTTP 요청을 우클릭하고 Add -> Listener -> View Results Tree를 선택합니다.
테스트 실행하기
테스트 설정이 완료되었으니 이제 실행해 볼 차례입니다. 초록색 재생 버튼을 클릭하기만 하면 됩니다.
보시는 것처럼 애플리케이션이 테스트를 통과했습니다. 하지만 이는 단일 요청일 뿐이라 결과는 당연한 것이죠. 이제 사용자 수와 기타 설정 옵션을 바꿔가며 애플리케이션이 어떻게 반응하는지 실험해 볼 수 있습니다. 제가 진행한 테스트에서는 약 200명의 사용자가 동시에 접근하기 시작했을 때 간단한 샘플 앱이 다운되기 시작했습니다.
다음 단계
부하 테스트를 수행하고 나면 애플리케이션의 취약 지점을 파악할 수 있습니다. 사용자 한도를 파악했다면 이제 스트레스 테스트를 진행해 한도를 넘었을 때 애플리케이션이 어떻게 동작하는지 확인해 보세요.
ruby-prof를 활용한 성능 테스트
성능 테스트 기능은 Rails 버전 3까지 내장되어 있었다가 이후 별도의 gem인 https://github.com/rails/rails-perftest 로 분리되었습니다. 최신 버전의 Rails에서 사용하는 데 문제가 좀 있었기 때문에 이 글에서는 제외하고, 대신 매우 잘 작동하는 ruby-prof 라이브러리를 사용하겠습니다.
평소와 같이 첫 번째 단계는 gem을 추가하는 것입니다.
bundle add ruby-prof설정 과정의 두 번째이자 마지막 단계는 config/application.rb를 수정해 gem의 미들웨어를 등록하는 것입니다. 이렇게 하면 라이브러리가 요청을 자동으로 분석하고 그에 기반한 리포트를 생성해 줍니다.
module Simpleapp
class Application < Rails::Application
config.middleware.use Rack::RubyProf, :path => './tmp/profile'
end
end이제 앱에 접속할 때마다 요청이 발생할 때마다 gem이 새로운 리포트를 생성합니다. 리포트는 다음과 같은 형태입니다.
리포트는 설정한 경로, 즉 이 예제에서는 tmp/profile에서 찾을 수 있습니다. 두 번째 리포트도 함께 생성되는데, 호출 스택(call stack)을 보여주며 Rails 애플리케이션의 성능 문제를 디버깅할 때 상당히 유용한 지표입니다.
한 가지 기억할 점은 cache_classes와 cache_template_loading 설정을 true로 하면 Rails가 필요한 파일을 로드하느라 애플리케이션이 느려지고 측정 지표가 왜곡될 수 있다는 것입니다.
마무리
테스트는 모든 개발 과정에서 필수적인 부분입니다. 코드가 의도한 대로 동작하는지 확인하는 것만큼 우리의 솔루션이 좋은 성능을 내는지 검증하는 것도 중요합니다. 테스트를 건너뛰면 앱의 성능과 사용자의 신뢰에 영향을 미치는 심각한 문제로 이어질 수 있습니다. 다행히 테스트는 생각만큼 어렵지 않습니다.
이 글에서 다룬 테스트의 핵심 내용은 다음과 같습니다.
- 코드를 테스트해야 하는 이유
- 다양한 종류의 성능 테스트
- Rails 앱의 성능을 테스트하는 방법
왜 테스트해야 하는지, 그리고 어떻게 하는지 이제 알았으니 테스트 작성의 중요성을 더욱 실감하셨기를 바랍니다.
로컬뿐만 아니라 프로덕션 또는 스테이징 환경에서도 앱의 성능을 모니터링하고 싶으시다면 AppSignal도 확인해 보세요.
P.S. Ruby Magic의 글을 발행 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!