안녕하세요! 오늘은 GraphQL을 운영하면서 거의 모든 개발자가 한 번쯤은 정면으로 부딪히는 N+1 문제, 그리고 그걸 깔끔하게 해결해주는 DataLoader에 대해 이야기해보려고 합니다. 솔직히 말하면 저도 처음엔 "쿼리 몇 개 더 나가는 게 뭐 그리 대수야?"라고 생각했는데, 실제 서비스 트래픽이 붙으니 DB가 비명을 지르더라고요. 그 경험을 바탕으로 최대한 현실적으로 풀어보겠습니다.

목차

N+1 문제가 대체 뭔가요?

GraphQL을 쓰다 보면 클라이언트가 원하는 데이터를 중첩해서 요청하는 일이 정말 흔합니다. 게시글 목록을 가져오면서 각 게시글의 작성자 정보까지 한 번에 달라고 하는 식이죠. 문제는 이게 서버 입장에서 생각보다 무서운 패턴이라는 겁니다.

N번의 추가 쿼리가 발생하는 순간

게시글 10개를 조회하는 쿼리 1번이 나갑니다(이게 1). 그런데 각 게시글의 작성자를 가져오기 위해 작성자 조회 쿼리가 게시글 수만큼, 즉 10번 더 나갑니다(이게 N). 합쳐서 1+N, 그래서 N+1 문제라고 부릅니다. 게시글이 100개면 101번, 1000개면 1001번의 쿼리가 DB로 날아갑니다. 저는 이걸 처음 모니터링 툴에서 봤을 때 정말 식은땀이 났습니다.

resolver 구조가 만드는 함정

GraphQL의 resolver는 필드 단위로 독립적으로 실행됩니다. 이게 GraphQL의 강력한 유연성을 만들어주는 핵심이지만, 동시에 N+1 문제의 근본 원인이기도 합니다. author 필드 resolver는 자신이 몇 번 호출되는지 모른 채 매번 정직하게 DB를 조회하거든요. 코드만 봐서는 문제가 전혀 안 보이는데 런타임에 폭발한다는 게 가장 까다로운 부분입니다.

DataLoader는 어떻게 N+1을 해결할까?

Facebook이 만든 DataLoader는 바로 이 N+1 문제를 해결하기 위해 탄생한 라이브러리입니다. 핵심 아이디어는 의외로 단순한데, "여러 개의 개별 요청을 모아서 한 방에 처리하자"는 겁니다.

배칭(Batching)의 마법

DataLoader는 한 번의 이벤트 루프 틱(tick) 안에서 들어온 개별 키 요청들을 모읍니다. 작성자 ID가 1, 2, 3... 10번 들어오면 이걸 즉시 실행하지 않고 잠깐 모았다가 WHERE id IN (1,2,3,...,10) 같은 단일 쿼리로 한 번에 처리합니다. 10번 나갈 쿼리가 1번으로 줄어드니, N+1이 1+1로 압축되는 셈이죠. 처음 이걸 적용했을 때 DB 쿼리 수가 90% 넘게 줄어드는 걸 보고 감탄했던 기억이 납니다.

캐싱(Caching)으로 중복 제거

DataLoader는 같은 요청(request) 안에서 동일한 키로 들어온 조회를 캐싱합니다. 같은 작성자가 여러 게시글에 등장해도 DB는 딱 한 번만 조회합니다. 다만 여기서 꼭 기억할 점은, 이 캐시는 요청 단위라는 겁니다. 영구 캐시가 아니라서 사용자 간 데이터가 섞일 걱정은 없습니다.

실전에서 써먹는 DataLoader 활용 팁

이론은 쉬운데 막상 실무에 적용하면 미묘하게 발목 잡히는 부분들이 있습니다. 제가 직접 삽질하면서 얻은 팁 세 가지를 솔직하게 공유합니다.

팁 1. DataLoader는 반드시 요청마다 새로 생성하세요

가장 많이 하는 실수입니다. DataLoader 인스턴스를 전역(싱글톤)으로 두면 캐시가 모든 사용자 사이에서 공유되어 심각한 데이터 유출이 발생할 수 있습니다. 반드시 GraphQL context 생성 시점에 매 요청마다 새 인스턴스를 만들어 주입하세요. 이 한 줄을 안 지켜서 운영 사고가 나는 경우를 종종 봤습니다.

팁 2. 배치 함수의 순서와 개수를 정확히 맞추세요

DataLoader의 배치 함수는 입력으로 받은 키 배열과 정확히 같은 순서, 같은 개수의 결과를 반환해야 합니다. IN 쿼리 결과는 DB가 순서를 보장하지 않으므로, 반환 전에 키 순서대로 매핑해서 재정렬하는 코드를 꼭 넣어야 합니다. 데이터가 없으면 그 자리에 null을 넣어주는 것도 잊지 마세요. 안 그러면 엉뚱한 데이터가 매칭됩니다.

팁 3. 모니터링으로 효과를 눈으로 확인하세요

DataLoader를 적용했다고 끝이 아닙니다. APM이나 DB 쿼리 로그를 켜놓고 실제로 쿼리 수가 줄었는지 확인하세요. 의외로 중첩이 깊어지면 또 다른 N+1이 숨어 있기도 합니다. 저는 적용 전후 쿼리 카운트를 비교하는 습관을 들이고 나서야 마음이 놓이더라고요.

DataLoader 장단점 비교와 추천 대상

좋은 도구지만 만능은 아닙니다. 도입 전에 장단점을 냉정하게 따져보는 게 좋습니다.

장단점 비교표

구분 장점 단점
성능 배칭으로 DB 쿼리 수를 극적으로 감소 이벤트 루프 틱 대기로 미세한 지연 발생
구현 resolver 코드 변경 최소화로 적용 용이 배치 함수의 순서·개수 매핑 로직 필요
캐싱 요청 단위 캐싱으로 중복 조회 제거 요청 간 캐시 미공유, 별도 캐시 전략 필요
안정성 검증된 라이브러리로 신뢰도 높음 인스턴스 생명주기 관리 실수 시 데이터 유출 위험

이런 분께 DataLoader를 추천합니다

중첩 관계가 많은 GraphQL 스키마를 운영 중이고, DB 쿼리 폭증으로 응답 지연이나 부하를 겪고 있다면 DataLoader는 거의 필수에 가깝습니다. 특히 트래픽이 점점 늘어나는 서비스라면 미리 도입해두는 걸 강력히 권합니다. 반대로 데이터 관계가 단순하고 중첩 조회가 거의 없는 소규모 프로젝트라면 굳이 처음부터 도입할 필요는 없습니다. 핵심은 "내 서비스에 N+1 문제가 실제로 있는가"를 먼저 측정해보는 것입니다.

정리하면, GraphQL의 유연함은 N+1 문제라는 그림자를 동반하지만, DataLoader라는 검증된 도구로 충분히 다스릴 수 있습니다. 배칭과 캐싱의 원리만 제대로 이해하면 생각보다 적용은 어렵지 않으니, 오늘 소개한 팁들을 바탕으로 꼭 한번 적용해보시길 바랍니다. 여러분의 DB가 더 이상 비명 지르지 않기를 응원합니다!