Active Record는 요청마다 무엇을 만드는가? Sequelize, GORM과 같은 방법으로 측정해 보았습니다
앞 글의 결론은 "루비는 느리지 않고 Rails 요청 비용의 대부분은 Active Record"였습니다. 그 Active Record가 요청마다 무엇을 하는지 소스 코드와 측정으로 Sequelize, GORM과 비교했습니다. 비용의 대부분은 has_and_belongs_to_many 조인 행을 전부 레코드로 만드는 preload 때문이었고, 같은 SQL을 유지한 채 비용을 크게 줄이는 방법도 찾았습니다.
1. 앞 글에서 이어지는 질문
앞 글은 같은 블로그 API를 4개 언어로 두 번씩 만들어 비교했습니다. 결론을 한 줄로 줄이면 이렇습니다. 루비 자체는 느리지 않고, Rails 요청 비용의 대부분은 Active Record입니다. 마법을 뺀 Ruby(Rack + 직접 SQL)는 요청당 CPU 0.145ms로 Express+Sequelize(0.567ms)보다 쌌고 Rails 요청 하나를 계층별로 나눠 보면 프레임워크가 11.4%, 나머지 88.6%가 ORM이었습니다.
그러면 궁금해집니다. Active Record는 요청마다 무엇을 하길래 이렇게 오래 걸릴까요? 같은 일을 하는데 Node와 Go의 ORM은 왜 더 빠를까요?
"ORM은 원래 느리다"로는 답이 되지 않습니다. 세 ORM 모두 ORM이고 같은 SQL을 실행합니다. 이번 글에서는 그 차이를 두 가지 방법으로 나눠 살펴봅니다. 하나는 같은 조건에서 단계별로 재는 것이고, 다른 하나는 세 라이브러리의 소스 코드를 설치된 버전 그대로 읽는 것입니다.
2. 세 ORM을 같은 방법으로 측정했습니다
앞 글에서 Active Record를 네 단계로 나눠 쟀던 방법을 Sequelize와 GORM에도 똑같이 적용했습니다. 한 요청이 하는 일은 목록 API 그대로입니다. 게시글 20개, 작성자, 태그 조인 행, 태그를 읽는 SQL 4문장입니다.
| 단계 | 하는 일 |
|---|---|
| ① 드라이버 직접 | bare 앱과 같은 방식. SQL을 직접 쓰고 prepared statement로 실행한 뒤 결과를 손으로 조립 (Node는 prepared statement 없이 bare 앱과 동일) |
| ② ORM으로 쿼리, 모델 없음 | ORM의 쿼리 빌더로 같은 4문장을 만들고 실행. 모델 객체는 만들지 않음 |
| ③ 모델 생성 | ORM으로 모델 객체를 만듦(Active Record와 GORM은 preload, Sequelize는 앱처럼 모델별 findAll 4번). 속성은 읽지 않음 |
| ④ + 직렬화 | 모든 속성을 읽어 같은 JSON을 만듦 |
ORM별로 ② 단계는 이렇게 구현했습니다. Active Record는 pluck, Sequelize는 raw: true, GORM은 빌더 API로 SQL을 만든 뒤 Rows()로 받아 직접 Scan합니다. 세 경우 모두 ORM이 SQL을 조립하고 실행하지만 결과를 모델로 바꾸지는 않습니다.
공정성을 위해 네 가지 규칙을 준수했습니다.
- 단계마다 같은 SQL 4문장을 실행합니다. ②와 ④가 만든 JSON이 ①의 JSON과 바이트 단위로 같은지 측정을 시작하기 전에 검사했고 세 번의 측정 모두 같았습니다.
- 각 런타임은 자기 앱의 Docker 이미지 안에서 돌렸습니다. 드라이버와 ORM 버전, GORM의
PrepareStmt: true같은 설정이 실제 앱과 같습니다. - 시간은 요청당 프로세스 CPU 시간입니다. 앞 글의 비교 기준이 요청당 CPU였기 때문입니다. 또 Go와 Node는 GC의 일부를 다른 스레드에서 돌리는데, 벽시계 시간으로 재면 이 시간이 빠집니다. 워밍업 20회 뒤 40회씩 6묶음을 돌려 가장 작은 값을 쓰고 이것을 3회 반복해 중앙값을 적었습니다.
- 할당량은 런타임마다 단위가 다릅니다. Ruby는 객체 수, Go는 malloc 횟수, Node는 힙 바이트입니다. 런타임끼리 비교할 수 없는 값이라 런타임 안에서 단계 사이 증가량만 봅니다.
3. 결과: 어디서 비용이 드는가
| 요청당 CPU (3회 중앙값) | Active Record | Sequelize | GORM |
|---|---|---|---|
| ① 드라이버 직접 | 0.106 | 0.235 | 0.116 |
| ② ORM으로 쿼리, 모델 없음 | 0.290 | 0.407 | 0.128 |
| ③ 모델 생성 | 0.601 | 0.429 | 0.186 |
| ④ + 직렬화 | 0.725 | 0.528 | 0.227 |
단위는 ms입니다. 단계 사이의 차이를 층별 비용으로 보면 다음과 같습니다.
| 층 | Active Record | Sequelize | GORM |
|---|---|---|---|
| 쿼리 계층 (①→②) | +0.183 ms | +0.172 ms | +0.012 ms |
| 모델 생성 (②→③) | +0.311 ms | +0.022 ms | +0.058 ms |
| 속성 읽기 (③→④) | +0.124 ms | +0.098 ms | +0.041 ms |
| 모델 계층, 행당 (②→④를 20으로 나눔) | 21.8 µs | 6.0 µs | 5.0 µs |
| ORM | 드라이버 (① 기준) | 쿼리 계층 (①→②) | 모델 생성 (②→③) | 속성 읽기 (③→④) | 합계 |
|---|---|---|---|---|---|
| Active Record | 0.106 | 0.183 | 0.311 | 0.124 | 0.725 |
| Sequelize | 0.235 | 0.172 | 0.022 | 0.098 | 0.528 |
| GORM | 0.116 | 0.012 | 0.058 | 0.041 | 0.227 |
세 가지가 눈에 들어옵니다.
첫째, 쿼리 계층 비용은 Active Record와 Sequelize가 거의 같습니다. SQL 한 문장당 약 45µs입니다. GORM은 3µs입니다. 이 층에는 SQL을 만드는 일과 함께 결과를 받아 값으로 바꾸는 일도 들어 있습니다.
둘째, Active Record만 모델 생성 단계에서 비용이 크게 늘어납니다. Sequelize는 모델을 만들어도 0.022ms밖에 늘지 않는데 Active Record는 0.311ms가 늡니다. 이 글은 대부분 이 한 줄을 설명하는 내용입니다.
셋째, 기본 비용이 다릅니다. 드라이버만 쓰는 ① 단계에서 Node의 비용이 Ruby와 Go의 두 배를 넘습니다. Ruby의 mysql2는 C 확장으로, Go의 드라이버는 컴파일된 Go 코드로 결과를 해석하지만 Node의 mysql2는 자바스크립트로 해석합니다. 앞 글에서 마법을 뺀 Ruby가 Node보다 쌌던 이유 중 하나가 여기 있습니다.
4. 쿼리를 만드는 비용
② 단계에서는 세 ORM 모두 SQL을 매 요청마다 새로 만듭니다. 그런데도 비용이 열 배 넘게 차이 납니다. SQL을 만드는 방식과 결과를 받는 방식이 다르기 때문입니다.
Active Record는 체인의 메서드마다 Relation을 복제합니다. Post.published.order(...).limit(20).offset(40).includes(...)의 각 호출이 spawn으로 새 Relation을 만들고(relation/spawn_methods.rb:9-11), 이 체인 하나에 Relation 6개가 생깁니다. Relation을 불변으로 다루기 위한 설계입니다. 그다음 Arel 구문 트리를 만들고 방문자가 이를 SQL 문자열로 바꿉니다. 이 결과는 캐시되지 않습니다. 컴파일된 SQL을 재사용하는 StatementCache는 find와 find_by에만 쓰입니다(core.rb:404-444). 바인드 값도 객체가 됩니다. IN (...)에 들어가는 id 하나하나가 SQL을 만드는 순간 ActiveModel::Attribute 객체로 바뀝니다(arel/nodes/homogeneous_in.rb:50-52). status, LIMIT, OFFSET 값까지 합치면 이 요청에서만 바인드 값 객체가 74개 생깁니다.
Sequelize는 옵션 객체를 깊게 복사하고 빈 훅을 기다립니다. findAll 한 번에 옵션, 기본 스코프, where를 합쳐 서너 번 깊은 복사를 합니다(model.js:1102, model.js:2052, utils.js:158). 등록된 훅이 하나도 없어도 모델 훅 4개와 인스턴스 훅 4개를 차례로 await하므로 쿼리마다 Promise가 8개 생깁니다(hooks.js:67-98). 쿼리마다 uuid()를 하나 만들고(abstract/query.js:27), 스택 추적을 담은 new Error()를 하나 만듭니다(mysql/query.js:52). 결과를 받을 때는 셀마다 Sequelize의 typeCast 콜백이 호출됩니다(mysql/connection-manager.js:54). DATETIME은 문자열로 받아 new Date()로 다시 해석합니다(mysql/data-types.js:54-65).
GORM도 매번 SQL을 새로 만들지만, 만드는 과정이 가볍습니다. 체인이 시작될 때 DB와 Statement를 하나씩 만들고 이후 호출은 그것을 재사용합니다(gorm.go:405-432). SQL은 strings.Builder에 바로 씁니다(statement.go:469-486). 구조체의 스키마는 타입마다 한 번만 해석해 sync.Map에 둡니다(schema/schema.go:150-163). AfterFind 같은 훅이 있는지도 스키마를 해석할 때 한 번 확인해 bool로 저장하므로, 쿼리마다 드는 비용은 bool 검사 하나입니다(schema/schema.go:306-322).
정리하면 Active Record와 Sequelize는 쿼리 한 문장을 만드는 데 객체를 수십, 많게는 백 개 넘게 만드는 쪽이고 GORM은 재사용과 정적 타입으로 그 일을 줄인 쪽입니다. GC가 있는 동적 언어에서 요청마다 객체를 수십 개 만드는 비용은 작지 않습니다.
5. 모델을 만드는 비용의 원인은 조인 행이었습니다
Active Record의 모델 생성 단계가 왜 크게 늘어나는지 보려고, 게시글과 함께 불러오는(preload) 대상을 하나씩 바꿔 가며 쟀습니다. SQL을 실행하고 모델을 만드는 데까지의 비용입니다.
| 같은 게시글 쿼리에서 | 요청당 CPU | 만들어진 레코드 |
|---|---|---|
| 게시글만 | 0.065 ms | 20 |
+ includes(:user) | 0.189 ms | 40 |
+ includes(:tags)만 | 0.492 ms | 117 |
+ includes(:user, :tags) | 0.608 ms | 137 |
게시글 20개만 만들면 쿼리까지 합쳐 0.065ms입니다. 비용은 태그를 함께 불러올 때 크게 늘어납니다. 그 이유는 레코드 수가 20에서 137로 늘어났기 때문입니다.
게시글과 태그 사이에는 둘을 잇는 조인 행이 있습니다. 게시글 하나에 태그가 여러 개 달리고 태그 하나도 여러 게시글에 달리므로, post_tags 테이블이 "몇 번 게시글에 몇 번 태그"라는 짝을 한 줄씩 저장합니다. 이번 요청에서는 이 조인 행이 66개입니다.
Rails에서 이 관계를 has_and_belongs_to_many로 선언하면, Active Record는 태그를 불러올 때 조인 행 66개도 하나하나 완전한 레코드로 만듭니다. 숫자 세 개(id, post_id, tag_id)만 든 행이 게시글과 똑같은 레코드 객체가 되는 셈입니다. 게다가 게시글과 조인 행에는 레코드마다 연관관계를 담는 객체가 따로 붙습니다. 그래서 게시글 20개를 보여 주려고 레코드를 137개나 만든 것입니다.
| 불러온 대상 | 게시글 | 작성자 | 조인 행 (post_tags) | 태그 | 합계 |
|---|---|---|---|---|---|
| 게시글만 (0.065 ms) | 20 | 0 | 0 | 0 | 20 |
| + user (0.189 ms) | 20 | 20 | 0 | 0 | 40 |
| + tags (0.492 ms) | 20 | 0 | 66 | 31 | 117 |
| + user, tags (0.608 ms) | 20 | 20 | 66 | 31 | 137 |
반대로 레코드 하나를 만드는 일 자체는 가볍습니다. 게시글만 불러올 때는 쿼리까지 합쳐 20개를 만드는 데 0.065ms밖에 걸리지 않았습니다. 비용이 커진 이유는 레코드가 무거워서가 아니라 많아서입니다.
앞 글에서 "행 하나당 객체 121개"라고 적었던 수치도 이렇게 고쳐 읽어야 합니다. 게시글 한 행이 무거운 것이 아니라, 게시글 한 행을 보여 주려고 조인 레코드와 연관관계 객체까지 함께 만들어졌기 때문입니다.
Sequelize와 GORM도 조인 행을 읽습니다. 다만 Sequelize는 조인 행 인스턴스에 연관관계 객체를 따로 붙이지 않고, GORM은 조인 행으로 짝만 맞춘 뒤 태그를 구조체에 바로 넣습니다. 조인 행마다 연관관계 객체까지 붙이는 것은 셋 중 Active Record뿐입니다.
6. 속성을 읽는 비용과 세 가지 설계
속성을 읽는 ③→④ 단계는 세 ORM의 설계 철학이 가장 뚜렷하게 갈리는 곳입니다.
Active Record는 변환을 미뤘다가 한 번만 합니다. 레코드를 만들 때는 드라이버가 준 값을 그대로 들고 있다가, 속성을 처음 읽을 때 타입에 맞게 변환하고 그 결과를 @casted_values에 저장합니다(activemodel/attribute_set/builder.rb:41-58). 두 번째 읽기는 아무것도 만들지 않습니다. 다만 처음 읽을 때의 비용은 타입마다 다릅니다.
| 타입 | 처음 읽을 때 | 이유 |
|---|---|---|
| 정수 | 객체 0개 | 드라이버가 준 Integer를 그대로 반환 |
| 문자열 | 문자열 복사 | 제자리 수정을 감지하려고 String.new(value)로 복사 (activemodel/type/string.rb:37-39) |
| 날짜시간 | 객체 약 8개 | 드라이버가 이미 준 UTC Time을 TimeWithZone으로 감쌈 (activesupport/core_ext/date_and_time/zones.rb:20-38) |
이 앱은 prepared statement를 켜 두어 mysql2가 바이너리 프로토콜을 쓰므로, 정수와 시각은 드라이버가 이미 Ruby 객체로 바꿔 줍니다. Active Record는 그 위에서 문자열은 복사하고 시각은 시간대 객체로 한 번 더 감쌉니다. 직렬화할 때 게시글마다 문자열 속성 세 개(title, slug, body)와 시각 속성 하나(published_at)를 읽으니 이 비용이 행마다 붙습니다.
Sequelize는 읽기에서는 아무것도 하지 않습니다. 변환은 결과를 받을 때 셀마다 이미 끝났고(4장의 typeCast), post.title은 프로토타입에 한 번 정의된 getter가 dataValues.title을 돌려줄 뿐입니다(model.js:2160-2179). 대신 변환은 결과를 받을 때 미리 끝내고 레코드를 만들 때는 값을 통째로 한 번 더 복사해 둡니다. 이 복사 이야기는 다음 장에서 이어집니다.
GORM은 속성을 읽을 때 따로 하는 일이 없습니다. p.Title은 구조체 필드를 읽는 기계어 한 줄입니다. 값은 Scan할 때 이미 필드에 들어가 있습니다. GORM의 비용은 그 Scan에 있습니다. 행마다 reflect.New로 구조체를 힙에 만들고(scan.go:319), 컬럼마다 sync.Pool에서 꺼낸 이중 포인터 홀더로 값을 받습니다. 이 경로에서는 database/sql이 NULL이 아닌 값마다 메모리를 새로 할당합니다(database/sql/convert.go:436).
컬럼 수를 늘려 가며 재면 이 차이를 숫자로 확인할 수 있습니다. 같은 20행에서 선택하는 컬럼만 2개에서 11개로 늘렸습니다.
| 컬럼이 하나 늘 때, 행당 | Active Record | Sequelize | GORM |
|---|---|---|---|
| 추가 CPU | 0.43 µs | 0.41 µs | 0.13 µs |
컬럼당 비용은 Active Record와 Sequelize가 거의 같습니다. 동적 언어에서 값 하나를 들여오는 비용은 두 런타임이 비슷하다는 뜻입니다. 그러니 Active Record의 모델 계층이 Sequelize의 3.6배인 이유는 컬럼 수에 비례하는 비용이 아니라 레코드와 연관관계마다 붙는 고정비에서 찾아야 합니다. 5장에서 본 것이 바로 그 고정비입니다.
7. 통념 몇 가지를 바로잡습니다
코드를 읽으면서 흔히 듣는 설명 몇 가지가 이 버전에서는 맞지 않는다는 것을 확인했습니다.
"Active Record는 dirty tracking 때문에 느리다." 적어도 읽기 경로에서는 아닙니다. 레코드를 로드할 때 원래 값을 따로 복사하지 않습니다. 원래 값은 드라이버가 준 행에 그대로 남아 있고, 변경 추적 객체는 changed? 같은 메서드를 처음 부를 때 만들어집니다(activemodel/dirty.rb:382-388). 직렬화만 하는 요청에는 이 비용이 전혀 들지 않습니다. 오히려 Sequelize가 로드 즉시 모든 행의 값을 통째로 복사합니다(_previousDataValues = {...dataValues}, model.js:2245). 한 번도 수정하지 않을 레코드도 마찬가지입니다.
"pluck은 타입 변환을 건너뛰어서 빠르다." 아닙니다. pluck은 선택한 셀을 전부 즉시 변환합니다. 게시글 쿼리 하나만 보면 pluck이 to_a보다 객체를 더 많이 만들었습니다(484개 대 427개). pluck이 실제로 건너뛰는 것은 레코드 껍데기와 preload·연관관계 처리 전체이고, ② 단계와 ③ 단계의 차이도 대부분 이 때문입니다.
"Sequelize의 raw: true는 모델을 안 만드니 훨씬 빠르다." 이 측정에서는 차이가 거의 없었습니다(②와 ③이 0.407ms와 0.429ms). raw: true가 건너뛰는 것은 인스턴스뿐입니다. 옵션 복사, 빈 훅 8개, 셀마다 도는 typeCast는 그대로 남고 행도 한 번 복사합니다(abstract/query.js:197-209). Sequelize의 비용은 대부분 모델을 만들기 전 단계에서 듭니다.
"ORM은 SQL을 캐시하니 쿼리 빌드 비용은 무시해도 된다." 세 ORM 모두 목록 쿼리 같은 체인형 쿼리의 SQL을 매번 새로 만듭니다. 캐시되는 것은 GORM과 Active Record의 prepared statement인데, 이것은 DB 서버 쪽 준비 비용을 아낄 뿐 SQL 문자열을 만드는 비용은 그대로입니다. Sequelize는 prepared statement도 쓰지 않고 값을 SQL 문자열에 직접 이스케이프해 넣습니다(mysql/query.js:59-61).
8. 같은 SQL, 같은 JSON으로 41% 줄이기
5장의 분석이 맞다면 해결책은 간단합니다. 조인 행을 레코드로 만들지 않으면 됩니다. SQL 4문장과 JSON은 그대로 두고 태그만 이렇게 바꿨습니다.
posts = scope.includes(:user).to_a
links = PostTag.where(post_id: posts.map(&:id)).pluck(:post_id, :tag_id)
tags = Tag.where(id: links.map(&:last).uniq).index_by(&:id)
게시글, 작성자, 태그는 여전히 Active Record 모델입니다. 조인 행만 [post_id, tag_id] 숫자 쌍으로 받습니다. 실행되는 SQL을 세어 보니 두 방식 모두 4문장이었고 만들어진 JSON은 바이트 단위로 같았습니다.
| 목록 API 한 번, SQL 실행부터 직렬화까지 | 요청당 CPU | 할당 객체 |
|---|---|---|
includes(:user, :tags) | 0.747 ms | 3,896 |
조인 행만 pluck | 0.442 ms | 2,219 |
요청당 CPU가 41% 줄었습니다. 이 값은 같은 일을 하는 Sequelize의 ④ 단계(0.528ms)보다 작습니다. 코드는 몇 줄 늘었을 뿐이고 모델도 버리지 않았습니다.
이 결과를 일반화하면 이렇습니다. Active Record 코드가 느릴 때 먼저 볼 것은 행 수가 아니라 만들어지는 레코드 수입니다. instantiation.active_record 이벤트를 구독하면 쿼리마다 레코드가 몇 개 만들어졌는지 셀 수 있습니다. 화면에 게시글 20개를 보여 주는데 레코드가 137개 만들어진다면, 그 차이는 대개 preload가 함께 불러온 조인 행과 중간 모델입니다. has_and_belongs_to_many와 has_many :through가 대표적인 경우입니다.
9. 이 측정의 한계
프로세스 안의 마이크로벤치마크입니다. HTTP, 라우팅, 미들웨어는 빠져 있습니다. 앞 글의 엔드포인트 측정(Rails 1.113ms, Express+Sequelize 0.567ms, Gin+GORM 0.277ms)과 절대값이 다릅니다. 층 사이의 비율과 경향을 보기 위한 측정입니다.
Active Record의 쿼리 캐시가 꺼진 상태입니다. 하네스는 Rails의 executor 밖에서 돌았습니다. 실제 요청은 executor 안에서 쿼리 캐시가 켜지고, 그러면 문장마다 결과를 한 번 더 복사합니다(abstract/query_cache.rb:263-325). 실제 요청에서 Active Record는 이 표보다 시간이 조금 더 걸립니다.
③ 단계는 ② 단계보다 컬럼을 더 읽습니다. 모델을 만드는 Active Record와 GORM은 기본으로 SELECT *로 조회해 게시글 11컬럼, 작성자 6컬럼, 조인 행 3컬럼을 읽고 ② 단계는 필요한 8컬럼, 2컬럼, 2컬럼만 읽습니다. Sequelize는 작성자에서 id와 name만 읽습니다. 늘어난 셀은 게시글 60개, 작성자 80개, 조인 행 66개입니다. 6장의 컬럼당 비용(속성을 읽을 때까지 포함한 값)으로 어림해도 Active Record의 ②→③ 증가분 0.311ms 중 이 차이 때문에 늘어난 시간은 많아야 0.09ms입니다. ③ 단계에서는 속성을 읽지 않으므로 실제로는 이보다 작고 이 차이를 빼도 5장의 설명은 달라지지 않습니다.
할당량은 런타임끼리 비교할 수 없습니다. Ruby 객체 하나와 Go의 malloc 한 번과 V8 힙의 바이트는 같은 단위가 아닙니다. 그래서 표에는 시간만 나란히 놓았습니다.
버전을 고정한 결과입니다. Active Record 8.1.3.1, Sequelize 6.37.5, GORM 1.25.12의 설치본을 읽었고 인용한 행 번호도 그 버전 기준입니다. 다른 버전에서는 구현이 다를 수 있습니다.
정리하면, Active Record가 비싼 이유는 모델을 만드는 단계에 있습니다. Sequelize와 비교하면 쿼리 계층과 속성 읽기 비용은 거의 같고, 모델 생성 단계의 차이(0.289ms)가 요청 전체의 차이(0.197ms)보다 큽니다. GORM과의 차이도 절반이 이 단계입니다. 그리고 그 모델 생성 비용은 레코드 하나가 무거워서가 아니라, preload가 조인 행까지 전부 레코드로 만들고 게시글과 조인 행마다 연관관계 객체를 붙이기 때문입니다. 그러니 Active Record를 쓰면서 비용을 줄이는 첫 수는 ORM을 버리는 것이 아니라, 만들어지는 레코드를 세어 보는 것입니다.
측정 일자: 2026-09-29 코드·원본 결과: https://github.com/MartianLee/study-rails-compare (
harness/orm_anatomy.py,docs/orm-anatomy.md) 버전: Active Record 8.1.3.1 (Ruby 3.4.10 + YJIT), Sequelize 6.37.5 (Node 24.20.0), GORM 1.25.12 (Go 1.25.14) 환경: Docker Linux 컨테이너 + MySQL 8.4 · 3회 측정의 중앙값 코드 인용: 각 라이브러리의lib/아래 경로와 행 번호입니다. 접두어가 없는 Rails 경로는activerecord/lib/active_record/기준이고, Active Model과 Active Support는activemodel/,activesupport/를 붙였습니다.
Claude Code와 함께 작성됨
Rails 성능 시리즈
- Rails는 느린가요? 같은 블로그 API를 8번 만들어 추상화 비용을 측정해 보았습니다
- Rails 요청 하나의 CPU는 어디에 쓰일까요? HTTP, 라우팅, DB, ORM으로 나눠 측정해 보았습니다
- Active Record는 요청마다 무엇을 만드는가? Sequelize, GORM과 같은 방법으로 측정해 보았습니다 (지금 읽는 글)