REST, gRPC, GraphQL - 선택 기준과 각자가 내는 비용
세 가지를 “REST는 무난하고, gRPC는 빠르고, GraphQL은 유연하다”로 요약하면 선택에 쓸 수 없다. 셋은 같은 문제의 다른 답이 아니라 서로 다른 문제를 풀고, 각자 비용을 다른 곳에 놓는다. 그 비용이 어디에 놓이는지가 선택 기준이다.
세 가지가 실제로 다른 점
| REST | gRPC | GraphQL | |
|---|---|---|---|
| 계약 | 느슨함(OpenAPI는 선택) | .proto가 강제 | 스키마가 강제 |
| 전송 | HTTP/1.1 또는 2, JSON | HTTP/2, protobuf | 주로 HTTP/1.1, JSON |
| 호출 모양 | 리소스 중심 | 메서드 호출 | 질의 |
| 응답 형태 | 서버가 정함 | 서버가 정함 | 클라이언트가 정함 |
| 스트리밍 | SSE·WebSocket 별도 | 양방향 내장 | subscription |
| 브라우저 | 그대로 | 프록시 필요(gRPC-Web) | 그대로 |
| 캐싱 | HTTP 캐시 그대로 | 직접 구현 | 어려움(POST 단일 엔드포인트) |
표의 차이는 결국 세 갈래다. REST는 HTTP의 의미를 그대로 쓴다. gRPC는 타입과 성능을 얻는 대신 HTTP의 의미를 버린다. GraphQL은 응답 모양의 결정권을 클라이언트에 넘긴다.
REST가 공짜로 얻는 것
HTTP를 그대로 쓰기 때문에 주변 생태계가 전부 동작한다.
- 브라우저,
curl, 프록시, CDN, API 게이트웨이가 별도 작업 없이 붙는다. - 상태 코드와 메서드 의미가 표준이다. 멱등성과 재시도 규약이 여기서 나온다(멱등한 API 설계).
Cache-Control,ETag로 캐싱이 무료다.- 관측 도구가 URL과 상태 코드 기준으로 이미 만들어져 있다.
대가는 계약이 느슨하다는 것이다. OpenAPI를 쓰지 않으면 필드 하나가 바뀐 것을 컴파일러가 잡아 주지 않는다. 그리고 리소스 모델에 안 맞는 동작(예: “정산 마감을 실행한다”)을 억지로 명사로 만드는 문제가 늘 남는다.
gRPC가 가져오는 것과 잃는 것
.proto 하나에서 서버와 클라이언트 코드가 생성된다. 필드를 지우면 빌드가 깨진다. 서비스가 많고 팀이 여럿일 때 이 강제력이 가장 큰 이점이다.
HTTP/2 위에서 동작하므로 연결 하나에 스트림이 여럿 흐르고, 헤더가 압축되며, 양방향 스트리밍이 기본이다. 직렬화가 바이너리라 payload가 작고 파싱이 빠르다.
잃는 것이 분명하다.
- 브라우저에서 직접 못 쓴다. gRPC-Web과 프록시가 필요하다.
- HTTP 캐싱을 못 쓴다. 캐시가 필요하면 직접 만든다.
- 디버깅이 불편하다. 바이너리라
curl로 못 보고grpcurl같은 도구가 필요하다. - HTTP/2 커넥션 하나에 스트림이 몰려 L4 로드 밸런서가 부하를 못 나눈다. gRPC 공식 블로그의 로드 밸런싱 글에 따르면 L3/L4 LB는 클라이언트 연결의 HTTP/2·gRPC 프레임을 고른 백엔드 연결로 그대로 복사한다. 그래서 연결 하나는 서버 하나로 간다. 한 클라이언트의 스트림을 여러 백엔드로 나누는 것은 HTTP/2를 해석하는 L7 LB의 일이다. 따라서 L7 프록시나 클라이언트 측 분산이 필요하다.
GraphQL이 옮기는 비용
클라이언트가 필요한 필드만 질의하므로 over-fetching이 사라지고, 화면 하나를 그리기 위한 왕복(under-fetching)도 준다. 화면 요구가 자주 바뀌는 프런트엔드에 유리하다.
비용은 서버로 옮겨간다.
- N+1 질의. 중첩 필드마다 리졸버(필드 값을 채우는 함수)가 돌면서 DB를 친다. GraphQL 문서도 이 문제를 짧은 시간 동안 모은 요청을 한 번에 보내는 배치 기법, 예컨대 DataLoader로 푼다고 설명한다. 이런 배치·캐시 계층이 사실상 필수다.
- 질의 비용의 상한이 없다. 깊게 중첩된 질의 하나가 서버를 마비시킬 수 있다. 깊이 제한, 복잡도 점수, 영속 질의(persisted query)가 방어 장치다.
- 캐싱이 어렵다. 단일 엔드포인트에 POST가 기본이라 HTTP 캐시가 안 붙는다. 같은 문서에 따르면 질의를 GET으로도 받는 구현이라면 캐시 헤더를 붙여 HTTP 캐시나 CDN을 쓸 수 있지만, 이는 구현이 선택하는 기능이다.
- 관측이 어렵다. 모든 요청이
POST /graphql이라 URL 기준 지표가 무의미하다. 오퍼레이션 이름을 지표 라벨로 써야 하고, 그러면 카디널리티를 신경 써야 한다.
고르는 기준
질문은 다음 순서로 던진다.
- 클라이언트가 브라우저인가? 그렇다면 gRPC는 프록시 비용을 먼저 계산한다.
- 호출자가 내부 서비스인가 외부 개발자인가? 외부 공개 API는 REST가 진입 장벽이 가장 낮다.
- 계약 위반을 빌드 타임에 잡아야 하는가? 서비스 수가 많고 변경이 잦으면 gRPC나 스키마 강제가 값을 한다.
- 화면마다 필요한 데이터가 크게 다른가? 그렇다면 GraphQL의 이점이 실제로 있다. 화면이 몇 개 안 되면 REST 엔드포인트 몇 개가 더 싸다.
- 스트리밍이 필요한가? 양방향이면 gRPC, 서버→클라이언트 단방향이면 SSE(Server-Sent Events)로 충분한 경우가 많다.
셋은 배타적이지 않다. 외부는 REST, 내부 서비스 간은 gRPC, BFF(화면별 백엔드) 계층만 GraphQL 같은 조합이 흔하다.
이 설명이 깨지는 곳
- “gRPC가 빠르다”는 대개 직렬화와 HTTP/2 이야기다. 전체 지연에서 그 비중이 작다면(DB가 대부분) 체감 차이가 없다.
- protobuf의 호환 규칙을 지켜야 계약의 이점이 산다. 필드 번호 재사용, 타입 변경은 조용히 깨진다(API 버저닝).
- REST도 스키마를 강제할 수 있다. OpenAPI에서 코드를 생성하면 gRPC와 비슷한 강제력을 얻는다. 차이가 절대적이지 않다.
- GraphQL의 유연성은 권한 검사를 어렵게 만든다. 필드 단위 인가를 리졸버마다 걸어야 하고, 한 곳만 빠져도 노출이 된다.
무엇을 재면 확인되는가
- 같은 응답을 JSON과 protobuf로 만들어 크기와 직렬화 시간을 비교한다. 전체 지연에서 차지하는 비중까지 봐야 의미가 있다.
- gRPC를 L4 LB 뒤에 두고 서버별 요청 분포를 본다. 쏠림이 실제로 보인다.
- GraphQL 질의의 깊이를 늘려 가며 DB 질의 수를 센다. DataLoader 전후로.
실무와의 접점
JSP 기반 시스템의 구조 전환에서 조회 경로를 옮길 때 가장 어려웠던 것은 프로토콜이 아니라 기존 응답 계약을 깨지 않는 것이었다. 세 방식 중 무엇을 고르든 그 문제는 남는다. 그래서 프로토콜 선택은 성능 비교표보다 계약을 어떻게 강제하고 진화시킬지를 먼저 묻는 일이 됐다.
정리
- gRPC의 L4 분산 문제는 연결 단위 분산에서 나오므로, 도입 전에 LB가 L7인지부터 확인한다.
- GraphQL의 비용(N+1, 질의 비용 상한, 관측)은 클라이언트가 아니라 서버 팀이 받는다.
- 성능 차이는 직렬화가 전체 지연에서 차지하는 비중을 재기 전까지는 근거가 아니다.
댓글
아직 댓글이 없습니다