우리는 지난 십수 년간 REST API를 절대 진리라 믿고 사용해 왔습니다. 하지만 화면이 좁고 네트워크가 불안정한 모바일(Mobile) 시대가 오면서 REST API의 두 가지 치명적인 한계가 드러나기 시작했습니다. 첫째, 오버패칭(Over-fetching)입니다. 모바일 앱 메인 화면에 유저의 "닉네임" 딱 하나만 필요한데, 백엔드가 만들어둔 GET /users/1 API를 찌르면 이메일, 주소, 장바구니 내역까지 쓸데없는 수십 개의 데이터가 한꺼번에 쏟아집니다(데이터 낭비). 둘째, 언더패칭(Under-fetching)입니다. 유저의 프로필, 최근 본 상품, 찜한 목록을 하나의 화면에 그리려면 /users/1, /products/recent, /favorites 세 개의 API를 세 번이나 찔러야 합니다. "아니, 프론트엔드가 딱 원하는 데이터(필드)만 고르고 골라서 한 번의 요청(POST)으로 싹 다 받아올 수는 없을까?" 페이스북(Meta)이 이 불만을 해결하기 위해 창조해 낸 차세대 쿼리 언어가 바로 GraphQL(그래프QL)입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "REST API 시대의 진화!" Spring Boot for GraphQL 연동과 실무 아키텍처에 대하여 다뤄보겠습니다.

🕸️ 1. GraphQL의 핵심: 스키마(Schema)와 단일 엔드포인트
GraphQL은 REST처럼 /users, /orders 처럼 수십 개의 URL이 필요 없습니다. 오직 POST /graphql 이라는 단 1개의 주소만 뚫어놓습니다. 대신 클라이언트와 서버가 서로 어떤 데이터를 주고받을 수 있는지 약속하는 스키마 파일(.graphqls)을 작성해야 합니다.

# src/main/resources/graphql/schema.graphqls
# 1. 뼈대가 되는 타입(Type) 정의
type User {
id: ID!
name: String!
email: String
posts: [Post] # 이 유저가 쓴 글 목록
}
type Post {
id: ID!
title: String!
content: String
}
# 2. 조회용 API (REST의 GET 역할)
type Query {
# id를 넣으면 User를 반환한다
findUserById(id: ID!): User
}
⚙️ 2. Spring Boot에서 GraphQL 컨트롤러 뚫기
스프링 부트 2.7(그리고 3.x)부터 spring-boot-starter-graphql을 공식적으로 편입하면서, 아주 직관적인 어노테이션으로 GraphQL 서버를 띄울 수 있게 되었습니다.
@Controller
@RequiredArgsConstructor
public class UserGraphqlController {
private final UserRepository userRepository;
// 🌟 schema.graphqls 에 정의된 "Query" 안의 "findUserById" 와 이름이 완벽히 매칭됩니다!
@QueryMapping
public User findUserById(@Argument Long id) {
// DB에서 User 엔티티를 찾아서 그대로 리턴 (JPA 연동도 완벽)
return userRepository.findById(id).orElseThrow();
}
}
이제 프론트엔드가 POST /graphql로 아래와 같은 요상한 텍스트를 던집니다.
query {
findUserById(id: 1) {
name
posts {
title
}
}
}
그러면 스프링 서버는 User의 이메일(email)이나 Post의 내용(content) 같은 불필요한 데이터(오버패칭)는 싹둑 잘라내고, 오직 클라이언트가 요구한 '이름'과 '게시글 제목'만 예쁜 JSON으로 조립해서 응답(200 OK)을 줍니다!
💀 3. N+1 문제의 재림: DataFetcher의 끔찍한 함정
"우와! 프론트엔드가 원하는 데이터만 쏙쏙 뽑아주는 궁극의 기술이네요! 무조건 써야지!" 하지만 치명적인 문제가 도사리고 있습니다. 바로 9단계에서 배웠던 JPA의 악몽, N+1 문제가 GraphQL에서는 더 극단적으로 발생한다는 점입니다.
만약 유저 100명의 목록과 그들의 게시글을 뽑아달라는 쿼리가 들어왔다고 칩시다. GraphQL은 부모(User)를 먼저 조회(1번 쿼리)한 뒤, 자식 필드(posts)를 맵핑하기 위해 100명의 User에 대해 각각 추가로 100번의 DB 쿼리(SELECT posts WHERE user_id = ?)를 날리는 방식(DataFetcher)으로 동작합니다. REST API에서는 개발자가 FETCH JOIN을 직접 짜서 N+1을 1방 쿼리로 묶어버렸지만, 클라이언트가 맘대로 쿼리를 날리는 GraphQL 환경에서는 백엔드 개발자가 어느 타이밍에 조인이 일어날지 예측할 수가 없습니다!
실무 해결책: 스프링 GraphQL은 BatchMapping (또는 DataLoader) 이라는 기능을 제공합니다. 100명의 user_id를 메모리 캐시에 잠시 모아두었다가 SELECT ... WHERE user_id IN (1, 2, ... 100) 형태로 쿼리를 단 한 방으로 묶어서(Batch) DB에 날리는 천재적인 캐싱 우회 기법을 반드시 적용해야 합니다.
🎯 4. 마무리 및 다음 단계
지금까지 수십 개의 REST 엔드포인트를 뚫는 노가다와 모바일 데이터 낭비(오버패칭)를 멈추게 하고, 프론트엔드에게 자유로운 데이터 패칭 권한을 넘겨주는 현대 API 통신의 혁명, Spring GraphQL 아키텍처와 치명적인 N+1(DataLoader) 이슈에 대해 상세히 다루어 보았습니다. 이제 프론트엔드와 "이 필드 좀 API에 추가해 줘"라며 싸울 일이 없어졌습니다.
클라이언트(브라우저)와 백엔드 간의 통신은 REST나 GraphQL로 예쁜 JSON을 주고받으면 됩니다. 그런데, 앞서 38단계에서 찢어놓은 서버 대 서버(Server to Server) 간의 통신은 어떨까요? 결제 서버와 재고 서버가 서로 초당 수만 건의 통신을 주고받는데, 여기에도 뚱뚱한 '글자(JSON)'를 쓰고 '무거운 HTTP/1.1'을 쓰려니 너무 느리고 비효율적입니다. "사람이 읽을 필요 없는 서버 간 통신은, 글자(JSON) 대신 컴퓨터가 0.001초 만에 읽는 바이너리(0과 1)로 암호화해서 쏘면 안 될까?" 이어지는 44단계 포스팅에서는 "넷플릭스와 구글의 내부망 통신 표준!" MSA 통신의 극한의 속도를 끌어내는 gRPC 프로토콜과 Protocol Buffers 연동 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] WebRTC 화상 회의: P2P 시그널링 서버의 완성 (0) | 2026.08.12 |
|---|---|
| gRPC 통신: JSON을 버리고 초고속 바이너리(Binary)를 달다 (0) | 2026.08.05 |
| [Spring Boot] Spring HATEOAS: 진정한 REST API (Level 3) 달성기 (0) | 2026.08.05 |
| [Spring Boot] 낙관적 락(Optimistic) vs 비관적 락(Pessimistic): JPA 동시성 제어 (0) | 2026.08.05 |
| [Spring Boot] Redis 분산 락(Distributed Lock): 분산 환경의 동시성 제어 (0) | 2026.08.05 |