본문으로 건너뛰기

Rails 응답 속도를 87% 개선한 Prometheus 모니터링 및 N+1 쿼리 최적화 사례

We Improved Rails Response Times by 87% – Fast Retro Blog

작성자
HackerNews
발행일
2026년 02월 08일

핵심 요약

  • 1 Prometheus와 Grafana를 활용한 모니터링 스택을 구축하여 p95 응답 속도가 느린 컨트롤러를 즉각적으로 식별하고 성능 병목의 원인을 파악함.
  • 2 Rails 컴포넌트 내부 루프에서 발생하는 N+1 쿼리 문제를 .includes, .size, GROUP BY 배치 로딩 및 Ruby 필터링을 통해 효과적으로 해결함.
  • 3 모니터링 도구 없이는 발견하기 어려운 미세한 지연 시간을 시각화함으로써 실질적인 사용자 경험 향상과 시스템의 기술적 부채를 성공적으로 제거함.

도입

본 글은 Fast Retro 서비스에서 Rails 애플리케이션의 성능을 획기적으로 개선한 실전 사례를 다룹니다. 개발자는 Prometheus와 Grafana를 포함한 통합 모니터링 스택을 구축하여, 기존에 인지하지 못했던 p95 응답 속도 지연(240ms~400ms) 구간을 발견했습니다. 특히 Kamal 2와 Tailscale을 활용한 안전하고 자동화된 인프라 구성 위에서 Yabeda 젬을 통해 Rails 내부 메트릭을 수집함으로써, 성능 저하의 주범인 N+1 쿼리 문제들을 구체적으로 특정하고 해결하는 과정을 상세히 설명합니다. 이 과정은 데이터 기반의 의사결정이 서비스 품질에 미치는 영향을 잘 보여줍니다.

1. 모니터링 인프라 및 기술 스택 구성

성능 개선의 첫걸음은 정확한 상태 파악을 위한 관측성(Observability) 확보였습니다. Fast Retro는 다음과 같은 도구들을 단일 서버에 구축하고 Kamal 2를 통해 배포했습니다.

  • Prometheus: 5초 간격으로 메트릭을 수집하고 30일간의 데이터를 저장합니다.
  • Grafana: 수집된 데이터를 시각화하고 대시보드를 통해 실시간 모니터링을 수행합니다.
  • Loki & Promtail: 컨테이너 로그를 집계하고 분석합니다.
  • Yabeda Gems: Rails 내부의 컨트롤러 지연 시간, 요청 수, ActiveJob 및 ActionCable 메트릭을 Prometheus 형식으로 노출합니다.

특히 보안을 위해 모든 모니터링 스택은 Tailscale 네트워크 내에 배치되어 외부 노출 없이 안전하게 관리됩니다.

2. 세 가지 주요 성능 병목 현상 식별

Grafana 대시보드를 통해 p95 지연 시간이 높은 세 가지 컨트롤러 액션을 특정했습니다.

① 토론 단계(DiscussionsController#show) - 400ms

이 화면은 피드백 카드를 렌더링할 때 작성자 정보, ActionText 내용, 투표 수 등을 개별적으로 쿼리하고 있었습니다. 피드백이 20개일 경우 약 80개 이상의 쿼리가 발생하는 전형적인 N+1 문제였습니다. * 해결책: .includes(:user, :rich_text_content)를 사용하여 연관 관계를 사전 로딩하고, SQL COUNT(*)를 유발하는 .count 대신 메모리에 로드된 배열 크기를 확인하는 .size를 사용하도록 변경했습니다.

② 레트로 목록(RetrosController#index) - 360ms

각 레트로 카드마다 포함된 피드백 개수를 표시하기 위해 루프 내에서 개별 쿼리를 실행하고 있었습니다. * 해결책: 컨트롤러에서 GROUP BY를 사용하여 모든 레트로의 피드백 개수를 단일 쿼리로 가져온 뒤, 해시 형태로 뷰 컴포넌트에 전달하는 배치 로딩 방식을 도입했습니다.

③ 투표 단계(VotingsController#show) - 243ms

투표 버튼 컴포넌트가 렌더링될 때마다 사용자의 남은 투표 권한과 개별 아이템의 투표 수를 반복적으로 조회했습니다. 특히 can_add_vote? 메서드는 모든 버튼에서 동일한 쿼리를 중복 실행하고 있었습니다. * 해결책: 사용자의 투표 내역을 한 번에 사전 로딩한 후, 데이터베이스 재조회 없이 Ruby의 .select 메서드를 사용하여 메모리 내에서 필터링하도록 로직을 개선했습니다.

3. 최적화 패턴 및 교훈

발견된 모든 버그는 공통된 패턴을 따르고 있었습니다. 개별적으로는 합리적으로 보이는 컴포넌트 로직이 루프 내에서 반복 실행되면서 O(1) 작업이 O(N)으로 확장된 것입니다. 이를 해결하기 위해 다음과 같은 최적화 원칙을 적용했습니다.

  1. Eager Loading: 쿼리 수준에서 includes를 적극 활용하여 연관 데이터를 미리 가져옵니다.
  2. Collection Usage: preloaded 데이터를 활용하기 위해 .count 대신 .size를 사용합니다.
  3. Batch Loading: 집계 데이터가 필요한 경우 GROUP BY를 통해 한 번에 조회합니다.
  4. Ruby-side Filtering: 이미 로드된 컬렉션은 DB를 다시 호출하는 대신 Ruby 내부 로직으로 필터링합니다.

결론적으로, Prometheus와 같은 메트릭 도구는 개발 환경에서는 체감하기 어려운 생산 환경의 성능 병목을 가시화하여 개발자가 올바른 최적화 지점을 찾을 수 있도록 돕는 필수적인 도구임을 입증했습니다.

결론

이번 최적화 사례의 핵심은 '측정할 수 없으면 개선할 수 없다'는 원칙을 실천한 데 있습니다. Prometheus와 Yabeda를 활용한 짧은 설정만으로도 애플리케이션의 보이지 않는 병목 구간을 명확히 시각화할 수 있었습니다. 특히 컴포넌트 기반 아키텍처에서 무심코 작성된 쿼리가 루프 내에서 실행될 때 발생하는 O(N) 문제의 위험성을 경고하며, 적절한 사전 로딩(Eager Loading)과 배치 처리가 Rails 성능 최적화에 얼마나 결정적인 역할을 하는지 시사합니다. 결과적으로 응답 시간을 87% 단축하며 시스템의 효율성을 극대화하는 성과를 거두었습니다.

댓글 0

댓글 작성

댓글 삭제 시 비밀번호가 필요합니다.

이미 계정이 있으신가요? 로그인 후 댓글을 작성하세요.

0/1000
정중하고 건설적인 댓글을 작성해 주세요.