프론트엔드 통신 방식 (5): GraphQL과 통신 방식 최종 선택 가이드
시리즈의 마지막 편입니다. 지금까지 REST, 폴링/SSE, WebSocket, gRPC를 봤습니다. 이들은 대부분 방향성과 연결 수명 축에서 REST의 약점을 메웠습니다. 실시간이 필요해서, 양방향이 필요해서, 효율과 계약이 필요해서.
GraphQL은 다른 축에서 REST의 약점을 건드립니다. 바로 "클라이언트가 받는 데이터의 모양" 입니다. REST에서 화면마다 필요한 데이터가 다른데 엔드포인트는 고정되어 있어 생기는 오버페칭/언더페칭 문제를, GraphQL은 클라이언트가 필요한 것을 직접 쿼리하게 함으로써 풉니다.
이번 편에서는 GraphQL을 통신 방식의 하나로 정리하고, 마지막으로 시리즈 전체를 하나의 의사결정 가이드로 묶습니다.
이 글은 프론트엔드 통신 방식 시리즈의 5편(마지막)입니다.
- 1편: 전체 지도와 선택 축
- 2편: HTTP 위에서의 실시간 — Polling, Long Polling, SSE
- 3편: WebSocket 심화
- 4편: gRPC와 gRPC-Web, Protocol Buffers
- 5편: GraphQL과 통신 방식 최종 선택 가이드 (현재 글)
한눈에 보면
- GraphQL은 프로토콜이 아니라 쿼리 언어 + 타입 시스템입니다. 보통 단일 엔드포인트로 HTTP POST 위에서 동작합니다.
- 핵심 가치는 클라이언트 주도 데이터 페칭입니다. 필요한 필드만 정확히, 여러 자원을 한 번에 가져옵니다(오버/언더페칭 해소).
- 스키마가 계약이고, 거기서 타입을 생성한다는 점은 gRPC와 닮았지만 GraphQL은 텍스트(JSON)라 디버깅이 쉽습니다.
- 공짜가 아닙니다. HTTP 캐시를 잃고, 서버에 N+1 문제와 쿼리 복잡도 방어 부담이 생깁니다.
- 실시간은 Subscription으로 제공하지만, 그 전송은 결국 WebSocket(또는 SSE)이라 앞 편들의 운영 비용을 그대로 물려받습니다.
- 결국 모든 방식은 트레이드오프의 묶음이며, "REST로 충분한가?"에서 시작해 부족한 축을 메우는 방식을 더하는 것이 시니어의 사고 순서입니다.
GraphQL이 푸는 문제: 오버페칭과 언더페칭
REST의 구조적 약점부터 보겠습니다. 엔드포인트는 고정인데, 화면마다 필요한 데이터는 다릅니다.
오버페칭(over-fetching): 필요 이상으로 많이 받습니다. 사용자 이름만 필요한데 /users/1이 주소·결제정보까지 다 내려줍니다.
언더페칭(under-fetching): 한 번으로 부족해 여러 번 호출합니다. 게시글 + 작성자 + 댓글을 그리려면 /posts/1, /users/3, /posts/1/comments를 따로 불러야 합니다(워터폴).
GraphQL은 클라이언트가 필요한 모양을 그대로 기술하게 해서 둘 다 해결합니다.
# 필요한 필드만, 여러 자원을 한 번에
query PostScreen($id: ID!) {
post(id: $id) {
title
author {
name
avatarUrl
}
comments(first: 5) {
body
author {
name
}
}
}
}응답은 요청과 같은 모양으로 옵니다.
{
"data": {
"post": {
"title": "GraphQL 정리",
"author": { "name": "Marco", "avatarUrl": "..." },
"comments": [{ "body": "좋네요", "author": { "name": "Lee" } }]
}
}
}한 번의 요청으로, 정확히 필요한 데이터만, 원하는 그래프 모양으로 받습니다. 프론트엔드 입장에서 이건 강력합니다. 화면이 바뀌어도 백엔드에 새 엔드포인트를 요청할 필요가 없습니다. 쿼리만 바꾸면 됩니다.
GraphQL의 정체: 프로토콜이 아니라 계약 언어
오해를 먼저 정리합시다. GraphQL은 WebSocket이나 gRPC 같은 전송 프로토콜이 아닙니다. 대부분 HTTP POST로, 단일 엔드포인트(/graphql)에 쿼리 문자열을 보냅니다.
즉 통신의 "전송 계층"은 평범한 HTTP이고, GraphQL이 바꾸는 것은 요청과 응답의 구조와 계약입니다. 이 점에서 GraphQL은 1편의 세 축 중 페이로드/계약 축에 속하는 기술입니다.
GraphQL의 계약은 스키마입니다.
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
type Subscription {
userUpdated(id: ID!): User
}스키마가 곧 계약이고 거기서 타입을 생성한다는 점은 gRPC의 .proto와 같은 발상입니다. 차이는:
- gRPC: 바이너리, 함수 호출(RPC), 디버깅 어려움
- GraphQL: 텍스트(JSON), 그래프 쿼리, 디버깅 쉬움(네트워크 탭에서 그대로 읽힘)
프론트엔드에서는 GraphQL Code Generator 같은 도구로 쿼리에서 타입 안전한 훅을 생성하는 것이 표준입니다.
// 생성된 타입 안전 훅 사용 (Apollo/urql 등)
const { data, loading, error } = usePostScreenQuery({
variables: { id: '1' },
});
// data?.post?.author.name 까지 전부 타입 추론됨공짜가 아니다: GraphQL의 트레이드오프
GraphQL의 유연함은 비용을 동반합니다. 시니어가 도입 전에 반드시 따지는 항목들입니다.
1. HTTP 캐시를 잃는다
REST의 큰 장점은 GET 요청을 URL 단위로 CDN·브라우저가 캐시한다는 점입니다. GraphQL은 보통 모든 요청이 같은 URL로 가는 POST라, 이 무료 HTTP 캐시를 못 씁니다.
그 대신 GraphQL은 클라이언트 정규화 캐시(Apollo Client, urql, Relay)로 캐싱을 옮깁니다. 각 객체를 id 기준으로 정규화해 메모리에 저장하고 재사용하는 방식입니다. 강력하지만, 캐시 책임이 인프라(CDN)에서 클라이언트 코드로 이동한다는 뜻입니다. 캐시 무효화 설정이 복잡해질 수 있습니다.
2. N+1 문제
클라이언트가 그래프를 자유롭게 요청하므로, 서버는 중첩 필드를 풀다가 데이터베이스를 과도하게 조회할 수 있습니다. 게시글 10개의 author를 각각 조회하면 쿼리가 1 + 10번 나갑니다(N+1). 이건 DataLoader 같은 배칭/캐싱 계층으로 막아야 하며, 서버 구현의 필수 과제가 됩니다. REST에는 없던 부담입니다.
3. 쿼리 복잡도 = 공격 표면
클라이언트가 쿼리 모양을 정한다는 건, 악의적 클라이언트가 깊게 중첩되거나 거대한 쿼리를 보내 서버를 마비시킬 수 있다는 뜻입니다. 그래서 운영 GraphQL은 쿼리 깊이 제한, 복잡도 점수, 영속 쿼리(persisted query) 같은 방어가 필요합니다. 이는 보안 관점에서 REST보다 신경 쓸 게 많다는 의미입니다.
4. 관측과 레이트 리밋이 까다롭다
모든 요청이 같은 URL·메서드이므로, "어떤 API가 느린가/얼마나 호출되는가"를 엔드포인트 단위로 보던 기존 관측·레이트리밋 도구가 잘 안 맞습니다. 필드/오퍼레이션 단위의 별도 관측이 필요합니다.
요약하면, GraphQL은 프론트엔드의 유연함을 위해 서버·운영 복잡도를 가져갑니다. 이 교환이 맞는지가 핵심 질문입니다.
N+1을 실제 코드로: DataLoader
N+1은 말로는 추상적이지만 코드로 보면 명확합니다. 게시글 목록을 그리며 각 글의 작성자를 가져온다고 합시다. 순진한 리졸버는 이렇게 생겼습니다.
const resolvers = {
Post: {
// 게시글 N개면 이 리졸버가 N번 호출되고, 매번 DB를 1번 친다 → 1 + N
author: (post) => db.user.findById(post.authorId),
},
};게시글 10개면 작성자 조회만 10번, 목록 쿼리 1번까지 11번입니다. 작성자가 겹쳐도 중복 조회합니다. DataLoader는 같은 틱(tick) 동안 들어온 key를 모아서 한 번에 조회(batch)하고 중복을 캐시해 이 문제를 풉니다.
import DataLoader from 'dataloader';
// 요청(request)마다 새로 생성해야 한다 — 사용자 간 캐시 오염 방지
function createUserLoader() {
return new DataLoader<string, User>(async (ids) => {
// ids: ['3','3','7','9'] → 중복 제거 후 한 번의 IN 쿼리
const users = await db.user.findByIds([...new Set(ids)]);
const map = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => map.get(id)!); // 입력 순서대로 정렬해 반환(계약)
});
}
const resolvers = {
Post: {
author: (post, _args, ctx) => ctx.loaders.user.load(post.authorId),
},
};여기서 프론트엔드 개발자도 알아야 할 두 가지 함정이 있습니다.
- DataLoader는 요청 단위로 생성해야 합니다. 전역으로 두면 사용자 A의 데이터가 사용자 B에게 캐시되어 권한 누수가 납니다. 보안 사고로 이어지는 단골 실수입니다.
- DataLoader는 반환 배열을 입력 key와 같은 순서·같은 길이로 맞춰야 합니다. 이 계약을 어기면 엉뚱한 작성자가 붙습니다.
핵심: GraphQL의 유연한 쿼리는 서버에 N+1이라는 숙제를 항상 남깁니다. 프론트엔드가 깊은 중첩 쿼리를 자유롭게 쓸 수 있는 건, 백엔드가 DataLoader로 이 비용을 흡수해 주기 때문입니다. 이 분업을 모르면 "내 쿼리가 왜 느리지"의 원인을 영원히 못 찾습니다.
캐시를 잃은 대가: 정규화 캐시와 그 함정
REST의 HTTP 캐시를 잃은 GraphQL은 클라이언트 정규화 캐시로 보상한다고 했습니다. 그런데 이게 강력한 만큼 함정도 많습니다.
정규화 캐시는 모든 객체를 __typename:id(예: User:3)를 키로 평탄하게 저장합니다. 그래서 한 화면에서 갱신한 User:3이 다른 화면에도 자동 반영됩니다. 이게 마법처럼 동작하다가, 다음 경우에 깨집니다.
// 문제: id가 없는 타입은 정규화 키를 못 만든다
query {
dashboard {
stats { totalUsers activeNow } # id 없음 → 캐시가 부모에 묶여 공유 안 됨
}
}id가 없는 타입은 정규화되지 않아 캐시 공유·갱신이 안 됩니다. 스키마 설계 단계에서 안정적 식별자를 노출해야 합니다.- 목록에 항목을 추가/삭제하면, 개별 객체 캐시는 갱신돼도 "그 객체가 이 목록에 속한다"는 관계는 자동으로 안 바뀝니다. mutation 후
update콜백이나 목록 재요청으로 직접 손봐야 합니다.
// Apollo: mutation 후 캐시의 목록을 직접 갱신
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data }) {
cache.modify({
fields: {
todos(existing = []) {
const ref = cache.writeFragment({ data: data.addTodo, fragment: TODO_FRAGMENT });
return [...existing, ref];
},
},
});
},
});시니어의 관점: "캐시 무효화가 어렵다"는 GraphQL의 가장 현실적인 운영 비용입니다. REST에서는 "관련 URL을 무효화"하면 끝이던 것이, 정규화 캐시에서는 객체·목록·관계를 따로 추론해야 합니다. 캐시 갱신 코드가 비즈니스 로직만큼 길어지는 일이 흔합니다.
부분 성공이라는 낯선 개념: errors와 data 공존
REST는 보통 "성공이면 2xx + 데이터, 실패면 4xx/5xx + 에러"로 깔끔합니다. GraphQL은 다릅니다. 하나의 쿼리가 여러 필드를 동시에 풀다 보니, 일부는 성공하고 일부는 실패할 수 있습니다. 그래서 응답에 data와 errors가 함께 올 수 있습니다.
{
"data": { "post": { "title": "정리", "author": null } },
"errors": [{ "message": "author 조회 권한 없음", "path": ["post", "author"] }]
}게다가 전송은 대부분 HTTP 200입니다. 즉 "200이면 성공"이라는 가정으로 짠 에러 핸들링이 GraphQL에서는 통째로 틀립니다.
// GraphQL은 200 + errors가 정상 시나리오 — data와 errors를 함께 본다
const { data, error } = useQuery(POST_QUERY, { variables: { id } });
if (error?.networkError) return <NetworkError />; // 전송 자체 실패
if (error?.graphQLErrors?.length) logFieldErrors(error.graphQLErrors); // 부분 실패
return <Post post={data?.post} authorUnavailable={!data?.post?.author} />; // 부분 데이터 렌더이 "부분 성공"은 양날의 검입니다. 화면 일부만 깨지고 나머지는 보여줄 수 있어 회복탄력성이 좋아지지만, 모든 필드가 null일 수 있다는 전제로 UI를 짜야 해서 방어 코드가 늘어납니다.
공개 API라면: persisted query와 복잡도 방어
클라이언트가 쿼리 모양을 정한다는 자유는, 공개 환경에서 공격 표면이 됩니다. 이론이 아니라 실제 방어가 필요합니다.
- Persisted Query(영속 쿼리): 허용된 쿼리들을 빌드 타임에 서버에 등록하고, 런타임에는 쿼리 전문 대신 해시만 보냅니다. 그러면 (1) 임의의 악성 쿼리를 원천 차단하고, (2) 페이로드도 줄고, (3)
GET+ 해시로 보내 CDN 캐시까지 일부 복원할 수 있습니다. 내부 앱이 아닌 공개 서비스라면 사실상 필수입니다. - 깊이/복잡도 제한: 중첩 깊이 상한, 필드별 비용 점수 합산으로 거대 쿼리를 거부합니다.
- 타임아웃·레이트리밋: 오퍼레이션 단위로 적용해야 하므로 엔드포인트 기반 도구로는 부족합니다.
핵심: 사내 도구라면 GraphQL의 자유를 거의 그대로 누려도 되지만, 공개 API라면 위 방어가 없는 GraphQL은 위험합니다. 이 차이를 모르고 공개에 올리는 것이 흔한 실수입니다.
GraphQL의 실시간: Subscription
GraphQL도 실시간을 제공합니다. Subscription이 그것입니다.
subscription OnUserUpdate($id: ID!) {
userUpdated(id: $id) {
id
name
}
}하지만 여기서 시리즈 전체가 연결됩니다. Subscription의 전송 자체는 GraphQL이 만드는 게 아니라, 결국 WebSocket(주로 graphql-ws 프로토콜) 또는 SSE 위에서 동작합니다.
즉 GraphQL Subscription을 쓰는 순간, 3편에서 다룬 WebSocket의 모든 운영 비용 — 재연결, 하트비트, sticky session, pub/sub 백플레인 — 을 그대로 물려받습니다. GraphQL이 그 복잡도를 없애주지 않습니다. 단지 쿼리 언어로 감쌀 뿐입니다.
클라이언트 코드만 보면 이 비용이 안 보이는 게 함정입니다. graphql-ws로 구독하면 표면은 이렇게 단순합니다.
import { createClient } from 'graphql-ws';
const wsClient = createClient({
url: 'wss://api.example.com/graphql',
connectionParams: () => ({ authToken: getAccessToken() }), // 인증 페이로드
retryAttempts: Infinity, // 재연결은 라이브러리가 처리(겉보기엔)
});
const unsubscribe = wsClient.subscribe(
{ query: 'subscription { userUpdated(id: "1") { id name } }' },
{
next: (data) => applyUpdate(data),
error: (err) => console.warn(err),
complete: () => console.log('구독 종료'),
}
);겉으로는 SSE만큼 간단해 보이지만, 이 뒤에는 3편의 모든 것이 그대로 있습니다. 끊긴 구간에 놓친 이벤트는 여전히 유실되고, 서버 쪽에는 sticky session과 pub/sub 백플레인이 필요하며, 토큰 만료 시 재인증도 설계해야 합니다. 라이브러리가 재연결을 "처리"한다는 말은 "끊김을 감지해 다시 연결한다"는 뜻이지 "그 사이 데이터를 보장한다"는 뜻이 아닙니다.
그래서 실무에서는 간단한 단방향 실시간은 SSE 위의 GraphQL 구독으로 가는 선택지도 많습니다.
graphql-sse같은 구현을 쓰면 WebSocket의 양방향 운영 부담 없이 단방향 푸시를 받을 수 있습니다. "Subscription = WebSocket"이라는 등식이 항상 참은 아닙니다.
이 사실은 중요한 교훈을 줍니다. 추상화는 하위 계층의 트레이드오프를 숨길 뿐 없애지 못합니다. 어떤 통신 방식을 고르든, 그 아래 깔린 메커니즘의 비용을 이해해야 하는 이유입니다.
종합: 하나의 의사결정 가이드
이제 시리즈 전체를 묶겠습니다. 먼저 한 장의 비교표입니다.
| 항목 | REST | SSE | WebSocket | gRPC(-Web) | GraphQL |
|---|---|---|---|---|---|
| 패러다임 | 자원 중심 | 서버 푸시 | 양방향 메시지 | 함수(RPC) | 그래프 쿼리 |
| 방향성 | 요청-응답 | 단방향 푸시 | 양방향 | 단항/스트리밍 | 요청-응답(+구독) |
| 연결 | 단명 | 장기 | 장기 | 장기(스트림) | 단명(+구독은 WS/SSE) |
| 페이로드 | JSON | 텍스트 | 자유 | 바이너리 | JSON |
| 계약 강제 | 약함(문서/OpenAPI) | 약함 | 약함 | 강함(.proto) | 강함(스키마) |
| 코드 생성 | 선택 | — | — | 기본 | 기본 |
| HTTP 캐시/CDN | ✅ 강함 | ⚠️ | ❌ | ❌ | ❌(클라 캐시로) |
| 브라우저 지원 | ✅ | ✅ | ✅ | ⚠️ 프록시 | ✅ |
| 자동 재연결 | N/A | ✅ 내장 | ❌ 직접 | ❌ | 라이브러리 의존 |
| 디버깅 | 쉬움 | 쉬움 | 중간 | 어려움(바이너리) | 중간 |
| 주된 약점 해소 | 기준점 | 단방향 실시간 | 양방향 실시간 | 효율·계약 | 오버/언더페칭 |
의사결정 순서
표보다 더 중요한 건 묻는 순서입니다. 시니어는 대체로 이렇게 좁혀갑니다.
graph TD
Q1[새 기능 설계 시작] --> Q2{서버가 먼저<br/>밀어줘야 하나?}
Q2 -->|아니오| Q3{데이터 모양이<br/>화면마다 크게 다른가?}
Q3 -->|아니오| REST[REST<br/>가장 단순·캐시 친화]
Q3 -->|예, 그래프가 복잡| GQL[GraphQL<br/>클라 주도 쿼리]
Q2 -->|예| Q4{클라이언트도 자주<br/>즉시 보내야 하나?}
Q4 -->|아니오| SSE[SSE<br/>단방향 실시간]
Q4 -->|예| WS[WebSocket<br/>양방향 실시간]
REST --> Q5{내부 서비스 간<br/>·강한 계약·효율?}
Q5 -->|예| GRPC[gRPC / Connect]이 트리에서 읽어야 할 핵심은 "REST가 기본값이고, 명확한 부족함이 있을 때만 벗어난다" 입니다. 화려한 기술을 먼저 고르고 정당화하는 게 아니라, 가장 단순한 것에서 출발해 필요한 만큼만 복잡도를 더합니다.
미리 알아두면 좋은 함정
아래는 직접 운영하며 겪은 사례가 아니라, 명세와 자료에서 공통적으로 지적되는 함정들입니다. 앞 절의 N+1 계산(1 + N)과 부분 실패 응답 예시처럼, 가능한 부분은 코드로 직접 따져본 것을 근거로 합니다.
함정 1: 캐시 무효화 비용. 데이터 모양이 크게 다르지 않은 서비스를 "프론트 편의"만 보고 GraphQL로 옮기면, 정작 개발 시간의 상당 부분이 정규화 캐시를 손으로 갱신하는 코드로 갈 수 있습니다. mutation마다 어떤 목록·관계를 건드릴지 추론하는 일이 비즈니스 로직보다 길어지기 쉽습니다. 데이터 모양이 화면마다 크게 다르지 않다면, REST + 적절한 캐시가 총비용이 더 쌉니다.
함정 2: 백엔드 배칭이 없을 때의 N+1. 개발 DB에서는 빠르던 화면이, 중첩을 한 단계 더 넣은 쿼리를 추가하는 순간 느려질 수 있습니다. 서버에 DataLoader가 없으면 한 화면이 DB를 수백 번 조회하기도 합니다. GraphQL의 유연함은 백엔드의 배칭 규율과 한 세트라서, 프론트·백이 N+1 분업을 공유하지 않으면 성능 문제로 이어집니다.
함정 3: 공개 API의 쿼리 남용. 사내용으로 만든 GraphQL을 방어 없이 외부에 열면, 깊게 중첩된 거대 쿼리 하나로 서버에 큰 부하를 줄 수 있습니다. 공개 GraphQL은 persisted query + 복잡도 제한이 없으면 위험하며, 이건 옵션이 아니라 전제로 보는 편이 안전합니다.
함정 4: 200인데 실패. "200이면 성공"이라는 가정으로 짠 에러 처리는 GraphQL의 부분 실패(200 + errors)를 통째로 놓쳐, 사용자에게 빈 화면이 조용히 뜰 수 있습니다. GraphQL을 쓰면 에러 모델 자체를 다시 설계해야 합니다.
실무는 결국 조합이다
1편에서 말했듯 실제 서비스는 단일 방식이 아닙니다. 전형적인 모습:
- 대다수 요청: REST (또는 GraphQL) — CRUD, 조회, 폼 제출
- 일부 실시간 알림/피드/AI 스트리밍: SSE
- 소수의 진짜 양방향 화면(채팅/협업): WebSocket
- 내부 마이크로서비스 간: gRPC
프론트엔드의 역량은 "하나의 정답을 아는 것"이 아니라, 각 화면의 통신 성격을 진단하고 거기에 맞는 방식을 고르며, 그 운영 비용을 예측하는 것입니다.
시니어 관점에서 한 번 더 강조할 것
이 시리즈를 관통하는 세 가지 원칙으로 마무리하겠습니다.
-
단순함은 기본값이다. REST·SSE처럼 표준이 많은 걸 대신해주는 방식이, 직접 다 만들어야 하는 방식보다 거의 항상 운영이 쉽습니다. 복잡도는 명확한 이유가 있을 때만 빌립니다.
-
추상화는 비용을 숨길 뿐 없애지 않는다. GraphQL Subscription이 WebSocket 비용을, gRPC-Web이 프록시 비용을 숨기듯, 편리한 표면 아래의 트레이드오프를 이해해야 장애를 다룰 수 있습니다.
-
선택의 진짜 기준은 운영이다. 실패 모드, 인프라 호환성, 디버깅 가능성, 팀 역량 — 벤치마크 숫자보다 이것들이 1년 뒤의 행복을 결정합니다.
자주 하는 오해
1. GraphQL은 REST를 대체하는 상위 호환이다
아닙니다. GraphQL은 오버/언더페칭이라는 특정 문제를 풀고, 대신 캐시·N+1·복잡도 방어 비용을 가져옵니다. 단순 CRUD에는 REST가 더 적합한 경우가 많습니다.
2. GraphQL은 그 자체로 실시간 기술이다
아닙니다. Subscription의 전송은 WebSocket이나 SSE이고, 그 운영 비용을 그대로 물려받습니다.
3. GraphQL을 쓰면 백엔드가 단순해진다
아닙니다. 클라이언트가 단순해지는 만큼 서버에 N+1, 복잡도 제한, 필드 단위 관측 같은 부담이 옮겨갑니다.
4. 좋은 통신 방식 하나를 고르면 끝이다
아닙니다. 성숙한 서비스일수록 화면 성격에 따라 여러 방식을 조합합니다. 중요한 건 진단하고 조합하는 감각입니다.
정리하면
GraphQL은 전송 프로토콜이 아니라, 클라이언트 주도 쿼리로 REST의 데이터 모양 문제를 푸는 계약 언어입니다.
짧게 정리하면:
- 오버/언더페칭 해소가 핵심 가치, 대가는 HTTP 캐시 상실·N+1·복잡도 방어입니다.
- 스키마 기반 계약·코드 생성은 gRPC와 닮았지만, 텍스트라 디버깅이 쉽습니다.
- 실시간(Subscription)은 결국 WebSocket/SSE 위에 있어 그 비용을 그대로 물려받습니다.
- 시리즈 전체의 결론: REST를 기본값으로, 부족한 축을 명확히 진단한 뒤에만 SSE/WebSocket/gRPC/GraphQL을 더하라.
통신 방식은 외워야 할 기술 목록이 아니라, 방향성·연결 수명·계약이라는 축 위에서 트레이드오프를 고르는 일입니다. 이 다섯 편이 그 저울을 손에 쥐는 데 도움이 되었길 바랍니다.
