GraphQL: 기업의 허니문이 끝났다

발행: (2025년 12월 15일 오전 02:13 GMT+9)
11 min read

Source: Hacker News

Introduction

저는 실제 기업용 애플리케이션에서 GraphQL, 특히 Apollo Client와 Server를 몇 년간 사용해 왔습니다. 장난감 앱이 아니라, 그린필드 스타트업도 아닙니다. 여러 팀, BFF, 하위 서비스, 가시성 요구사항, 실제 사용자를 가진 정식 프로덕션 환경입니다.

그동안 사용하면서 꽤 지루한 결론에 도달했습니다:

GraphQL은 실제 문제를 해결하지만, 그 문제는 사람들이 생각하는 것보다 훨씬 틈새적입니다. 대부분의 기업 환경에서는 이미 다른 곳에서 해결되고 있으며, 트레이드오프를 모두 합산하면 GraphQL은 종종 순수히 부정적인 결과를 낳습니다.

이 글은 “GraphQL이 나쁘다”는 포스트가 아니라, “GraphQL은 신혼기가 끝났다”는 포스트입니다.

What GraphQL is supposed to solve

GraphQL이 해결하려는 주요 문제는 오버패칭(over‑fetching) 입니다. 아이디어는 간단하고 매력적입니다:

  • 클라이언트가 정확히 필요한 필드만 요청한다
  • 더 이상도, 덜도 아니다
  • 낭비되는 바이트가 없다
  • 새로운 UI 요구사항마다 백엔드 변경이 필요하지 않다

이론적으로는 훌륭합니다. 실제로는 상황이 더 복잡합니다.

Overfetching is already solved by BFFs

대부분의 기업 프론트엔드 아키텍처는 이미 BFF(Backend for Frontend)를 가지고 있습니다. BFF는 특별히 다음을 위해 존재합니다:

  • UI에 맞게 데이터를 형태화한다
  • 여러 하위 호출을 집계한다
  • 백엔드 복잡성을 숨긴다
  • UI가 필요로 하는 정확한 데이터를 반환한다

BFF 뒤에 REST를 사용한다면 오버패칭은 이미 해결 가능한 문제입니다. BFF는 응답을 축소하고 UI가 필요로 하는 것만 반환할 수 있습니다.

물론 GraphQL도 같은 일을 할 수 있습니다. 하지만 대부분의 하위 서비스는 여전히 REST이기 때문에, GraphQL 레이어는 여전히 그 API들에서 오버패칭을 하고 그 후에 응답을 재구성해야 합니다. 오버패칭을 없앤 것이 아니라 한 층 아래로 옮긴 것에 불과합니다. 이는 GraphQL의 주요 장점을 크게 감소시킵니다.

GraphQL이 승리하는 경우가 있습니다: 여러 페이지가 같은 엔드포인트를 호출하지만 약간씩 다른 필드가 필요할 때, GraphQL은 쿼리마다 차이를 지정할 수 있게 해줍니다. 하지만 이는 보통 몇 개의 필드만 절약하고 다음과 같은 비용을 감수하게 됩니다:

  • 더 많은 설정
  • 더 많은 추상화
  • 더 많은 간접성
  • 유지보수해야 할 코드 증가

몇 킬로바이트를 절약하기 위한 비용이 꽤 비쌉니다.

Implementation time is much higher than REST

GraphQL은 REST BFF에 비해 구현 시간이 크게 깁니다.

REST 워크플로우:

  1. 하위 서비스 호출
  2. 응답을 적응시킴
  3. UI가 필요로 하는 형태로 반환

GraphQL 워크플로우:

  1. 스키마 정의
  2. 타입 정의
  3. 리졸버 정의
  4. 데이터 소스 정의
  5. 어댑터 함수 작성 (필요함)
  6. 스키마, 리졸버, 클라이언트를 동기화 유지

GraphQL은 소비를 최적화하는 대신 생산 속도를 희생합니다. 기업 환경에서는 생산 속도가 이론적 우아함보다 더 중요합니다.

Observability is worse by default

GraphQL은 특이한 상태 코드 규칙을 사용합니다:

  • 쿼리를 파싱할 수 없으면 400
  • 실행 중 오류가 발생하면 errors 배열과 함께 200
  • 성공하거나 부분 성공이면 200
  • 서버에 접근할 수 없으면 500

가시성 관점에서 보면 이것은 고통스럽습니다. REST에서는:

  • 2XX는 성공
  • 4XX는 클라이언트 오류
  • 5XX는 서버 오류

대시보드에서 2XX만 필터링하면 해당 요청이 성공했음을 알 수 있습니다. GraphQL에서는 200이 부분 혹은 전체 실패를 의미할 수도 있습니다. Apollo는 이 동작을 커스터마이징할 수 있게 하지만, 그만큼 추가 설정, 관례, 정신적 부담이 늘어나며 이는 블로그 글이 아니라 실제 호출 시에 부담이 됩니다.

Caching sounds amazing until you live with it

Apollo의 정규화된 캐싱은 이론적으로 인상적입니다. 실제로는 깨지기 쉽습니다. 두 쿼리가 단 하나의 필드만 다를 경우, Apollo는 이를 별개의 쿼리로 취급해 다음을 수동으로 연결해야 합니다:

  • 캐시에서 기존 필드 가져오기
  • 차이나는 필드만 새로 가져오기

결과:

  • 여전히 라운드‑트립이 발생
  • 코드 복잡도 증가
  • 캐시 문제 디버깅 자체가 새로운 문제

반면 REST는 몇 개의 여분 필드를 오버패칭해도 전체 응답을 캐시하고 넘어갑니다. 여분의 킬로바이트는 저렴하지만 복잡성은 그렇지 않습니다.

The ID requirement is a leaky abstraction

Apollo는 기본적으로 모든 객체에 id 혹은 _id 필드가 있기를 기대합니다. 아니면 커스텀 식별자를 설정해야 합니다. 많은 기업 API는:

  • ID를 반환하지 않는다
  • 자연스러운 고유 키가 없다
  • 전역적으로 식별 가능한 엔터티로 모델링되지 않았다

따라서 BFF는 GraphQL 클라이언트를 만족시키기 위해 로컬에서 ID를 생성해야 하며, 이는:

  • 로직 추가
  • 필드 추가
  • 추가로 가져와야 하는 필드(원래 오버패칭을 줄이려는 목표와 역설적)

REST 클라이언트는 이런 제약이 없습니다.

File uploads and downloads are awkward

GraphQL은 바이너리 데이터를 다루기에 적합하지 않습니다. 실제로는 다음과 같은 상황이 발생합니다:

  • 다운로드 URL을 반환하고, 파일을 가져올 때는 REST를 사용하거나
  • 큰 페이로드(예: PDF)를 GraphQL 응답에 직접 포함시켜 응답이 부풀고 성능이 저하됨

이렇게 되면 “단일 API”라는 이야기가 깨집니다.

Onboarding is slower

대부분의 프론트엔드·풀스택 개발자는 REST에 더 익숙합니다. GraphQL을 도입하면 다음을 가르쳐야 합니다:

  • 스키마
  • 리졸버
  • 쿼리 구성
  • 캐싱 규칙
  • 오류 의미론

이 학습 곡선은 마찰을 만들며, 팀이 빠르게 움직여야 할 때 큰 장애가 됩니다. REST는 지루하지만, 그 지루함이 매우 잘 확장됩니다.

Error handling is harder than it needs to be

GraphQL 오류 응답은… 이상합니다. 다음과 같은 요소가 있습니다:

  • nullable vs non‑nullable 필드
  • 부분 데이터 + errors 배열
  • 커스텀 상태 코드를 담은 extensions
  • 어느 리졸버가 왜 실패했는지 추적 필요

이 모든 것이 간접성을 추가합니다. 반면 단순한 REST 설정에서는:

  • 입력 검증 실패 → 400
  • 백엔드 오류 → 500
  • 검증 라이브러리(예: Zod) 오류 → 바로 처리

단순한 오류가 우아한 오류보다 이해하기 쉽습니다.

The net result

GraphQL은 확실히 유효한 사용 사례가 있습니다. 하지만 대부분의 기업 환경에서는:

  • 이미 BFF가 존재한다
  • 하위 서비스는 REST이다
  • 오버패칭이 가장 큰 문제는 아니다
  • 가시성, 신뢰성, 속도가 더 중요하다

모든 것을 합산하면 GraphQL은 좁은 문제를 해결하면서 더 넓은 새로운 문제들을 도입합니다. 그래서 수년간 프로덕션에서 사용해 본 결과 저는 이렇게 말하고 싶습니다:

GraphQL이 나쁜 것은 아니다. 다만 틈새 기술일 뿐이며, 특히 기존 아키텍처가 이미 해결하고 있다면 필요 없을 가능성이 높다.

Back to Blog

관련 글

더 보기 »