Ruby on Rails 환경에서 GraphQL API의 N+1 문제 해결: 심층 분석

Stefan Kanev – The Joy and Agony of GraphQL

작성자
Balkan Ruby
발행일
2025년 05월 05일

핵심 요약

  • 1 본 강연은 Ruby on Rails 환경에서 GraphQL API를 구축할 때 발생하는 N+1 쿼리 문제를 심층적으로 분석하고 해결 방안을 모색합니다.
  • 2 GraphQL Batch, Lazy Resolve, 그리고 Ruby Fibers를 활용한 최신 Data Loader (`data_load_record`, `data_load_association`) 등 다양한 접근법과 그 한계를 설명합니다.
  • 3 궁극적으로 Rails의 `preload` 기능을 GraphQL 타입 메타데이터와 통합하여 N+1 문제를 근본적으로 해결하는 이상적인 방안을 제시합니다.

도입

본 강연은 Dex의 CTO인 Stefan이 Ruby on Rails 환경에서 GraphQL API를 구현할 때 직면하는 복잡성과 주요 과제인 N+1 쿼리 문제를 다룹니다. Stefan은 20년 이상의 소프트웨어 개발 경험을 바탕으로, 특히 복잡한 정보 시스템에서 GraphQL을 사용하며 얻은 실질적인 통찰력을 공유합니다. 그는 GraphQL을 '양파'에 비유하며, 처음에는 매력적이지만 깊이 들어갈수록 여러 겹의 레이어를 벗겨내야 하고 때로는 어려움(눈물)을 동반할 수 있음을 강조합니다. 이 강연의 목표는 GraphQL, 특히 서버 측 구현에서 N+1 문제를 효과적으로 해결하고, 더 나은 개발 경험을 위한 방법을 제시하는 것입니다.

Stefan은 먼저 GraphQL의 기본적인 개념을 설명합니다. GraphQL은 REST API의 대안으로, 단일 엔드포인트를 통해 HTTP 기반으로 JSON 응답을 제공합니다. 클라이언트가 필요한 데이터를 직접 명시하여 오버페칭(over-fetching)이나 언더페칭(under-fetching)을 방지할 수 있으며, 강력한 타입 시스템을 특징으로 합니다. GraphQL은 크게 쿼리(데이터 조회), 뮤테이션(데이터 변경), 서브스크립션(실시간 데이터)으로 구성되지만, 복잡성의 핵심은 데이터 조회 방식인 쿼리에 있다고 설명합니다.

강연의 핵심은 GraphQL 환경에서 Active Record를 사용할 때 발생하는 N+1 쿼리 문제입니다. 이는 하나의 쿼리로 데이터를 가져온 후, 해당 데이터의 각 항목에 대해 연관된 데이터를 가져오기 위해 추가적인 쿼리가 N번 발생하는 현상을 의미합니다. 예를 들어, Pull Request의 댓글 목록을 가져올 때, 각 댓글의 작성자 정보를 가져오기 위해 댓글 수만큼 사용자 쿼리가 별도로 실행되는 상황을 시연합니다.

이러한 N+1 문제를 해결하기 위한 여러 접근법을 소개합니다. 첫 번째는 Shopify에서 개발한 GraphQL Batch Gem입니다. 이 Gem은 RecordLoaderAssociationLoader를 제공하여 쿼리를 일괄 처리하지만, Promise 기반의 비동기 처리 방식 때문에 Ruby 코드 내에서 복잡한 then 체인을 사용해야 하는 단점이 있습니다. Stefan은 GraphQL Batch의 이러한 복잡성과 한계를 지적하며 개인적으로 선호하지 않는다고 밝힙니다.

두 번째이자 최근 주목받는 해결책은 GraphQL Ruby Gem에 내장된 data_load_recorddata_load_association 메서드입니다. 이 메서드들은 Ruby의 ‘Fibers’ 기능을 활용하여 N+1 문제를 해결합니다. Fibers는 함수 실행을 일시 중단하고 나중에 다시 시작할 수 있는 경량 스레드와 유사한 개념으로, GraphQL 쿼리 실행 중 필요한 데이터를 수집한 후 한 번에 배치 쿼리를 실행하여 N+1을 방지합니다. 이 방식은 GraphQL Batch보다 코드 가독성이 좋지만, 예외 처리 시 스택 트레이스가 복잡해지는 등 여전히 난이도가 있다고 언급합니다.

Stefan은 이러한 기존 해결책들의 한계를 넘어, Rails의 강력한 preload 기능을 GraphQL 스키마 메타데이터와 결합하는 이상적인 해결책을 제안합니다. 그는 쿼리 요청만으로 필요한 모든 연관 관계를 미리 파악하고 ActiveRecord::Associations::Preloader와 같은 Rails의 내부 API를 사용하여 효율적으로 데이터를 로드하는 방안을 설명합니다. 이는 250줄 정도의 코드로 구현 가능하며, Fibers나 Promise 없이도 N+1 문제를 해결할 수 있는 더 직관적이고 Rails 친화적인 방식이라고 강조합니다.

또한, has_many 관계에서 발생하는 N+1 문제와 페이징(pagination) 처리의 어려움을 다룹니다. 특히, LIMIT 절이 적용된 has_many 관계에서 효율적인 단일 쿼리 실행이 어렵다는 점을 지적하며, PostgreSQL의 LATERAL JOIN을 활용한 고급 쿼리 기법을 해결책으로 제시합니다. 이는 각 그룹(예: Pull Request)별로 정렬 및 제한을 적용하면서도 전체 N+1을 방지할 수 있는 강력한 방법입니다.

결론

결론적으로 Stefan은 GraphQL 사용 여부는 프로젝트의 특성과 요구사항에 따라 달라진다고 조언합니다. 복잡한 정보 시스템이나 여러 클라이언트(React, iOS, Android 등)를 지원해야 하는 경우 GraphQL이 매우 유용하지만, Hotwire나 React Server Components와 같은 대안이 더 적합한 경우도 있음을 인정합니다. 그는 GraphQL이 '양파 수프'와 같아서, 해결해야 할 복잡한 문제가 많고 이를 즐기는 개발자에게는 큰 만족감을 줄 수 있지만, 그렇지 않다면 고통스러울 수 있다고 비유합니다. 궁극적으로 N+1 문제와 같은 복잡한 도구의 한계에 직면했을 때, 기존 방식에 안주하지 않고 더 나은 해결책을 탐구하는 개발자의 적극적인 자세가 중요하다고 강조하며 강연을 마무리합니다. 그는 자신이 제안한 Rails `preload` 기반의 해결책을 오픈 소스화할 의향이 있음을 밝히며, 비슷한 문제에 직면한 개발자들과의 협업을 독려합니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!