우리는 지난 수십년간 REST API를 절대 진리라 믿고 사용해 왔습니다. 하지만 화면이 좁고 네트워크가 불안정한 모바일(Mobile) 시대가 오면서 REST API의 두 가지 치명적인 한계가 드러나기 시작했습니다.
첫째, 오버패칭(Over-fetching)입니다.
모바일 앱 메인 화면에 유저의 "닉네임" 딱 하나만 필요한데, 백엔드가 만들어둔 GET /users/1 API를 찌르면 이메일, 주소, 장바구니 내역까지 쓸데없는 수십 개의 데이터가 한꺼번에 쏟아집니다(데이터 낭비).
둘째, 언더패칭(Under-fetching)입니다.
유저의 프로필, 최근 본 상품, 찜한 목록을 하나의 화면에 그리려면 /users/1, /products/recent, /favorites 세 개의 API를 세 번이나 찔러야 합니다. "아니, 프론트엔드가 딱 원하는 데이터(필드)만 고르고 골라서 한 번의 요청(POST)으로 싹 다 받아올 수는 없을까?" 페이스북(Meta)이 이 불만을 해결하기 위해 창조해 낸 차세대 쿼리 언어가 바로 GraphQL(그래프QL)입니다.
본 글에서는 "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의 끔찍한 함정
"우와! 프론트엔드가 원하는 데이터만 쏙쏙 뽑아주는 궁극의 기술이네요! 무조건 써야지!" 하지만 치명적인 문제가 도사리고 있습니다. 바로 에서 배웠던 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을 주고받으면 됩니다. 그런데, 앞서 에서 찢어놓은 서버 대 서버(Server to Server) 간의 통신은 어떨까요? 결제 서버와 재고 서버가 서로 초당 수만 건의 통신을 주고받는데, 여기에도 뚱뚱한 '글자(JSON)'를 쓰고 '무거운 HTTP/1.1'을 쓰려니 너무 느리고 비효율적입니다. "사람이 읽을 필요 없는 서버 간 통신은, 글자(JSON) 대신 컴퓨터가 0.001초 만에 읽는 바이너리(0과 1)로 암호화해서 쏘면 안 될까?" 본 설명에서는 "넷플릭스와 구글의 내부망 통신 표준!" MSA 통신의 극한의 속도를 끌어내는 gRPC 프로토콜과 Protocol Buffers 연동 완벽 가이드에 대해 아주 심도 있게 살펴보겠습니다.
우리는 지금까지 프론트엔드와 백엔드가 데이터를 주고받을 때 당연하다는 듯이 { "name": "철수", "age": 25 } 같은 JSON (JavaScript Object Notation) 텍스트 포맷을 사용해 왔습니다. 사람이 읽기 쉽고 브라우저에서 잘 돌아가기 때문입니다. 그런데 마이크로서비스(MSA) 환경에서 뒷단의 [주문 서버]와 [결제 서버]끼리 통신할 때도 과연 사람이 읽기 쉬운 JSON을 써야 할까요? "name": 이라는 껍데기 글자를 파싱(Parsing)하느라 서버의 CPU와 네트워크 대역폭이 엄청나게 낭비됩니다. 구글(Google) 아저씨들은 참다못해 "서버끼리 통신할 때는 사람 눈치 보지 마! 텍스트(JSON) 집어치우고 컴퓨터가 제일 좋아하는 0과 1(이진수, Binary)로 압축해서 쏴버려!"라는 미친 성능의 프레임워크를 세상에 공개합니다. 바로 gRPC(Google Remote Procedure Call)입니다.
본 글에서는 "MSA 서버 간 통신의 абсолютный 최강자!" gRPC 프로토콜 버퍼(Protobuf)와 Spring Boot 연동 실무 아키텍처에 대하여 다뤄보겠습니다.
1. 핵심 무기: Protocol Buffers (프로토버프)
gRPC는 JSON 대신 구글이 만든 Protocol Buffers(Protobuf)라는 직렬화 방식을 사용합니다. .proto라는 파일에 우리가 주고받을 데이터 규격을 적어두면, 자바, 파이썬, Go 등 어떤 언어의 코드로든 자동으로 변환(Compile)해 줍니다.
// src/main/proto/payment.proto
syntax = "proto3";
option java_package = "com.myapp.grpc";
// 1. 요청/응답 데이터 규격 (JSON 껍데기를 버리고 숫자와 타입만 정의)
message PaymentRequest {
int64 orderId = 1; // 변수명 뒤의 =1, =2 는 값이 아니라 '필드 순서(Tag)' 입니다!
int32 amount = 2;
}
message PaymentResponse {
bool success = 1;
string message = 2;
}
// 2. 서비스(인터페이스) 정의
service PaymentService {
// 결제 서버야, 내가 Request 줄테니 Response 줘! (REST의 POST 역할)
rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);
}
이 .proto 파일을 빌드(Gradle 플러그인)하면, 자바 파일(PaymentServiceGrpc.java 등)이 수천 줄 자동으로 생성됩니다. 이 생성된 클래스를 가져다 쓰기만 하면 됩니다.
2. Spring Boot gRPC 서버 구현하기
결제 서버(gRPC Server) 측에서는 스프링 컨트롤러(@RestController) 대신 gRPC 전용 서비스 어노테이션(@GrpcService)을 달아줍니다. (외부 라이브러리인 grpc-spring-boot-starter 사용)
// 결제 서버 (gRPC Server) - 보통 9090 포트로 띄웁니다.
@GrpcService
@Slf4j
public class PaymentGrpcServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {
@Override
public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
// 1. 주문 서버에서 넘어온 바이너리 데이터를 객체로 받아 꺼냅니다.
long orderId = request.getOrderId();
int amount = request.getAmount();
log.info("gRPC 요청 수신! 주문번호: {}, 금액: {}", orderId, amount);
// 2. 응답(Response) 객체를 빌더 패턴으로 예쁘게 굽습니다.
PaymentResponse response = PaymentResponse.newBuilder()
.setSuccess(true)
.setMessage("결제 성공! (초고속 바이너리 통신)")
.build();
// 3. 응답 발사 및 통신 종료 (콜백 방식)
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
주문 서버(gRPC Client)에서는 @GrpcClient 어노테이션을 통해 마치 로컬 서버에 있는 자바 함수를 호출(RPC, Remote Procedure Call)하듯 아주 자연스럽게 결제 서버의 메서드를 호출할 수 있습니다.
3. gRPC의 치명적 단점: 브라우저와 포스트맨의 눈물
성능은 압도적이지만 실무에서 모든 REST를 gRPC로 갈아엎지 못하는 이유가 있습니다. 프론트엔드(웹 브라우저)에서는 gRPC를 직접 호출할 수 없습니다. 브라우저의 자바스크립트는 바이너리를 던지는 HTTP/2 기반의 순수 gRPC 트래픽을 만들어낼 수 없기 때문입니다 (gRPC-Web 이라는 우회로가 있지만 매우 복잡합니다). 또한 포스트맨(Postman)으로 GET 쳐보면서 디버깅하던 달콤한 시절은 끝납니다. 바이트 배열이라 사람이 눈으로 디버깅할 수가 없기 때문입니다.
실무 결론: 프론트엔드와 백엔드의 통신은 여전히 REST(JSON) 또는 GraphQL을 쓰고, 백엔드 서버끼리 내부망에서 데이터를 미친 듯이 퍼 나를 때(Server-to-Server)만 gRPC를 도입하는 것이 글로벌 아키텍처 스탠다드입니다.
4. 마무리 및 주요 발전 방향
지금까지 뚱뚱한 JSON의 허물을 벗어던지고 Protobuf 바이너리로 압축하여 HTTP/2의 고속도로 위를 질주하는 MSA 내부 통신의 최강자, gRPC의 작동 원리와 Spring Boot 구현법에 대해 상세히 다루어 보았습니다. 이제 여러분의 마이크로서비스들은 0.001초의 낭비도 없이 완벽하게 데이터를 주고받게 되었습니다.
웹소켓()으로 채팅도 만들었고, gRPC()로 서버 간 통신도 극대화했습니다. 그런데 갑자기 사장님이 "우리도 줌(Zoom)이나 구글 미트(Google Meet)처럼 1:1 화상 회의(영상 통화) 기능을 앱에 넣어줘!"라고 지시합니다. 영상과 음성을 웹소켓이나 서버를 거쳐서(Relay) 스트리밍하면 스프링 서버는 데이터 트래픽 요금 폭탄을 맞고 파산할 것입니다.
"서버는 딱 처음 둘이 만날 수 있게 전화번호만 교환해 주고 빠지고, 영상 데이터는 철수 스마트폰에서 영희 스마트폰으로 다이렉트(P2P)로 쏴버릴 순 없을까?"
다음 글에서는 "서버 부하 0%의 기적! 실시간 화상 회의의 표준" WebRTC(웹알티씨)의 P2P 시그널링(Signaling) 서버를 Spring Boot WebSocket으로 구축에 대해 살펴보겠습니다.
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] 낙관적 락Optimistic vs 비관적 락Pessimistic JPA 동시성 제어 (0) | 2026.10.21 |
|---|---|
| [Spring Boot] Spring Cloud Gateway (0) | 2026.10.21 |
| [Spring Boot] Spring 클라우드 게이트웨이(Cloud Gateway) (0) | 2026.10.20 |
| [Spring Boot] Elasticsearch 연동 RDB의 LIKE 검색 (0) | 2026.10.20 |
| [Spring Boot] Spring Batch 입문 (0) | 2026.10.19 |