대규모 GraphQL 브릿지의 파멸적 트래픽: N+1 쿼리 폭발과 Dataloader 패턴을 통한 데이터베이스 융단폭격 방어
REST API 중심의 사내 게이트웨이를 완전히 GraphQL 백엔드 서버로 탈바꿈 시키며 환희에 젖었던 것도 잠시, 저희 메인 데이터베이스 모니터 탭은 빨갛게 달아오른 사과처럼 불타고 있었습니다. 원인은 무한한 자유가 낳은 참혹한 대가, 백엔드의 영원한 난제 N+1 문제였습니다.
사용자가 "게시글 100개에 속한 각각의 작성자 정보"를 조회하는 GraphQL 쿼리를 프론트엔드에서 날리면, 트리의 깊이(Depth)를 따라 최상단 쿼리 핸들러가 최초로 게시글 100개를 가져옵니다(1번 쿼리). 그 후 자식 필드 매핑 로직으로 인해 그래프 서버 엔진이 게시글마다 매달려 있는 User id를 기반으로 작성자 정보 쿼리를 무려 100번 연속해서 루프로 때려넣습니다(+ N번 쿼리). 즉, 단 한 번의 클라이언트 요청에 무려 101번의 데이터베이스 트랜잭션 문맥 부하가 생성되는 극악의 O(N) 스택 버그가 발발한 것입니다. 성능 튜닝은 고사하고 방화벽이 막아설 수준의 트래픽이었습니다.
이를 방어해 낸 생명줄은 오픈소스 라이브러리인 'Dataloader'의 존재였습니다. 데이터로더 패턴의 원리는 Node.js의 싱글 이벤트 루프의 틱(Tick) 대기 시간을 교묘하게 해킹하는 것에 있습니다. 자식 필드 리졸버 함수들이 각자 데이터베이스에서 User id 1, 2, 3.. 을 당당히 개별 요청하게 내버려 둡니다. 하지만 데이터로더는 이 요청을 즉시 DB로 쏘지 않고 메모리에 모아 둡니다. 1 밀리초 뒤에 이벤트 루프 사이클이 끝나기 직전, 메모리에 쌓인 100개의 조각난 ID들을 단 하나의 거대한 "WHERE id IN (1,2,3...100)" 다중 쿼리 배치(Batching)로 말아서 데이터베이스에 단 한 번의 I/O만 날린 뒤, 응답을 쪼개어 각각 돌려줍니다. I/O 콜 수를 101번에서 단 2번으로 융합 분해해 내는 이 우아한 미들웨어 덕에 GraphQL 아키텍처가 비로소 프로덕션 반열에 오르며 살아남을 수 있었습니다.
Related Posts
JVM JIT 컴파일러의 극단적 런타임 최적화: 탈출 분석(Escape Analysis)과 스칼라 치환의 마법
정적 컴파일 언어를 압도하는 자바 머신의 동적 스크립트 프로파일링 및 객체 힙 버림 최적화 기법.
리눅스 eBPF와 XDP를 활용한 커널 바이패스(Kernel Bypass) 초저지연 패킷 필터링 아키텍처
운영체제 네트워크 스택의 병목을 우회하여 디바이스 드라이버 레벨에서 직접 샌드박스 코드를 주입하는 eBPF의 혁명.
스플릿 브레인(Split-Brain) 붕괴를 막는 분산 락(Distributed Lock) 시스템과 펜싱(Fencing) 토큰의 도입
Zookeeper, Redis Redlock의 시계 위임 맹점을 찌르는 가비지 컬렉션 시간 정지(Stop-the-World) 현상 롤백 설계.