ML.
← 글 목록

Rails 요청 하나의 CPU는 어디에 쓰일까요? HTTP, 라우팅, DB, ORM으로 나눠 측정해 보았습니다

같은 목록 API 요청 하나를 HTTP 서버, 라우팅·미들웨어, DB 쿼리, ORM 네 부분으로 나눠 Rails, Express, Gin에서 측정했습니다. Rails 요청 CPU의 70%는 Active Record였고, 라우팅과 미들웨어는 7%에 그쳤습니다.

SeongHwa Lee··11 min read

1. 요청 하나를 단계별로 나눠 보기

첫 글에서는 같은 블로그 API를 언어마다 두 번씩 만들어 프레임워크와 ORM을 쓰는 버전이 직접 SQL을 쓰는 버전보다 CPU를 얼마나 더 쓰는지 측정했습니다. Rails는 요청당 CPU가 7.67배였습니다.

그런데 이 배율만으로는 요청의 어떤 부분이 느린지 알 수 없습니다. HTTP 요청을 받고, 라우팅하고, 미들웨어를 지나고, DB 쿼리를 실행하고, 결과를 모델로 만들고, JSON으로 내보내는 과정 중 어디가 느린 걸까요? 그래서 이번에는 요청 전체를 하나하나 나눠서 Rails와 Express(Node), Gin(Go)에서 같은 방법으로 측정해 보았습니다.

2. 한 단계씩 더해 가는 엔드포인트 네 개

런타임마다 엔드포인트를 네 개 만들었습니다. 각 엔드포인트는 앞의 것에 딱 한 가지만 더합니다.

엔드포인트하는 일앞의 것에 더해지는 것
A. bare 서버 + 고정 JSON프레임워크 없이 HTTP 요청을 받아 고정된 JSON을 반환HTTP 서버와 JSON 출력
B. 프레임워크 + 고정 JSON같은 JSON을 프레임워크의 라우터와 렌더러로 반환라우팅·미들웨어
C. 프레임워크 + 직접 SQL게시글 목록을 SQL 4문장으로 직접 조회해 반환DB 쿼리와 드라이버
D. 프레임워크 + ORM같은 목록을 ORM으로 조회해 반환 (실제 목록 API)ORM

따라서 두 엔드포인트의 차이가 곧 새로 더한 부분이 쓰는 CPU입니다. B에서 A를 빼면 라우팅·미들웨어, C에서 B를 빼면 DB 쿼리, D에서 C를 빼면 ORM입니다.

비교가 공정하도록 두 가지를 맞췄습니다. 고정 JSON은 여섯 앱 모두 같은 게시글 20개를 반환하고 요청마다 새로 직렬화합니다. C는 bare 앱의 목록 코드를 그대로 프레임워크 안으로 옮긴 것이고 D와 바이트 단위로 같은 JSON을 반환합니다.

측정 조건은 첫 글과 같습니다. 앱 하나에 CPU 1개, 프로세스 1개, 스레드 1개를 주고 동시 요청 8개로 10초 동안 부하를 걸었습니다. CPU 시간은 컨테이너의 cgroup에서 읽어 요청 수로 나눴고 3회 측정의 중앙값을 적었습니다.

3. 결과

요청당 CPUHTTP 서버와 JSON (A)라우팅·미들웨어 (B−A)DB 쿼리 (C−B)ORM (D−C)합계 (D)
Rails0.056 ms0.086 ms0.210 ms0.804 ms1.155 ms
Express0.019 ms0.011 ms0.215 ms0.227 ms0.472 ms
Gin0.017 ms0.002 ms0.133 ms0.130 ms0.283 ms
HTTP 서버와 JSON라우팅·미들웨어DB 쿼리ORM
Rails
1.155
Express
0.472
Gin
0.283
0.00.30.60.91.2 ms
요청 하나의 CPU를 층별로 나눈 것. 목록 API 요청 하나의 CPU 시간 전체입니다. 가장 진한 칸이 ORM입니다. 측정: study-rails-compare, 3회 중앙값
프레임워크HTTP 서버와 JSON라우팅·미들웨어DB 쿼리ORM합계
Rails0.0560.0860.2100.8041.155
Express0.0190.0110.2150.2270.472
Gin0.0170.0020.1330.1300.283

같은 결과를 비율로 보면 이렇습니다.

요청 CPU 중HTTP 서버와 JSON라우팅·미들웨어DB 쿼리ORM
Rails5%7%18%70%
Express4%2%46%48%
Gin6%1%47%46%

Rails 요청 CPU의 70%는 Active Record입니다. Express와 Gin에서는 ORM이 쓰는 CPU가 DB 쿼리와 비슷한데, Rails에서는 DB 쿼리의 네 배 가까이 됩니다.

4. 부분별로 살펴보기

HTTP 서버와 JSON 출력은 세 런타임 모두 작습니다. Rails(Puma와 Rack)가 0.056ms로 Node와 Go의 세 배이지만 요청 전체에서는 5%입니다.

라우팅과 미들웨어도 크지 않습니다. Rails는 0.086ms로 Express(0.011ms)나 Gin(0.002ms)보다 훨씬 크지만 요청 전체의 7%입니다. Rails API 앱은 기본으로 미들웨어를 13개 거치고 라우터와 컨트롤러와 렌더러를 지납니다. 이 과정을 모두 거쳐도 7%입니다. 첫 글에서 Rails 프레임워크가 차지하는 비율을 11.4%라고 적었는데, 그 값은 HTTP 서버까지 합친 것이었습니다. 이번에는 그 둘을 나눴습니다.

DB 쿼리에 드는 CPU는 세 런타임이 비슷합니다. 같은 SQL 4문장을 실행하고 결과를 받아 JSON 형태로 만드는 데 0.13ms에서 0.21ms가 듭니다.

다만 Rails에서는 확인하지 못한 차이가 하나 있습니다. 같은 SQL 코드를 bare 앱에서 돌리면 0.112ms인데, Rails 안에서 돌리면 0.210ms입니다. JSON 인코딩 방식의 차이는 0.003ms라 원인이 아니었고 Rails 프로세스 안에서 같은 코드만 따로 재면 0.116ms로 bare 앱과 같았습니다. Rails 요청 안에서만 0.1ms가 더 드는 원인은 이번 측정으로는 찾지 못했습니다. 그래서 표의 Rails DB 쿼리 값에는 이 0.1ms가 포함되어 있습니다.

ORM은 Rails에서만 압도적입니다. Active Record는 요청당 0.804ms로, Sequelize(0.227ms)의 3.5배, GORM(0.130ms)의 6.2배입니다. Active Record 하나가 HTTP 서버, 라우팅·미들웨어, DB 쿼리를 모두 합친 값(0.352ms)의 두 배가 넘습니다.

5. Active Record 안에서는 무엇이 느릴까요

ORM 부분을 조금 더 들여다보면, Active Record에서 가장 느린 단계는 모델을 만드는 단계입니다. 같은 SQL을 실행하고 결과를 받는 쿼리 계층은 Sequelize와 비슷한 수준이고 결과 행을 Active Record 레코드로 바꾸는 단계에서 CPU 사용량이 크게 늘어납니다.

그렇다고 레코드 하나를 만드는 데 오래 걸리는 것은 아닙니다. 이 목록 API는 게시글과 태그를 잇는 조인 테이블의 행까지 모두 레코드로 만들기 때문에, 게시글 20개를 보여 주는 데 레코드를 137개나 만듭니다. 이 부분은 다음 글에서 Sequelize, GORM과 단계별로 비교하며 자세히 다루겠습니다.

6. 정리

  • Rails 요청이 느린 이유는 대부분 Active Record 때문입니다. 요청 CPU의 70%입니다.
  • 라우팅과 미들웨어는 생각보다 작습니다. 미들웨어를 줄이거나 라우팅을 손보는 최적화로 줄일 수 있는 것은 요청의 7% 안쪽입니다.
  • DB 쿼리에 드는 CPU는 언어와 상관없이 비슷합니다. 세 런타임 모두 0.1ms대입니다.

7. 이 측정의 한계

엔드포인트 하나로 측정했습니다. 게시글 목록 API 하나만 측정했으므로, 형태가 다른 요청에서는 각 부분의 비율이 달라질 수 있습니다.

CPU 1개, 동시 요청 8개 조건입니다. 워커를 여러 개 띄우거나 스레드를 늘리면 절대값은 달라집니다. 이 측정은 절대값보다 각 부분의 비율을 보기 위한 것입니다.

DB는 같은 머신에 있습니다. DB가 다른 서버에 있으면 응답 시간은 늘지만, 이 측정은 앱의 CPU 시간만 재므로 네트워크 대기 시간은 포함되지 않습니다.

각 부분의 값은 엔드포인트 간 차이로 계산했습니다. 각 부분이 쓰는 CPU가 서로 독립적이라고 가정한 값입니다. 4장에 적은 Rails DB 쿼리 값처럼, 여러 부분이 함께 동작하면서 생기는 비용은 어느 한 부분의 값에 섞일 수 있습니다.


측정 일자: 2026-09-29 코드·원본 결과: https://github.com/MartianLee/study-rails-compare (harness/layers.py, docs/layers.md) 버전: Rails 8.1.3.1 (Ruby 3.4.10 + YJIT, Puma 6.6.1), Express 4.21.2 + Sequelize 6.37.5 (Node 24.20.0), Gin 1.10.1 + GORM 1.25.12 (Go 1.25.14) 환경: Docker Linux 컨테이너 + MySQL 8.4 · CPU 1개, 프로세스 1개, 스레드 1개, 동시 요청 8개 · 3회 측정의 중앙값

Claude Code와 함께 작성됨


Rails 성능 시리즈

  1. Rails는 느린가요? 같은 블로그 API를 8번 만들어 추상화 비용을 측정해 보았습니다
  2. Rails 요청 하나의 CPU는 어디에 쓰일까요? HTTP, 라우팅, DB, ORM으로 나눠 측정해 보았습니다 (지금 읽는 글)
  3. Active Record는 요청마다 무엇을 만드는가? Sequelize, GORM과 같은 방법으로 측정해 보았습니다