이번 주에도 Rails 생태계에서 “속도”와 “기본기의 힘”을 다시 생각하게 만드는 이야기들이 여럿 나왔어요. 실제 DB를 치는 대규모 테스트가 30초 만에 끝난 사례부터, RSpec에서 Minitest로 옮기며 14분짜리 테스트를 4초로 줄인 경험담, 그리고 Prometheus 하나 붙였을 뿐인데 Rails 응답 시간이 87%나 개선된 운영 사례까지요.
공통점이 있다면, 모두 새로운 마법 같은 도구보다는 Rails가 이미 제공하고 있는 기본 메커니즘과 관측성, 그리고 단순한 선택이 얼마나 큰 차이를 만들어내는지를 보여준다는 점이에요. “이 정도면 충분히 빠르지 않나?” 하고 넘어가던 지점들을 다시 한 번 점검해보게 만드는 이야기들이었어요.
바쁘게 개발하다 보면 테스트나 모니터링은 늘 뒤로 밀리기 쉬운데, 이번 소식들은 그 선택이 결국 개발 속도와 팀의 사고방식까지 좌우한다는 걸 잘 보여줘요.
🎧 10분 요약 오디오로 들어보시겠어요? → YouTube로 듣기
새로운 소식
DHH: “DB 때리는 3만 assert 테스트가 30초” — Rails 기본 트랜잭션 전략의 힘
최근 David Heinemeier Hansson가 인상적인 테스트 성능 사례를 공유했어요. Hey가 운영하는 대규모 SaaS 애플리케이션의 테스트 스위트가 무려 3만 개의 assertion을 포함하고 있음에도, 빠른 로컬 Linux 환경에서 약 30초 만에 실행된다는 내용이었어요. 특히 이 테스트들은 단순한 mock이나 in-memory 스토리지가 아니라 실제 데이터베이스를 직접 사용하고 있다는 점에서 많은 개발자들의 관심을 끌었어요.
https://x.com/dhh/status/2018989659481571424?s=20
이에 대해 한 개발자가 “이 정도 속도면 혹시 실제 DB가 아니라 더 가벼운 대안을 쓰는 게 아니냐”고 질문했는데, DHH의 답변은 명확했어요. 테스트는 실제 데이터베이스를 대상으로 실행되고 있고, Rails에서 기본으로 제공하는 테스트 전략을 그대로 사용하고 있다는 것이었어요. 구체적으로는 테스트 시작 시 fixtures를 한 번만 로드해두고, 각 테스트 케이스는 트랜잭션 안에서 실행한 뒤 롤백하는 방식이에요.
https://x.com/bushido_hk/status/2018991647456124988?s=20
https://x.com/dhh/status/2018993446837465358?s=20
이 사례가 흥미로운 이유는, 복잡한 커스텀 테스트 프레임워크나 특별한 인프라 없이도 Rails의 기본 메커니즘만 잘 활용하면 DB를 사용하는 테스트에서도 충분히 빠른 실행 속도를 낼 수 있다는 점을 다시 한 번 보여주기 때문이에요. “DB를 치는 테스트는 느릴 수밖에 없다”는 생각을 하고 있었다면, 지금 사용하는 Rails 테스트 설정을 다시 한 번 점검해볼 만한 계기가 되는 이야기예요.
This Week in Rails: 2026년 2월 첫째 주 — 조용하지만 알찬 개선들이 있었어요
이번 주에도 Ruby on Rails 팀이 This Week in Rails 최신 호를 공개했어요. Greg가 전한 이번 주 소식은 전반적으로는 비교적 조용한 한 주였지만, 실제 개발과 운영에서 바로 체감할 수 있는 개선들이 여럿 포함되어 있었어요.
먼저 Rails.app.revision이 애플리케이션의 리비전을 확인할 때, 기존처럼 REVISION 파일이나 git 정보만 보는 것이 아니라 ENV["REVISION"] 값이 있으면 이를 우선적으로 읽도록 개선됐어요. 컨테이너 환경이나 배포 파이프라인에서 리비전을 환경 변수로 주입해 사용하는 팀이라면 훨씬 자연스럽게 활용할 수 있는 변화예요.
개발 환경에서의 에러 페이지도 조금 더 친절해졌어요. 중첩된 스택 트레이스를 더 쉽게 탐색할 수 있도록, 에러 요약 영역 옆에 작은 화살표가 추가됐고 이를 클릭하면 전체 스택 트레이스를 펼쳐볼 수 있게 됐어요. 복잡한 에러를 추적할 때 한 단계씩 맥락을 파악하기가 훨씬 수월해질 것 같아요.
Active Record 쪽에서는 쿼리 로그 태그에 실제 SQL 쿼리가 전달되도록 변경됐어요. 쿼리 로그 태그를 활용해 디버깅이나 모니터링을 하고 있다면, 이제 SQL 자체를 기준으로 한 커스텀 태그 로직을 만들 수 있게 된 셈이에요. 성능 분석이나 로그 가독성을 높이는 데 꽤 유용한 변화예요.
또 하나 눈에 띄는 업데이트는 insert_all!에 unique_by 옵션이 추가된 점이에요. PostgreSQL과 SQLite에서 지원되며, 여러 유니크 인덱스가 있는 테이블에서도 특정 인덱스만 기준으로 중복 여부를 판단하도록 지정할 수 있어요. 이미 insert_all에는 존재하던 기능이지만, 이제 예외를 발생시키는 insert_all!에서도 동일한 제어가 가능해졌다는 점에서 실무 활용도가 높아졌어요.
이번 주에는 총 19명의 컨트리뷰터가 Rails 코드베이스에 기여했어요.
https://rubyonrails.org/2026/2/6/this-week-in-rails
14분이 4초로 — RSpec에서 Minitest로 옮긴 이야기
이번 주 Ruby Weekly에서 인상적인 테스트 마이그레이션 사례가 소개됐어요. 내부 Ruby on Rails 프로젝트인 Nebula에서 테스트 프레임워크를 RSpec과 FactoryBot 조합에서 Minitest와 Fixtures로 전환한 경험담인데, 결과만 보면 꽤 과감한 선택이었어요. 기존 테스트 스위트는 전체 실행에 약 14분이 걸렸고, 이 때문에 한 작업에 대해 시간당 네 번 정도밖에 피드백 사이클을 돌릴 수 없을 정도로 개발 흐름이 막혀 있었어요.
처음부터 프레임워크를 바꾼 건 아니었어요. 팀은 test-prof와 let_it_be 같은 도구를 활용해 Factory 오버헤드를 줄이는 데 많은 시간을 썼고, 그 결과 실행 시간을 약 2분 30초까지 줄이는 데는 성공했어요. 당시에는 큰 성과처럼 느껴졌지만, 이후 David Heinemeier Hansson가 HEY의 대규모 코드베이스 테스트를 1분대까지 줄였다는 글을 보고 나니, “아직 더 갈 수 있겠다”는 생각이 들었다고 해요. Nebula는 HEY보다 코드베이스도 훨씬 작았고, 그럼에도 테스트는 더 느렸다는 점이 결정적인 계기였어요.
마이그레이션은 한 번에 갈아엎는 방식이 아니라, 두 개의 테스트 스위트를 병행하는 전략으로 시작했어요. 규칙은 단순했는데, 새로운 테스트는 더 이상 RSpec에 추가하지 않고 반드시 Minitest로만 작성하는 거였어요. 그 과정에서 Factory는 점점 Fixture로 바뀌고, RSpec 테스트 일부가 Minitest로 옮겨지다가, 결국 남은 것들을 정리하는 단계까지 자연스럽게 이어졌다고 해요. 전환 기간 동안에는 두 테스트 스위트를 모두 실행하면서 커버리지를 유지했어요.
과정은 솔직히 쉽지 않았다고 해요. AI를 활용해 자동 변환을 시도했지만, RSpec 특유의 컨텍스트 상속이나 암묵적인 상태를 제대로 이해하지 못해 한계가 있었고, 결국 일정한 패턴을 잡은 뒤 일부 도움을 받는 수준에서, 상당 부분은 수작업으로 풀어내야 했다고 해요. 그래도 그 고생 끝에 나온 결과는 극적이었어요. 거의 동일한 테스트 스위트가 4초 만에 실행됐고, 그 사이 코드베이스는 오히려 2.6배나 커졌어요. 테스트 코드는 불필요한 의식적인 코드가 줄어들면서 26% 더 작아졌고요.
작성자는 RSpec의 DSL이 처음에는 매력적으로 느껴지지만, 지연 평가나 암묵적 subject, 복잡한 let 체인과 shared context가 쌓이면서 보이지 않는 비용을 만든다고 이야기해요. FactoryBot까지 더해지면 그 오버헤드는 더 커지고요. 반면 Minitest는 “그냥 Ruby”라서 디버깅도 쉽고, 테스트 코드가 실제 애플리케이션 코드와 비슷하게 읽히며, 기본적으로 멀티스레드를 활용하기 때문에 속도 면에서도 유리하다고 강조해요.
이 전환 이후에는 테스트 실행을 언제 할지 고민할 필요가 없어졌다고 해요. AI 에이전트가 테스트를 여러 번 돌려도 부담이 없고, 굳이 테스트를 스코프해서 실행할 이유도 사라졌어요. bin/rails test를 치는 데 걸리는 시간과, 테스트 파일 경로를 타이핑하는 시간이 비슷해졌기 때문이에요. 테스트는 동일한 머신에서 실행됐고, Minitest에서는 CPU 코어 수에 맞춰 병렬 실행을 사용했으며, 이를 끄더라도 실행 시간은 약 20초 수준이었다고 해요.
https://x.com/ryanrhughes/status/2019258699001294911
Rails 응답 시간을 87% 개선한 방법
최근 회고 도구 서비스 Fast Retro 팀이 Rails 애플리케이션의 응답 속도를 87% 개선한 경험을 공유했어요. 흥미로운 점은 거창한 리팩터링이나 아키텍처 변경이 아니라, **모니터링을 추가한 뒤 문제를 정확히 “보게 된 것”**이 모든 변화의 시작이었다는 점이에요. Prometheus와 Grafana를 붙인 지 몇 시간 만에 p95 응답 시간이 240ms에서 400ms 사이로 튀는 컨트롤러 세 개가 대시보드에서 바로 눈에 띄었고, 그 정체는 모두 익숙한 문제인 N+1 쿼리였어요.
관측 스택은 꽤 단순해요. Prometheus가 5초 주기로 메트릭을 수집하고, Grafana가 이를 시각화하며, Loki와 Promtail이 로그를 모으고, node exporter와 cAdvisor가 서버와 컨테이너 리소스를 추적해요. 이 모든 구성은 Kamal 2로 단일 서버에 배포됐고, Grafana는 설정 파일 기반으로 자동 프로비저닝돼서 웹 UI에서 따로 클릭할 일도 거의 없었다고 해요. 외부 노출은 하지 않고 Tailscale 안에서만 접근하도록 구성한 점도 인상적이에요.
Rails 쪽에서는 Yabeda 계열 젬을 사용해 컨트롤러 지연 시간, 요청 수, 상태 코드, ActiveJob, ActionCable 메트릭을 Prometheus 포맷으로 노출했어요. 이렇게 수집된 데이터를 Grafana에서 컨트롤러·액션 단위 p95 지연 시간으로 쪼개서 보니, “어디가 느린지”가 바로 드러났다고 해요. 앱은 겉보기엔 잘 동작하고 있었지만, 실제로는 수십 개의 작은 쿼리가 쌓이면서 200~300ms의 지연을 만들고 있었던 거예요.
첫 번째 문제는 토론 화면이었어요. 컨트롤러 코드는 거의 비어 있었지만, 컴포넌트 내부에서 feedback의 작성자, ActionText 콘텐츠, 투표 수, 그룹 정보를 렌더링하면서 eager loading 없이 연관 관계에 접근하고 있었어요. 피드백 20개만 있어도 80개 이상의 쿼리가 나가는 구조였고, includes를 추가하고 .count 대신 .size를 사용하면서 한 번에 해결됐어요. 두 번째는 회고 목록 페이지였는데, 카드마다 feedback 개수를 .count로 계산하면서 카드 수만큼 COUNT 쿼리가 나가고 있었어요. 이를 컨트롤러에서 GROUP BY로 한 번에 집계해 넘기는 방식으로 바꾸자 바로 정리됐어요. 세 번째는 투표 단계였는데, 버튼 하나마다 전체 투표 수와 사용자 투표 수를 계속 DB에서 다시 세고 있었고, 특히 “더 투표할 수 있는지”를 확인하는 동일한 COUNT 쿼리가 버튼마다 반복되고 있었어요. 참가자의 votes를 미리 로드한 뒤 Ruby 배열에서 필터링하도록 바꾸면서 쿼리 수를 수십 개에서 몇 개 수준으로 줄였어요.
이 세 가지 사례는 모두 같은 패턴을 보여줘요. 컴포넌트 하나만 보면 합리적으로 보이는 코드가, 루프 안에서 렌더링되면서 O(N) 쿼리 폭탄으로 변하고 있었던 거예요. 해결 방법도 일관돼요. 연관 관계를 미리 로드하고, .count 대신 .size를 쓰고, 숫자만 필요할 때는 DB에서 한 번에 집계하고, 이미 로드된 데이터는 Ruby에서 처리하는 거예요.
이 글에서 가장 인상적인 메시지는 “모니터링이 없었다면 이 문제들은 계속 모르고 지나갔을 것”이라는 점이에요. 기능적으로는 아무 문제 없어 보이는 앱이었지만, Prometheus 대시보드에 찍힌 p95 400ms 그래프 하나가 모든 걸 바꿨어요. Rails 앱을 운영하고 있으면서 아직 요청 단위 메트릭을 보고 있지 않다면, 이 사례는 왜 관측성이 필수인지 아주 설득력 있게 보여줘요. Yabeda 추가는 15분이면 끝나고, Prometheus와 Grafana 설정도 한 번 해두면 계속 써먹을 수 있으니까요.
https://fastretro.app/blog/how-we-improved-rails-response-times-by-87-percent