GraphQL

"GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data."

— GraphQL 공식 스펙 개요

프론트엔드 개발자에게는 데이터 조회를 무한 자유 설계할 수 있는 신세계를 열어주어 '백엔드 제발 이것 좀 API에 필드로 추가해 주세요'라는 굽신거림을 끝장냈으나, 백엔드 개발자에게는 쿼리 성능 튜닝과 보안(Depth 제약)이라는 묵직한 부담감을 떠안긴 양날의 검. 백엔드 개발자들은 GraphQL 스키마 타이핑과 리졸버(Resolver) 작성 보일러플레이트를 짜다가 지쳐서 결국 다시 REST API로 회귀하고 싶은 충동을 하루에도 수십 번씩 느낀다

1. 개요

GraphQL은 2012년 페이스북(현 Meta)이 모바일 앱의 심각한 성능 저하와 데이터 무거움을 해결하기 위해 개발하고, 2015년에 오픈소스로 공개한 API용 데이터 쿼리 언어이자 런타임이다. 기존의 REST API가 엔드포인트마다 정해진 데이터 구조만 반환하는 경직성을 가졌다면, GraphQL은 클라이언트가 쿼리를 작성해 자신이 필요한 데이터의 '형태와 필드'를 직접 정의하여 요청할 수 있다. 이 덕분에 오버페칭(필요 없는 필드까지 다 받아오는 현상)과 언더페칭(원하는 정보를 다 얻기 위해 엔드포인트를 여러 번 찔러야 하는 현상)을 기가 막히게 해결했다.(...)

2. REST API를 저격하는 GraphQL의 핵심 무기: Query, Mutation, Subscription

2.1. 단 하나의 엔드포인트로 모든 것을 해결한다

기존 REST API/users, /posts, /comments 등 리소스별로 무수히 많은 엔드포인트를 설계하고 관리해야 했다. 반면 GraphQL은 오직 단 하나의 엔드포인트(보통 /graphql)만 열어두고, 클라이언트가 HTTP POST 바디에 요청 쿼리를 실어 보내는 단일 통로 방식을 취한다.

GraphQL의 데이터 조작 행위는 크게 세 가지로 분류된다.

  1. Query (조회): REST의 GET에 해당한다. 내가 원하는 필드만 트리 구조로 적어 보내면, 딱 그 구조에 맞춰 정제된 JSON 데이터가 반환된다.
  2. Mutation (변경): REST의 POST, PUT, DELETE에 해당한다. 데이터를 생성, 수정, 삭제하고 그 결과로 변경된 데이터를 즉시 한 큐에 조회할 수 있어 통신 횟수를 절약해 준다.
  3. Subscription (구독): 실시간 양방향 통신을 위한 스펙이다. 웹소켓을 기반으로 작동하며, 특정 데이터가 변경되었을 때 서버가 클라이언트에 실시간으로 이벤트를 밀어 넣어 준다.(...)

이러한 강력한 유연성 덕분에 화면 기획이 수시로 바뀌는 애자일한 프론트엔드 개발 환경에서 극강의 생산성을 보장받는다.

3. 빛 뒤에 숨은 백엔드의 절규: N+1 문제와 보안 지옥

3.1. 우아한 쿼리가 초래하는 성능의 파멸

GraphQL이 프론트엔드의 구세주라면, 백엔드에는 아주 매서운 가시밭길을 선사한다. 그중 가장 대표적인 악마가 바로 N+1 문제다. 클라이언트가 유저 목록을 가져오면서 각 유저가 쓴 글 목록과 댓글 목록까지 계층형으로 쿼리하면, 백엔드의 리졸버(Resolver) 함수들이 순진하게 각 객체마다 데이터베이스에 개별 SELECT 쿼리를 날려 수백 번의 무의미한 DB 조회를 일으킨다.1

이 파멸적인 성능 저하를 막기 위해 백엔드 개발자들은 Facebook이 고안한 DataLoader 같은 배치(Batching) 및 캐싱 라이브러리를 의무적으로 도입해 쿼리를 하나로 묶어 날려야 한다. 이 작업을 완벽히 수행하려면 리포지토리 레이어와 리졸버 실행 컨텍스트에 대한 엄청난 수준의 최적화 공수가 들어간다.

3.2. 악의적인 무한 쿼리와 보안 구멍

또한, REST API는 백엔드가 사전에 통제한 고정된 데이터만 나가므로 안전하지만, GraphQL은 클라이언트가 쿼리를 마음대로 커스텀해 날릴 수 있다. 만약 악의적인 유저가 user { friends { friends { friends { ... } } } } 형태로 쿼리를 수백 단계 깊이로 꼬아 보내면, 서버는 이를 파싱하고 리졸버를 돌리다가 메모리가 터져 서버 다운(DoS) 대참사로 이어진다. 이를 방어하기 위해 쿼리 복잡도(Query Complexity) 분석기를 도입하여 일정 깊이 이상의 쿼리는 컴파일 단에서 칼같이 거부하는 추가적인 방어벽을 설계해야 한다.2

4. 관련 밈 및 드립

4.1. 프론트엔드의 천국, 백엔드의 지옥

GraphQL 도입을 둘러싼 두 진영의 영원한 불화설.

프론트엔드 개발자들은 REST API 스펙 문서를 일일이 뒤적거리지 않고 스스로 쿼리를 조립하는 GraphQL 도입을 격렬히 외치는 선교사가 된다. 반면 백엔드 개발자들은 SQL 튜닝, N+1 문제 해결, 타입 라이팅, 에러 핸들링 지옥에 빠져 GraphQL 이야기만 나와도 등 뒤에 식은땀을 흘리며 '그냥 REST API로 합시다'라고 철저히 방어 자세를 취하는 짤방이 개발자 커뮤니티의 단골 유머 소재로 쓰인다.(...)

5. 여담

  • 스키마 퍼스트 vs 코드 퍼스트: GraphQL 백엔드를 구축할 때, 스키마 정의서(.graphql)를 먼저 쓰고 코드를 짜는 'Schema-First' 방식과, TypeScript나 Python 코드를 짜면 스키마가 자동으로 빌드되는 'Code-First' 방식이 존재한다. 두 진영의 장단점이 워낙 뚜렷해 기술 스택을 정할 때 키보드 배틀이 자주 일어난다.
  • 강력한 플레이그라운드: GraphQL은 기본적으로 GraphQL PlaygroundGraphiQL 같은 웹 UI 기반의 테스트 도구를 인-박스로 제공한다. 여기서 쿼리를 치면 자동 완성은 물론, API 스키마 전체가 우측 탭에 인터랙티브한 문서로 실시간 시각화되어 있어 Swagger 같은 API 문서 도구를 따로 빌드하는 노력을 완전히 절약해 준다.
  • HTTP 캐싱의 좌절: REST API는 URL 단위로 데이터가 매핑되어 웹 브라우저나 CDN 단에서 손쉽게 HTTP 캐싱을 때릴 수 있지만, GraphQL은 모든 요청이 /graphql이라는 동일한 단일 엔드포인트와 POST 요청으로 가기 때문에 표준 HTTP 캐싱을 전혀 적용할 수 없다. 이를 극복하기 위해 Apollo Client 같은 별도의 정교한 클라이언트 측 가상 캐시 저장소를 억지로 굴려야 한다.(...)

6. 관련 문서

각주

  1. 실제로 N+1 문제를 방치한 채 운영 서버를 배포했다가, 동시 접속자가 조금만 몰려도 DB 커넥션 풀이 마르고 CPU 점유율이 100%를 찍으며 전체 시스템이 마비되는 피눈물 나는 운영 대참사를 겪게 된다.

  2. 또한 GraphQL은 기본적으로 모든 응답에 대해 HTTP 상태 코드를 200 OK로 뱉어내고, 에러는 JSON 바디의 errors 배열에 따로 담아주기 때문에, 기존의 표준 HTTP 모니터링 도구(APM)들이 서버 에러를 정상으로 오인하는 대환장 파티가 펼쳐지기도 한다.