[GraphQL - Apollo Federation] GraphQL이 어떻게 동작하는데 ? (feat. Spring과 간단한 비교)

2026. 3. 16. 10:36·Engine/GraphQL + Apollo Federation

Spring Boot 개발자가 GraphQL을 만났을 때 — REST vs GraphQL 패러다임 시프트

서론: 왜 이 글을 쓰는가

Java/Spring Boot 기반의 REST API 환경에서 학습하다가,
Node.js + NestJS + GraphQL(Apollo Federation) 기반의 MSA 환경으로 넘어오게 되었다. 가장 먼저 부딪힌 것은 패러다임의 충돌이었다.
Controller가 없는데 요청이 어떻게 처리되는지, URL이 하나뿐인데 라우팅은 어떻게 되는지, Gateway가 URL이 아니라 "타입"을 보고 분기한다는 게 무슨 뜻인지 — 기존의 학습 방향성이 전혀 통하지 않았다.

이 글은 Spring Boot의 REST API에 익숙한 백엔드 개발자가 GraphQL로 전환할 때 겪는 핵심적인 패러다임 차이를, Spring의 용어와 개념에 1:1로 매핑하며 정리한 글이다. 물론, 환경마다 다른 부분이 있을 수 있고, 틀린 부분이 있을 수 있다 ..

 

 


1. 라우팅의 주체가 바뀐다 — URL에서 스키마로

REST와 GraphQL의 가장 근본적인 차이는 "누가 라우팅을 결정하는가"다.

REST: URL × HTTP Method = 라우팅 키

Spring에서는 @RestController + @GetMapping("/api/posts/{id}") 조합이 곧 "이 요청은 어떤 로직을 실행할 것인가"를 결정한다. 게시글 목록은 GET /posts, 삭제는 DELETE /posts/123 — URL 자체가 라우팅 키다.

@RestController
@RequestMapping("/api/posts")
public class PostController {
    @GetMapping("/{id}")    // GET /api/posts/123
    public PostResponse getPost(@PathVariable Long id) { ... }

    @DeleteMapping("/{id}") // DELETE /api/posts/123
    public void deletePost(@PathVariable Long id) { ... }
}

GraphQL: 단일 엔드포인트 + 쿼리 문서 = 라우팅 키

GraphQL에서는 엔드포인트가 딱 하나다: POST /graphql. 클라이언트가 요청 body에 "이 데이터를 이 형태로 줘"라는 쿼리 문서(query document)를 보내고, 서버의 GraphQL 엔진이 그 문서를 파싱해서 어떤 Resolver를 호출할지 결정한다.

# 클라이언트가 보내는 쿼리 문서
query {
    post(id: 123) {
        title
        author { name }
    }
}

라우팅의 주체가 "URL 패턴 매칭"에서 "스키마 기반 자동 디스패치"로 이동한 것이다.

비교 요약

관점 REST (Spring) GraphQL
엔드포인트 수 기능별로 수십~수백 개 딱 하나 (POST /graphql)
라우팅 키 URL 경로 + HTTP Method 쿼리 문서 내용 (필드명)
라우팅 주체 DispatcherServlet (HTTP 레이어) GraphQL 엔진 (애플리케이션 레이어)
요청 형태 GET /posts/123 POST /graphql + body에 쿼리
응답 결정권 서버가 응답 형태를 설계 클라이언트가 필요한 필드를 선언

💡 Architect's Insight: 라우팅 판단이 HTTP 레이어에서 애플리케이션 레이어로 내려왔다는 것은, 인프라(로드밸런서, CDN)에서 URL 기반으로 하던 캐싱/라우팅/모니터링 전략을 재설계해야 한다는 뜻이기도 하다.

 

 

 


2. Overfetching / Underfetching — 데이터 결정권의 이동

REST의 가장 흔한 비효율 두 가지는 Overfetching과 Underfetching이다.

Overfetching

GET /posts/123을 호출하면 서버가 정해놓은 응답 형태 전체가 내려온다. 게시글 제목만 필요한데 작성자 프로필, 댓글 수, 첨부파일 URL까지 다 내려오면 — 그게 Overfetching이다. 네트워크 대역폭과 클라이언트의 파싱 비용이 낭비된다.

Underfetching

반대로, 게시글 + 작성자 정보를 한 화면에 보여줘야 하는데 /posts/123과 /users/456을 따로 호출해야 한다면 — 그게 Underfetching이다. 네트워크 왕복이 2번 필요하고, 클라이언트에서 응답을 조합하는 코드까지 필요하다.

GraphQL의 해결

GraphQL에서는 클라이언트가 필요한 필드만 선언한다:

{
    post(id: 123) {
        title              # 제목만 필요하면 이것만
        author { name }    # 작성자 이름도 필요하면 함께
    }
}

한 번의 요청으로 정확히 필요한 데이터만 받는다. "서버가 응답 형태를 결정"하는 모델에서 "클라이언트가 응답 형태를 결정"하는 모델로 권력 이동이 일어난 것이다.

문제 REST에서의 양상 GraphQL에서의 해결
Overfetching 필요 없는 필드까지 전부 내려옴 클라이언트가 필요한 필드만 선언
Underfetching 여러 엔드포인트를 따로 호출 (왕복 N번) 한 쿼리에 관련 데이터를 함께 요청
응답 결정권 서버 클라이언트

💡 Architect's Insight: 클라이언트에 데이터 결정권을 넘기면 프론트엔드 개발 속도가 빨라지지만, 서버 입장에서는 "어떤 쿼리가 들어올지 예측할 수 없다"는 새로운 문제가 생긴다. 쿼리 복잡도 제한(depth limit, cost analysis)이 REST에는 없는 GraphQL 고유의 운영 과제다.

 

 

 


3. Controller 없이 요청이 처리되는 원리 — Resolver의 동작

Spring Boot에서 GraphQL로 전환했을 때 가장 혼란스러웠던 부분이 이것이다. Controller가 없는데, 요청은 어떻게 처리되는가?

Spring: DispatcherServlet의 라우팅

Spring에서는 DispatcherServlet이 요청을 받아 HandlerMapping에게 "이 URL + Method에 해당하는 Controller 메서드가 뭐야?"라고 묻는다. HandlerMapping은 서버 부팅 시 @GetMapping, @PostMapping 등의 어노테이션을 스캔해서 "URL 패턴 → 메서드" 매핑 테이블을 미리 만들어둔다.

HTTP 요청
  → DispatcherServlet
  → HandlerMapping이 URL + Method로 메서드 결정
  → Controller 메서드 실행
  → HttpMessageConverter가 JSON으로 직렬화

GraphQL: 엔진의 스키마 기반 디스패치

GraphQL 엔진(Apollo Server 등)도 정확히 같은 일을 하는데, 매핑 키가 다를 뿐이다.

서버가 부팅할 때 스키마와 Resolver 객체를 로딩하면서 "타입.필드명 → Resolver 함수" 매핑 테이블을 만든다. 요청이 들어오면 쿼리 문서를 AST(추상 구문 트리)로 파싱하고, 스키마와 대조해서 해당 Resolver를 호출한다.

POST /graphql (body: query document)
  → GraphQL 엔진
  → 쿼리 파싱 → AST 생성
  → 스키마와 대조: "이 필드는 어떤 타입의 어떤 필드인가?"
  → 매핑된 Resolver 함수 호출
  → 클라이언트가 요청한 형태로 직렬화

예를 들어 스키마에 이렇게 선언되어 있고:

type Mutation {
    postsUpdate(boardID: ID, input: PostsUpdateInput!): PostsUpdatePayload
}

Resolver 객체에 이렇게 등록되어 있으면:

{
    Mutation: {
        postsUpdate: async (_, { boardID, input }, { user, elastic }) => { ... }
    }
}

GraphQL 엔진이 부팅 시 "Mutation.postsUpdate → 이 함수"라는 매핑을 만들어둔다. 클라이언트가 mutation { postsUpdate(input: {...}) { ... } }를 보내면, 엔진이 AST를 걸어서 매핑 테이블에서 함수를 찾아 호출한다.

Spring과의 1:1 매핑

역할 Spring (REST) GraphQL
요청 수신 DispatcherServlet GraphQL 엔진 (Apollo Server)
매핑 테이블 URL 패턴 → Controller 메서드 타입.필드명 → Resolver 함수
매핑 시점 부팅 시 어노테이션 스캔 부팅 시 스키마 + Resolver 로딩
매핑 키 URL + HTTP Method 타입명 + 필드명
핸들러 @Controller 클래스의 메서드 Resolver 객체의 함수

💡 Architect's Insight: Spring의 HandlerMapping과 GraphQL 엔진의 Resolver 매핑은 본질적으로 같은 역할이다. 차이는 매핑 키가 URL에서 "스키마의 타입.필드"로 바뀌었다는 것뿐이다. 이 이해가 있으면 "Controller가 없는데 어떻게 동작하지?"라는 혼란이 사라진다.

 

 

 


4. API Gateway의 진화 — URL 라우팅에서 타입 시스템 통합으로

MSA 환경에서 Gateway의 역할도 근본적으로 달라진다.

REST: URL 패턴 기반 프록시

Spring Cloud Gateway, Kong, Nginx 같은 REST API Gateway는 URL 패턴을 보고 요청을 각 마이크로서비스로 프록시한다. /api/posts/** → Post Service, /api/users/** → User Service. 라우팅 규칙이 단순하고 명확하다.

GraphQL: 스키마 레벨의 통합 (Apollo Federation)

Apollo Gateway는 이것을 GraphQL 스키마 레벨에서 한다. 각 마이크로서비스(Subgraph)가 자기만의 스키마를 선언하고, Gateway가 부팅할 때 이 스키마들을 합쳐서(Compose) 하나의 Supergraph를 만든다.

[클라이언트]
    │
    │  { post(id: 1) { title, author { name } } }
    │
    ▼
[Apollo Gateway]
    │
    │  Gateway가 Supergraph를 보고 판단:
    │  "title은 게시글 서비스에서, author.name은 회원 서비스에서"
    │
    ├──→ [게시글 서비스(Subgraph A)] → { title }
    └──→ [회원 서비스(Subgraph B)]  → { author { name } }
    │
    │  결과를 조합해서 클라이언트에 응답
    ▼
[클라이언트가 받는 응답]
    { post: { title: "...", author: { name: "..." } } }

클라이언트는 Gateway 하나만 바라보고, Gateway가 쿼리를 분석해서 "이 필드는 A 서비스에서, 저 필드는 B 서비스에서 가져와야 해"를 판단하고 각 Subgraph에 부분 쿼리를 날린다.

트레이드오프 비교

관점 REST Gateway Apollo Gateway (Federation)
라우팅 기준 URL 패턴 GraphQL 스키마 (타입 + 필드)
통합 수준 HTTP 프록시 (요청 전달만) 타입 시스템 레벨 통합 (쿼리 분석 + 조합)
스키마 관리 각 서비스가 독립 (OpenAPI 등) Supergraph로 합성 (충돌 가능성)
복잡도 낮음 높음
SPOF 위험 있음 (Gateway 장애 시) 동일하게 있음 + 스키마 합성 실패 시 부팅 불가
조직적 이점 서비스 간 API 계약 별도 관리 각 팀이 독립적으로 스키마를 발전시킬 수 있음

💡 Architect's Insight: REST Gateway는 "요청을 어디로 보낼지"만 결정하지만, Apollo Gateway는 "쿼리를 어떻게 쪼개서 어디로 보내고, 결과를 어떻게 합칠지"까지 결정한다. 이것은 더 강력하지만, Gateway의 장애가 더 치명적이라는 뜻이기도 하다. Federation 환경에서는 Gateway 이중화와 스키마 호환성 테스트가 필수다.

 

 

 


결론: 패러다임이 바뀌면 사고 모델도 바뀌어야 한다

REST에서 GraphQL로의 전환은 단순히 "라이브러리를 바꾸는 것"이 아니다.

라우팅, 데이터 계약, Gateway, 캐싱, 모니터링 — 백엔드 아키텍처의 거의 모든 레이어에서 사고 모델이 바뀌어야 한다.

개념 REST 사고 모델 GraphQL 사고 모델
"이 요청은 어디로 가지?" URL 패턴을 본다 쿼리 문서의 필드를 본다
"응답에 뭐가 들어있지?" 서버가 정한 형태 클라이언트가 선언한 형태
"서비스 간 통합은?" URL 라우팅 타입 시스템 합성
"캐싱은?" URL + HTTP Method 기반 쿼리 해시 기반 (APQ)
"핸들러는?" Controller 메서드 Resolver 함수

 

 

Spring Boot에서 @GetMapping을 찾아가던 습관을,

이제는 스키마에서 필드명을 찾고 Resolver를 추적하는 습관으로 바꿔야 한다.

 

이 사고 전환이 이루어지면, GraphQL 코드베이스에서 "Controller가 없는데 어떻게 동작하지?"라는 혼란은 해소될 수 있다.

 

 


다음 글에서는 이 패러다임 위에서 레거시(Express) Resolver와 최신(NestJS) Resolver가 어떻게 한 서비스 안에서 공존하는지, 그리고 APQ(Automatic Persisted Queries)가 GET과 POST를 어떻게 나눠 쓰는지를 다룰 예정이다.

'Engine > GraphQL + Apollo Federation' 카테고리의 다른 글

[GraphQL - Apollo Federation] GraphQL이 라우팅하는 과정  (1) 2026.03.16
'Engine/GraphQL + Apollo Federation' 카테고리의 다른 글
  • [GraphQL - Apollo Federation] GraphQL이 라우팅하는 과정
하가네
하가네
  • 하가네
    하 렌
    하가네
  • 전체
    오늘
    어제
    • 분류 전체보기 (127)
      • Computer Science (23)
        • 운영체제 (7)
        • 데이터통신 (6)
        • 자료구조 (4)
        • 논리회로 (0)
        • 확률 및 통계 (0)
        • 데이터베이스 (2)
        • AI소프트웨어 (3)
        • 컴퓨터네트워크 (1)
      • Design (4)
        • OOP - 객체 지향 프로그래밍 (2)
        • DDD - 도메인 주도 개발 (데이터베이스 주도 .. (0)
        • EDA - 이벤트 기반 아키텍처 (1)
        • MSA - 마이크로서비스 아키텍처 (0)
        • ADD - AI 주도 개발 (1)
      • Language (2)
        • Java (0)
        • TypeScript (2)
      • Framework (12)
        • Spring (9)
        • NestJS (3)
      • Engine (3)
        • Elasticsearch (1)
        • GraphQL + Apollo Federation (2)
      • Plugin - Extension (1)
        • VS Code (1)
        • IntelliJ (0)
      • Tips (2)
        • 터미널 명령어 (1)
        • 우분투 명령어 에러 (1)
      • SSA (26)
        • Front (1)
        • Back (23)
        • DB (1)
        • 기획 (1)
      • CNU SW 아카데미 (42)
        • 1주차 (5)
        • 2주차 (5)
        • 3주차 (2)
        • 4주차 (1)
        • 5주차 (3)
        • 6주차 (2)
        • 7주차 (0)
        • 8주차 (1)
        • 9주차 (14)
        • 10주차 (0)
        • 11주차 (1)
        • 12주차 (0)
        • 13주차 (2)
        • 14주차 (2)
        • 15주차(최종 프로젝트) (3)
        • 최종 프로젝트 이후 (1)
      • 모각코 (6)
        • 2023 동계 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ESLint
    Husky
    ci/cd
    Typescript
    프론트엔드/백엔드
    릴리스엔지니어링
    DX(DeveloperExperience)
    lint-staged
    생산성
    개발자경험(DX)
    아키텍처
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.
하가네
[GraphQL - Apollo Federation] GraphQL이 어떻게 동작하는데 ? (feat. Spring과 간단한 비교)
상단으로

티스토리툴바