이 글에서 얻는 것

  • GraphQL을 “한 방에 다 가져오는 API”로만 보지 않고, 스키마/리졸버/권한/성능을 함께 설계하는 감각을 얻습니다.
  • N+1, 과도한 쿼리 복잡도, 캐싱/페이징 같은 실무 문제를 어떻게 구조로 풀지 기준이 생깁니다.
  • 버전 관리(Deprecated 중심)와 breaking change를 최소화하는 변경 전략을 이해합니다.

0) GraphQL의 장점은 ‘클라이언트 주도’지만, 그만큼 서버가 책임져야 한다

GraphQL의 매력:

  • 클라이언트가 필요한 필드만 요청 → over/under-fetch 감소
  • 하나의 엔드포인트로 다양한 화면 요구를 수용

대가:

  • 서버는 “임의의 쿼리”를 안전하고 빠르게 처리해야 합니다(권한/복잡도/캐싱/관측).

1) 스키마 설계: 타입은 계약(Contract)이다

1-1) nullable 설계를 명확히

GraphQL에서 nullable은 “없을 수도 있다”가 아니라 “에러/부분 응답”과도 연결됩니다.

  • 반드시 있어야 하는 값은 non-null로 두고,
  • 실패 가능성이 높은 필드는 nullable + 에러 정책(extensions)을 함께 설계하는 편이 좋습니다.

1-2) 입력/출력 타입 분리

  • 입력(Input)은 검증/제약이 중요(필요한 필드만)
  • 출력(Type)은 조회 최적화와 도메인 표현이 중요

1-3) Mutation은 “상태 변경”을 명확히 드러내라

  • “무엇이 바뀌는지”가 계약에 드러나야 합니다
  • 반환 타입에 변경 결과(새 상태/에러)를 포함해 클라이언트가 후속 조회를 줄이게 설계합니다

2) 리졸버 구조: N+1은 설계 문제다

N+1을 막는 대표 전략:

  • DataLoader(배치 로딩)로 연관 데이터 조회를 모아 한 번에 가져오기
  • 조회를 “필드별 쿼리”가 아니라 “요청 단위 쿼리”로 합치기

추가로 실무에서 중요한 것:

  • 캐싱(요청 스코프 캐시 + 공유 캐시)
  • 사전 조인/프리페치 전략(엔드포인트별로 자주 쓰는 조합 최적화)

3) 페이징: 커서 기반(Connection)으로 간다

대량 조회에서는 offset 페이징이 느려지거나 불안정해질 수 있습니다.

  • 커서 기반은 “다음 페이지”가 안정적이고 성능도 유리한 경우가 많습니다.
  • 정렬 키/커서 설계(중복 정렬 키 처리)가 핵심입니다.

4) 권한/보안: 필드 단위로 새는 사고를 막아라

GraphQL은 “한 요청에 여러 리소스”를 가져오기 쉬워서, 권한 검증이 엔드포인트 단위로 끝나면 데이터가 새기 쉽습니다.

  • 필드/타입 단위 권한 정책(민감 필드 분리)
  • 테넌트/소유권 필터 강제
  • 쿼리 복잡도 제한(depth/complexity)으로 과도한 탐색 차단

5) 버전 관리: v2를 만들기보다 Deprecated로 이행한다

GraphQL은 “필드 추가”가 자연스럽고, “필드 삭제”가 어려운 편입니다.

  • breaking change는 @deprecated로 충분히 오래 유예
  • 클라이언트 사용량(필드 usage)을 수집해 안전한 제거 시점을 결정

6) 운영 포인트(관측성)

  • 쿼리별 latency/에러율/DB 쿼리 수를 추적
  • 느린 쿼리를 식별하기 위해 “operationName”과 샘플링 로그를 남김
  • persisted queries로 쿼리를 제한/캐싱 최적화(필요한 경우)

연습(추천)

  • “주문 조회 화면”을 예로 스키마를 설계하고, N+1이 어떻게 발생하는지 리졸버 호출 흐름을 그려보기
  • DataLoader를 적용해 DB 쿼리 수가 어떻게 줄어드는지 측정해보기
  • 쿼리 depth/complexity 제한 정책을 정하고, 악의적 쿼리를 만들어 차단되는지 확인해보기