안녕하세요! 오늘은 GraphQL을 운영하면서 거의 모든 개발자가 한 번쯤은 정면으로 부딪히는 N+1 문제, 그리고 그걸 깔끔하게 해결해주는 DataLoader에 대해 이야기해보려고 합니다. 솔직히 말하면 저도 처음엔 "쿼리 몇 개 더 나가는 게 뭐 그리 대수야?"라고 생각했는데, 실제 서비스 트래픽이 붙으니 DB가 비명을 지르더라고요. 그 경험을 바탕으로 최대한 현실적으로 풀어보겠습니다.
목차
- N+1 문제가 대체 뭔가요?
- DataLoader는 어떻게 N+1을 해결할까?
- 실전에서 써먹는 DataLoader 활용 팁
- DataLoader 장단점 비교와 추천 대상
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가 더 이상 비명 지르지 않기를 응원합니다!