REST
클라이언트가 요청하고 서버가 응답한다. HTTP 메서드, 상태 코드, 캐시, 브라우저 도구를 그대로 쓴다.
공개 API, 단순 조회/수정, 외부 파트너 연동분류: Layer 1 - 백엔드 기초 | 작성일: 2026-04-02
WebSocket은 HTTP 요청으로 시작해 101 Switching Protocols 응답을 받은 뒤, 같은 TCP 연결을 지속 연결로 바꾸어 클라이언트와 서버가 서로 먼저 메시지를 보낼 수 있게 하는 양방향 통신 프로토콜이다.
gRPC는 Protobuf(Protocol Buffers) 스키마로 요청과 응답 타입을 먼저 정하고, HTTP/2 스트림 위에서 원격 프로시저 호출(RPC, Remote Procedure Call)을 수행하는 서버 간 통신 프레임워크다.
둘 다 HTTP를 완전히 버리는 기술은 아니다. WebSocket은 HTTP 핸드셰이크로 시작해 WebSocket 프레임으로 전환하고, gRPC는 HTTP/2의 연결·스트림·메타데이터를 사용한다. 차이는 HTTP의 요청-응답 모델만으로 부족한 문제를 어디서 푸는가다.
HTTP 문서에서 본 기본 모델은 “클라이언트가 요청하고 서버가 응답한다”이다. 이 모델은 공개 API, 브라우저 요청, 단순 CRUD에는 강하다. 하지만 다음 두 가지 상황에서는 부담이 커진다.
예를 들어 주문 상태를 화면에 보여준다고 하자.
HTTP polling:브라우저 -> GET /orders/7/status브라우저 -> GET /orders/7/status브라우저 -> GET /orders/7/status...주문 상태는 10분 동안 한 번만 바뀔 수 있다. 그런데 클라이언트가 5초마다 묻는다면 한 사용자당 분당 12번의 요청이 생긴다. 10,000명이 같은 방식으로 접속하면 상태가 바뀌지 않아도 분당 120,000번의 HTTP 요청이 발생한다. 응답 대부분은 “아직 그대로”라는 빈 확인이다.
WebSocket은 이 문제를 지속 연결 + 서버 push로 푼다. 연결을 한 번 열어두고, 상태가 바뀔 때 서버가 먼저 메시지를 보낸다.
WebSocket:브라우저 -> GET /orders/7/stream, Upgrade: websocket서버 -> 101 Switching Protocols서버 -> {"orderId":7,"status":"DELIVERED"} # 바뀐 순간에만 전송gRPC가 풀려는 문제는 조금 다르다. 내부 서비스가 REST/JSON으로 서로 호출하면 사람은 읽기 쉽지만, 고빈도 호출에서는 JSON 파싱·필드명 반복·스키마 불일치가 비용이 된다.
REST/JSON:POST /inventory/reserve{ "orderId": "o-7", "sku": "keyboard", "quantity": 1 }
gRPC:InventoryService.Reserve(ReserveRequest)gRPC는 “HTTP 경로와 JSON 모양을 호출마다 맞추자”가 아니라, .proto 파일에 함수처럼 보이는 RPC와 메시지 타입을 먼저 정의한다. 호출자는 언어가 달라도 같은 스키마에서 생성된 코드를 사용하므로 타입 불일치를 더 빨리 발견할 수 있고, 전송은 HTTP/2 스트림과 Protobuf 이진 포맷을 사용해 작아진다.
첫 회독의 목표는 “NestJS에서 실행하는 법”이나 운영 세부를 모두 익히는 것이 아니라, 다음 질문에 답할 수 있게 되는 것이다.
필드 번호 호환성, 네 가지 streaming의 종료 규칙, heartbeat·backpressure·재연결·fan-out, deadline·status·retry는 본문에 보존한 선택 심화다. 처음에는 이름과 실패 신호를 알아보고, 실제로 해당 통신 방식을 설계하거나 운영할 때 세부 메커니즘을 다시 확인하면 된다.
첫 회독에서는 3절, 4.14.3절, 5.15.3절, 5.4절의 비교표와 .proto 예시까지, 78절, 1011절과 13절의 첫 번째 체크리스트를 읽는다. 5.4절의 패턴별 상세 흐름부터는 4.44.8절, 5.55.7절, 6절, 9절, 12절과 함께 설계·구현·운영이 필요할 때 돌아오는 선택 심화다. 이 경로는 심화 내용을 삭제하지 않으면서도, 지속 연결과 typed RPC의 핵심 모델을 먼저 완성하게 한다.
기술 이름보다 먼저 통신 방향과 계약의 성격을 본다.
클라이언트가 요청하고 서버가 응답한다. HTTP 메서드, 상태 코드, 캐시, 브라우저 도구를 그대로 쓴다.
공개 API, 단순 조회/수정, 외부 파트너 연동Server-Sent Events. 일반 HTTP 연결로 서버가 클라이언트에게만 이벤트를 스트리밍한다.
진행률, 로그, 알림처럼 단방향 push면 충분할 때하나의 지속 연결에서 양쪽이 동시에 메시지를 주고받는다.
채팅, 협업 편집, 위치 추적처럼 양방향 실시간성이 필요할 때Protobuf 계약과 HTTP/2 스트림으로 서버 간 RPC를 수행한다.
내부 마이크로서비스, 고빈도 호출, 스트리밍, 다언어 팀짧은 결정 기준은 다음과 같다.
브라우저나 외부 파트너가 직접 호출하는 공개 API인가?├── 예: REST가 기본값└── 아니오: 서버가 클라이언트에게 먼저 자주 알려야 하는가? ├── 예, 단방향이면 SSE ├── 예, 양방향이면 WebSocket └── 아니오: 서버 간 고빈도 호출과 타입 계약이 중요한가? ├── 예: gRPC └── 아니오: REST이 흐름도는 절대 규칙이 아니다. 브라우저에서 gRPC를 직접 쓰기 어렵고, WebSocket은 연결 운영 비용이 크며, REST는 성능이 가장 낮다는 뜻이 아니다. 첫 판단을 좁힌 뒤 실제 제약을 확인하는 출발점이다.
HTTP polling은 클라이언트가 계속 묻는다. 구현은 단순하지만 이벤트가 드문데 클라이언트가 많으면 빈 요청이 급증한다.
10,000 clientspoll interval = 5srequests per minute = 10,000 * 60 / 5 = 120,000상태 변경이 실제로는 분당 200번뿐이라면 119,800번에 가까운 요청은 최신 상태를 다시 확인하는 비용이다. WebSocket은 이 비용을 “열린 연결을 유지하는 비용”으로 바꾼다. 요청 수는 줄지만 서버는 연결 객체, 파일 디스크립터, 메모리, heartbeat, 재연결 폭풍을 관리해야 한다.
따라서 WebSocket의 핵심 철학은 “모든 HTTP 요청을 없애자”가 아니다. 변경이 발생했을 때 즉시 알려야 하고, 양쪽이 계속 대화해야 하는 구간만 지속 연결로 바꾸자이다.
WebSocket은 처음부터 별도 포트의 낯선 프로토콜로 시작하지 않는다. 클라이언트가 HTTP 요청에 Upgrade: websocket 헤더를 실어 보내고, 서버가 받아들이면 101 Switching Protocols를 반환한다.
sequenceDiagram participant C as Client participant S as Server C->>S: GET /chat HTTP/1.1 C->>S: Upgrade: websocket C->>S: Connection: Upgrade C->>S: Sec-WebSocket-Key: random nonce S-->>C: 101 Switching Protocols S-->>C: Sec-WebSocket-Accept: derived value C->>S: WebSocket frame S-->>C: WebSocket frame
101 이후에도 TCP 연결은 그대로다. 달라지는 것은 그 연결 위에 실리는 메시지 형식이다. HTTP 요청/응답 메시지를 더 보내는 것이 아니라 WebSocket 프레임을 주고받는다.
이 경계는 장애 해석에서 중요하다.
| 관찰 | 의미 | 먼저 볼 것 |
|---|---|---|
101이 오지 않는다 | WebSocket으로 전환되기 전 실패 | HTTP 경로, 인증, proxy upgrade 헤더 |
101은 왔지만 곧 끊긴다 | 연결 유지 단계의 실패 | idle timeout, heartbeat, 서버 disconnect |
| 특정 메시지만 처리 안 된다 | 연결보다 애플리케이션 메시지 계약 문제 | event name, payload shape, authorization |
| 로드밸런서 뒤에서만 실패한다 | 중간 계층이 Upgrade나 idle 연결을 막을 수 | ALB/Nginx 설정, timeout, sticky session |
WebSocket은 전이중(Full-Duplex)이다. 전이중은 양쪽이 독립적으로 송신과 수신을 할 수 있다는 뜻이다. 클라이언트가 메시지를 보내는 중에도 서버가 별도 메시지를 보낼 수 있고, 서버가 먼저 알림을 밀어 넣을 수도 있다.
주의할 점은 WebSocket이 TCP 위에 올라간다는 것이다. TCP는 바이트 스트림이고, 자체적으로 “여기까지가 채팅 메시지 하나”라는 의미를 모른다. WebSocket 프레임은 이 바이트 스트림 위에 메시지 경계를 만들어준다.
애플리케이션 메시지:{ "type": "chat", "text": "hello" }
WebSocket frame:FIN=1, opcode=text, payload_length=32, payload=...
TCP:bytes, bytes, bytes ...프레임과 메시지는 같은 말이 아니다.
| 개념 | 의미 | 학습 포인트 |
|---|---|---|
| Frame | WebSocket 프로토콜이 전송하는 최소 단위 | opcode, FIN, payload length 같은 메타데이터를 가진다 |
| Message | 애플리케이션이 의미를 부여하는 단위 | 큰 메시지는 여러 frame으로 쪼개질 수 있다 |
| Event | Socket.IO 같은 라이브러리가 얹는 애플리케이션 추상 | sendMessage, receiveMessage 같은 이름은 표준 자체가 아니다 |
표준 WebSocket과 Nest adapter, Socket.IO의 계층도 구분해야 한다.
| 계층 | 메시지를 다루는 방식 | 제공하지 않는 것 |
|---|---|---|
| 표준 WebSocket 프로토콜 | text/binary message와 Ping/Pong 같은 control frame을 주고받는다 | named event, room, namespace |
Nest WsAdapter | { event, data } 형태의 메시지를 파싱해 @SubscribeMessage() handler로 라우팅한다 | room, namespace |
Nest IoAdapter / Socket.IO | Socket.IO의 named event·namespace와 서버 측 room API를 사용한다 | 표준 WebSocket client와의 직접 호환 |
WsAdapter의 event는 표준 WebSocket 기능이 아니라 Nest가 메시지 payload에 정한 라우팅 규약이다. 반면 IoAdapter의 event·room·namespace는 Socket.IO 프로토콜과 서버 API가 제공한다.
작은 반례를 보자. “TCP에서 한 번 send()하면 상대도 한 번 message 이벤트를 받는다”라고 생각하면 틀린다. TCP는 중간에서 나뉘거나 합쳐질 수 있다. WebSocket은 프레임 규칙으로 메시지 경계를 복원해 주지만, 그 안의 JSON 필드 의미는 애플리케이션이 직접 정해야 한다.
좋은 메시지 설계:{ "type": "order.status.changed", "orderId": 7, "status": "DELIVERED" }
나쁜 메시지 설계:{ "data": "done" }두 메시지 모두 전송은 가능하다. 하지만 두 번째는 수신자가 무엇이 완료됐는지, 멱등하게 다시 처리할 수 있는지, 사용자에게 어떤 상태를 보여줘야 하는지 알기 어렵다. WebSocket은 전송 채널을 주지만, 메시지 계약을 대신 설계해 주지는 않는다.
Backpressure는 생산자가 보내는 속도가 소비자가 처리하는 속도보다 빨라서 버퍼가 쌓이는 현상이다. WebSocket은 연결을 오래 유지하므로 이 문제가 HTTP 단건 요청보다 오래 숨어 있을 수 있다.
예를 들어 서버가 한 클라이언트에게 초당 1,000개의 가격 업데이트를 보내는데, 모바일 클라이언트가 초당 100개만 처리한다고 하자.
server send rate = 1,000 msg/sclient process rate = 100 msg/sbuffer growth = 900 msg/s1분 후 backlog = 54,000 messages이 상황에서 “연결은 살아 있다”는 관측만으로는 충분하지 않다. 연결은 살아 있지만 사용자는 1분 전 가격을 볼 수 있다. 그래서 실시간 시스템은 연결 생존과 데이터 최신성을 나눠서 본다.
Backpressure를 줄이는 방법은 메시지를 더 빨리 보내는 것이 아니라 덜 보내거나, 합치거나, 늦은 메시지를 버릴 수 있는 의미를 설계하는 것이다.
| 방법 | 언제 맞는가 | 경계 |
|---|---|---|
| sampling | 모든 중간값보다 최신 상태가 중요할 때 | 감사 로그처럼 모든 이벤트가 필요하면 부적합 |
| coalescing | 같은 key의 최신 값만 의미 있을 때 | 중간 상태 전이가 비즈니스 의미를 가지면 위험 |
| per-client buffer | 짧은 네트워크 흔들림을 흡수해야 할 때 | 한도 없이 키우면 메모리 장애가 된다 |
| drop policy | 오래된 메시지를 버려도 되는 스트림일 때 | 주문/결제 이벤트처럼 손실 불가면 부적합 |
| ack 기반 흐름 제어 | 수신자가 처리 완료를 알려야 할 때 | latency와 구현 복잡도가 늘어난다 |
실패 신호는 다음처럼 보인다.
오래 열린 연결은 “아무 데이터도 안 오지만 실제로 살아 있는 상태”와 “중간 네트워크 장비가 조용히 끊은 상태”를 구분해야 한다. Heartbeat는 이 구분을 위해 주기적으로 작은 생존 신호를 보내는 방식이다.
WebSocket 표준은 제어 프레임으로 Ping과 Pong을 정의한다.
sequenceDiagram participant S as Server participant C as Client S->>C: Ping opcode 0x9 C-->>S: Pong opcode 0xA S->>C: Ping Note over C: NAT or mobile sleep S-->>S: pingTimeout 동안 Pong 없음 S-->>C: close
수치 감각은 timeout의 상대값에서 나온다. AWS ALB의 기본 idle timeout은 60초다. 서버가 60초마다 Ping을 보내면 ALB가 연결을 끊는 시점과 Ping이 겹쳐 간헐적인 끊김이 생길 수 있다.
위험한 설정:ALB idle timeout = 60sping interval = 60s
더 안전한 설정:ALB idle timeout = 60sping interval = 25sping timeout = 20s여기서 외울 것은 25초라는 숫자가 아니라 원리다. 중간 장비가 idle로 판단하기 전에 애플리케이션 또는 프로토콜 레벨 heartbeat가 먼저 지나가야 한다.
WebSocket 연결은 끊길 수 있다. 모바일 네트워크 전환, 서버 배포, ALB idle timeout, 탭 sleep, NAT timeout 때문에 정상 운영 중에도 끊어진다. 그래서 재연결은 기능의 일부다.
나쁜 재연결 전략은 모든 클라이언트가 즉시 다시 붙는 것이다.
서버 재시작10,000 clients disconnected모든 client가 즉시 reconnect-> handshake 폭증-> 서버가 다시 느려짐-> 재연결 실패-> 다시 즉시 reconnect이 현상을 재연결 폭풍(Reconnection Storm)이라고 한다. 흔한 완화책은 exponential backoff와 jitter다.
delay(attempt) = min(base * 2^attempt + random_jitter, max_delay)
attempt 0: 약 1초attempt 1: 약 2초attempt 2: 약 4초attempt 3: 약 8초attempt 4 이후: 상한에 수렴Jitter는 랜덤 지연이다. 모든 클라이언트가 같은 수식으로 같은 시각에 붙는 것을 막는다. 재연결은 사용자 경험과 서버 보호의 균형이다. 채팅 입력창은 빠르게 회복해야 하지만, 백그라운드 가격 스트림은 몇 초 늦게 붙어도 서버를 보호하는 편이 낫다.
재연결 설계에서 함께 정해야 하는 것은 상태 복원이다.
WebSocket은 연결을 다시 열 수 있게 해주지만, 끊긴 동안의 비즈니스 의미를 자동으로 복구해 주지는 않는다.
WebSocket 인증은 보통 Upgrade 핸드셰이크 단계에서 시작한다. 브라우저는 쿠키를 자동으로 붙일 수 있고, 클라이언트는 쿼리 파라미터나 Sec-WebSocket-Protocol 같은 방식으로 토큰을 전달하기도 한다. 하지만 핸드셰이크가 성공했다고 해서 이후 모든 메시지가 영원히 권한을 갖는 것은 아니다.
HTTP와 WebSocket의 인증 경계는 다르다.
| 항목 | HTTP 요청 | WebSocket 연결 |
|---|---|---|
| 인증 시점 | 요청마다 헤더나 쿠키를 확인하기 쉽다 | 연결 시점에 확인하고 연결 상태에 붙여두기 쉽다 |
| 권한 변경 | 다음 요청부터 새 정책을 적용하기 쉽다 | 연결 중 권한 변경을 별도로 반영해야 한다 |
| 토큰 만료 | 만료된 요청만 실패시키면 된다 | 장시간 연결에서 재인증 또는 연결 종료가 필요하다 |
| 로그 추적 | request id 단위가 자연스럽다 | connection id와 message id를 설계해야 한다 |
실패 예시는 다음과 같다.
1. 사용자가 관리자 권한으로 WebSocket 연결2. 운영자가 관리자 권한 회수3. 기존 WebSocket 연결은 여전히 admin 이벤트를 수신이 문제는 “인증을 했는가”가 아니라 “장시간 연결에서 권한 변경을 어떻게 반영하는가”의 문제다. 해결책은 서비스마다 다르지만, 최소한 다음 중 하나를 선택해야 한다.
HTTP 요청은 무상태(stateless)로 설계하면 어느 인스턴스가 받아도 된다. WebSocket 연결은 다르다. 연결 객체는 특정 서버 프로세스 메모리에 있다.
Client A -> ALB -> Instance 1 # socket object exists hereClient B -> ALB -> Instance 2 # socket object exists here이 상태에서 Instance 1이 order:7 방에 메시지를 emit하면, Instance 2에 연결된 클라이언트는 그 메시지를 모를 수 있다. 두 가지 문제가 섞여 있다.
Socket.IO의 room은 클라이언트가 연결 URL에서 직접 선택하는 프로토콜 기능이 아니라, 서버가 연결을 그룹화하는 API다. 서버가 인증과 권한을 확인한 뒤 socket.join("user:" + userId)처럼 가입시킨다. 클라이언트가 room 가입을 요청하는 event를 보낼 수는 있지만, 어느 room에 넣을지는 서버가 검증하고 결정해야 한다.
여기서 fan-out은 하나의 이벤트를 여러 연결이나 여러 서버 인스턴스로 분배하는 일이다. 주문 상태 변경 이벤트 하나가 웹, 모바일, 관리자 화면의 여러 연결로 퍼지는 것을 떠올리면 된다.
WebSocket 연결은 Upgrade가 끝난 뒤 그 TCP 연결이 닫힐 때까지 이미 한 backend에 붙어 있다. 따라서 표준 WebSocket의 established connection을 유지하려고 sticky session이 반드시 필요한 것은 아니다.
Sticky Session은 Socket.IO가 HTTP long-polling 요청 여러 개를 거쳐 WebSocket으로 upgrade할 때처럼 연결 수립이 여러 HTTP 요청에 걸치거나, 재연결 후에도 서버 로컬 세션을 찾아야 할 때 첫 번째 문제를 완화한다. 로드밸런서가 쿠키나 소스 IP 기준으로 같은 클라이언트를 같은 인스턴스에 붙이는 방식이다. WebSocket-only transport와 외부 세션 저장소를 사용하면 이 affinity가 필요하지 않을 수 있다. 어느 경우든 sticky session만으로 두 번째 문제는 해결되지 않는다.
Sticky Session:Client A -> Instance 1 고정Client B -> Instance 2 고정
문제:Instance 1에서 발생한 broadcast를 Instance 2의 Client B가 어떻게 받을까?상태 fan-out에는 보통 Redis Pub/Sub 같은 backplane을 둔다.
Instance 1 -- publish order:7 changed --> RedisInstance 2 -- subscribe order:7 --------> RedisInstance 3 -- subscribe order:7 --------> RedisSocket.IO의 Redis adapter는 이 패턴을 라이브러리로 감싼 것이다. 핵심은 “sticky session이 scale-out의 끝”이 아니라는 점이다. sticky session은 연결 라우팅 문제를 줄이고, Redis adapter 같은 backplane은 인스턴스 간 메시지 공유 문제를 줄인다.
선택 기준은 다음과 같다.
| 상황 | 우선 볼 선택지 | 이유 |
|---|---|---|
| 단일 인스턴스 개발 환경 | 별도 sticky/backplane 불필요 | 연결이 한 프로세스에만 있다 |
| 2개 이상, 표준 WebSocket/WS-only | 연결 registry + backplane 검토 | 열린 연결은 이미 고정되며 사용자 위치 공유가 핵심이다 |
| Socket.IO long-polling 사용 | sticky session + backplane | handshake·polling 요청 affinity가 필요할 수 있다 |
| 여러 인스턴스에 broadcast 필요 | Redis adapter/backplane | 한 인스턴스의 emit을 전체로 전파해야 한다 |
| 인스턴스 장애가 잦다 | 재연결 + 상태 복원 설계 | sticky만 있으면 해당 인스턴스 장애에 취약 |
실패 신호는 명확하다.
REST는 HTTP 리소스와 JSON 표현을 중심으로 설계한다. 외부 공개 API에는 이 점이 장점이다. 사람이 curl로 읽을 수 있고, 브라우저와 프록시 생태계가 잘 맞는다.
서버 내부 통신에서는 다른 문제가 커진다.
gRPC는 이 문제를 RPC 모델로 푼다. RPC는 원격 프로시저 호출이다. 네트워크 너머의 함수를 로컬 함수처럼 호출한다는 추상화다.
service InventoryService { rpc Reserve (ReserveRequest) returns (ReserveReply);}이 정의는 “HTTP POST 경로가 무엇인가”보다 먼저 “어떤 서비스에 어떤 메서드가 있고, 입력과 출력 타입이 무엇인가”를 말한다. 실제 전송은 HTTP/2 위에서 이뤄지지만, 개발자가 보는 표면은 메서드 호출에 가깝다.
Protobuf는 스키마 정의 언어이자 이진 직렬화 포맷이다. 직렬화는 메모리의 값을 네트워크나 파일로 보낼 수 있는 바이트 형태로 바꾸는 일이다.
syntax = "proto3";package user;
message User { string name = 1; int32 age = 2;}여기서 1, 2는 기본값이 아니다. 필드 번호(field number)다. Protobuf 와이어 포맷은 name, age라는 문자열을 매번 보내지 않고, 필드 번호와 타입 정보를 조합해 값을 보낸다.
JSON:{ "name": "Alice", "age": 30 }대략 28 bytes 이상. 필드명 문자열도 전송된다.
Protobuf:field 1 -> "Alice"field 2 -> 30대략 9 bytes 수준의 compact binary로 표현 가능하다.정확한 크기는 값, 타입, 라이브러리, framing에 따라 달라진다. 중요한 감각은 “필드명이 매번 전송되지 않는다”와 “스키마가 있어야 해석할 수 있다”이다.
필드 번호는 호환성 계약이므로 함부로 재사용하면 안 된다.
// v1message User { string id = 1; int32 age = 2;}
// 나쁜 v2: field 2를 다른 의미로 재사용message User { string id = 1; string grade = 2;}v1 클라이언트는 field 2를 age로 알고 있고, v2 서버는 field 2를 grade로 보낼 수 있다. 같은 번호에 다른 의미가 붙으면 양쪽이 서로 다른 세계를 본다. 삭제한 필드 번호는 reserved로 막아두는 것이 안전하다.
message User { reserved 2; reserved "age";
string id = 1; string grade = 3;}이 한 가지 규칙만 기억해도 Protobuf 스키마 변경 사고의 큰 부분을 피할 수 있다. 필드명은 사람이 읽는 이름이고, 필드 번호는 네트워크 계약이다.
gRPC는 HTTP/2를 사용한다. HTTP/2는 하나의 TCP 연결 위에 여러 stream을 동시에 싣는다. 그래서 gRPC 클라이언트는 보통 channel을 재사용하고, 그 channel 안에서 여러 RPC stream을 만든다.
TCP connection└── HTTP/2 connection ├── stream 1: InventoryService.Reserve ├── stream 3: UserService.GetUser └── stream 5: PaymentService.AuthorizeHTTP/1.1에서는 연결 하나에서 요청/응답 순서가 성능 병목이 되기 쉽다. HTTP/2 stream multiplexing은 여러 요청이 같은 연결을 공유하면서도 독립된 stream id를 갖게 한다. 다만 TCP 한 연결 위에 올라가므로 패킷 손실 같은 하위 계층 문제의 영향은 공유할 수 있다.
gRPC를 “Protobuf라서 빠르다”로만 이해하면 절반만 본 것이다. 성능과 사용성은 다음 네 가지가 같이 만든다.
gRPC는 요청과 응답이 각각 한 번인지, 여러 번인지에 따라 네 가지 패턴을 제공한다.
요청 1개에 응답 1개를 돌려준다. 일반 함수 호출과 가장 비슷하다.
단건 조회, 재고 예약, 권한 검증클라이언트 요청 1개에 서버가 여러 응답을 순차적으로 보낸다.
로그 tail, 검색 결과 스트림, 진행률클라이언트가 여러 메시지를 보낸 뒤 서버가 최종 응답 1개를 반환한다.
파일 청크 업로드, 배치 집계, 센서 샘플 업로드클라이언트와 서버가 각자 여러 메시지를 독립적으로 주고받는다.
실시간 음성, 협상 프로토콜, 양방향 제어작은 .proto로 네 패턴을 함께 보면 경계가 선명해진다.
service OrderService { rpc GetOrder (GetOrderRequest) returns (Order); // Unary rpc WatchOrder (WatchOrderRequest) returns (stream OrderEvent); // Server streaming rpc UploadEvents (stream OrderEvent) returns (UploadSummary); // Client streaming rpc SyncOrders (stream ClientEvent) returns (stream ServerEvent); // Bidirectional}첫 회독은 위 비교표와 .proto 예시에서 네 패턴의 방향을 구분하면 끝난다.
아래 패턴별 흐름과 client streaming의 half-close는 실제 streaming RPC를
설계하거나 디버깅할 때 돌아오는 선택 심화다.
Unary는 HTTP 요청-응답과 가장 비슷하다.
client -> GetOrder({ id: "o-7" })server -> Order({ id: "o-7", status: "PAID" })단건 조회나 검증처럼 입력 하나와 결과 하나가 자연스러울 때 쓴다. REST로도 충분한 상황이 많지만, 내부 서비스 간 타입 생성과 deadline 정책을 통일하고 싶다면 gRPC Unary가 맞을 수 있다.
Server streaming은 클라이언트가 한 번 구독하고 서버가 여러 메시지를 보낸다.
client -> WatchOrder({ id: "o-7" })server -> OrderEvent({ status: "PAID" })server -> OrderEvent({ status: "PACKING" })server -> OrderEvent({ status: "SHIPPED" })server -> completeSSE와 비슷하게 보일 수 있지만 주 사용처가 다르다. SSE는 브라우저 친화적인 서버->클라이언트 이벤트 스트림이고, gRPC server streaming은 서버 간 typed stream에 강하다.
Client streaming은 클라이언트가 여러 조각을 보내고 마지막에 서버 응답 하나를 받는다.
client -> OrderEvent(...)client -> OrderEvent(...)client -> OrderEvent(...)client -> half-closeserver -> UploadSummary({ count: 3 })여기서 half-close는 “나는 더 보낼 메시지가 없다”는 신호다. 이 신호가 없으면 서버는 최종 집계를 언제 계산해야 할지 모른다. 예를 들어 서버 구현이 stream completion을 기다려 집계를 반환하는 구조라면, 클라이언트가 stream을 닫지 않는 순간 호출은 deadline까지 대기하다 실패할 수 있다.
Bidirectional streaming은 양쪽이 독립적으로 여러 메시지를 보낸다.
client -> ClientEvent(seq=1)server -> ServerEvent(ack=1)server -> ServerEvent(config_version=4)client -> ClientEvent(seq=2)WebSocket과 비슷해 보이지만 중요한 차이가 있다. gRPC bidi stream은 Protobuf schema, HTTP/2 stream, deadline, status code, service/method 계약 안에서 움직인다. WebSocket은 더 범용적인 양방향 메시지 채널이고, 메시지 계약과 상태 코드는 애플리케이션이 더 많이 설계한다.
gRPC에서는 모든 호출에 deadline을 붙인다고 생각하는 편이 안전하다. Deadline은 “이 시각 이후에는 이 RPC 결과가 더 이상 필요 없다”는 호출 계약이다. Timeout은 흔히 현재 시점부터 얼마 동안 기다릴지를 말한다. 많은 라이브러리에서 timeout을 설정하면 deadline으로 변환된다.
OrderService -> InventoryService.Reserveclient deadline = now + 300ms
100ms: request sent250ms: server still waiting for DB300ms: client cancelsserver should stop unnecessary work if cancellation is observeddeadline이 없으면 느린 하위 서비스 하나가 호출자를 계속 붙잡아 thread, connection, memory를 소모한다. deadline이 너무 짧으면 정상 요청도 실패한다. 그래서 timeout은 “성능 튜닝 숫자”가 아니라 장애 전파를 제한하는 계약이다.
gRPC status code는 HTTP 상태 코드와 다르다. 서버가 HTTP/2 위에서 통신하더라도 RPC 결과는 gRPC status로 표현된다.
| gRPC code | 의미 | 재시도 감각 |
|---|---|---|
OK | 성공 | 불필요 |
INVALID_ARGUMENT | 요청 값이 계약과 맞지 않음 | 같은 요청 재시도 불필요 |
NOT_FOUND | 대상 리소스 없음 | 보통 재시도 불필요 |
UNAUTHENTICATED | 인증 실패 | 토큰 갱신 후 가능 |
PERMISSION_DENIED | 권한 없음 | 권한 변경 전까지 불필요 |
DEADLINE_EXCEEDED | deadline 안에 끝나지 않음 | 멱등 호출이면 제한적으로 가능 |
UNAVAILABLE | 서버/네트워크/로드밸런서 일시 실패 | backoff + jitter로 재시도 가능 |
RESOURCE_EXHAUSTED | quota, rate limit, 리소스 부족 | Retry-After 성격의 정책 확인 후 재시도 |
INTERNAL | 서버 내부 오류 | 원인 확인 전 무작정 재시도하면 증폭 위험 |
재시도는 “실패했으니 다시”가 아니다. 멱등성과 상태 변경 여부를 먼저 봐야 한다.
안전한 후보:GetUser, ListOrders, ReadMetrics
위험한 후보:CreatePayment, ReserveInventory, SendEmail상태를 바꾸는 RPC에 재시도를 적용하려면 idempotency key나 request id로 중복 처리를 막아야 한다. 그렇지 않으면 DEADLINE_EXCEEDED 후 재시도한 결제가 실제로는 두 번 처리될 수 있다.
재시도가 오히려 장애를 키울 때는 빠르게 실패시키는 경계도 필요하다.
예를 들어 하위 재고 서비스의 UNAVAILABLE이 계속 증가하고 큐가 쌓이면,
모든 주문 요청을 계속 재시도하기보다 잠시 실패를 빠르게 반환하고 fallback
또는 대기열로 넘기는 편이 전체 시스템을 보호한다.
Backoff와 jitter는 WebSocket 재연결과 같은 이유로 필요하다. 하위 서비스가 잠깐 죽었을 때 모든 클라이언트가 즉시 재시도하면 장애가 더 커진다.
gRPC는 서버 간 통신에는 강하지만, 브라우저에서 직접 쓰기에는 경계가 있다. 브라우저 JavaScript는 일반적으로 HTTP/2 raw frame을 직접 제어하지 못한다. 그래서 브라우저에서 gRPC를 쓰려면 gRPC-Web과 Envoy 같은 프록시가 필요하다.
| 경계 | 의미 |
|---|---|
| 브라우저 직접 호출 | 일반 gRPC가 아니라 gRPC-Web 프록시가 필요할 수 있다 |
| Client streaming | gRPC-Web에서는 제약이 있다 |
| 디버깅 | curl만으로 payload를 읽기 어렵고 proto가 필요하다 |
| 외부 파트너 연동 | JSON REST가 훨씬 보편적이다 |
| 프록시 timeout/body limit | HTTP/2와 gRPC trailer를 이해하는 프록시 설정이 필요하다 |
따라서 “gRPC가 빠르니까 모든 API를 gRPC로 바꾸자”는 결론은 위험하다. 브라우저와 외부 파트너가 직접 쓰는 API는 REST가 여전히 좋은 기본값이다. gRPC는 내부 서비스 간 고빈도 호출, streaming, 강한 타입 계약이 필요한 곳에서 빛난다.
gRPC 실패를 HTTP 실패처럼만 보면 원인을 놓칠 수 있다.
| 관찰 | 의미 | 먼저 확인할 개념 |
|---|---|---|
모든 에러가 INTERNAL로 보인다 | 서버가 구체적 gRPC status를 매핑하지 않음 | RpcException, status code mapping |
DEADLINE_EXCEEDED가 늘어난다 | 하위 작업이 timeout budget 안에 안 끝남 | deadline, DB/API latency, cancellation |
UNAVAILABLE이 배포 직후 증가한다 | 서버/프록시/channel 연결이 흔들림 | readiness, connection backoff, LB config |
| proto 변경 후 일부 클라이언트만 깨진다 | 생성 코드나 field number 계약이 불일치 | proto package version, reserved fields |
| streaming RPC가 끝나지 않는다 | half-close 또는 completion 신호 누락 | client stream close, deadline |
| 브라우저에서만 호출이 어렵다 | 일반 gRPC와 브라우저 런타임 경계 | gRPC-Web, Envoy, REST 대안 |
| retry 후 중복 처리가 발생한다 | 멱등성 없이 상태 변경 RPC를 재시도 | idempotency key, retry policy |
이 절은 첫 회독 완료 조건이 아니다. 지속 연결인 WebSocket과 외부 시스템이 보내는 단발성 HTTP push를 대비하고 싶을 때 읽는다.
Webhook은 WebSocket이나 gRPC의 경쟁자가 아니라, 외부 시스템이 내 HTTP 엔드포인트를 호출하는 push 방식이다.
Polling:우리 서버 -> 결제사: 결제 끝났나요?우리 서버 -> 결제사: 결제 끝났나요?
Webhook:결제사 -> 우리 서버: 결제 완료 이벤트입니다.Webhook은 일반 HTTP 요청이므로 연결을 오래 유지하지 않는다. 외부 시스템이 이벤트를 보낼 때마다 POST /webhooks/payment 같은 엔드포인트를 호출한다. 그래서 WebSocket의 heartbeat나 sticky session 문제와는 다르다.
Webhook에서 중요한 학습 포인트는 신뢰 경계다. 신뢰 경계는 통제할 수 없는 외부 입력이 내부 처리로 넘어오는 지점이며, 이 경계를 넘을 때 발신자와 메시지 무결성을 검증해야 한다. Webhook은 외부에서 온 HTTP 요청이므로 “정말 결제사가 보냈고 전송 중 내용이 바뀌지 않았는가”를 확인해야 한다.
HMAC(Hash-based Message Authentication Code)은 송신자와 수신자가 공유한 비밀키와 원본 메시지로 인증 코드를 계산해, 발신자와 메시지 무결성을 확인하는 방식이다.
sender:signature = HMAC_SHA256(secret, raw_request_body)
receiver:expected = HMAC_SHA256(secret, raw_request_body)signature == expected 인지 비교여기서 raw request body가 중요하다. JSON을 파싱한 뒤 다시 JSON.stringify()하면 공백과 키 순서가 바뀔 수 있다. 서명은 원본 바이트에 대해 계산되므로, 프레임워크에서 raw body를 보존해야 한다.
이 문서에서는 Webhook을 push 통신의 대비용으로만 둔다. 핵심은 WebSocket의 지속 양방향 연결과 gRPC의 typed RPC다.
선택 기준은 “실시간이면 WebSocket”, “빠르면 gRPC”처럼 단순하지 않다. 통신 방향, 호출 주체, 브라우저 경계, 상태 보유 비용, 계약 강도를 함께 본다.
| 질문 | REST | SSE | WebSocket | gRPC |
|---|---|---|---|---|
| 브라우저 직접 사용 | 매우 좋음 | 좋음 | 좋음 | 일반 gRPC는 어려움, gRPC-Web 필요 |
| 외부 공개 API | 좋음 | 제한적 | 제한적 | 파트너 지원 낮음 |
| 서버가 먼저 알림 | polling 필요 | 단방향 가능 | 양방향 가능 | server streaming은 서버 간에 적합 |
| 클라이언트도 자주 말함 | 요청 반복 | 부적합 | 적합 | bidi streaming은 서버 간에 적합 |
| 타입 계약 | OpenAPI 등 별도 관리 | 이벤트 포맷 별도 관리 | 메시지 포맷 별도 관리 | Protobuf가 기본 계약 |
| 디버깅 | curl/Postman 친화적 | 브라우저 Network 친화적 | 프레임/이벤트 도구 필요 | proto/grpcurl 같은 도구 필요 |
| 운영 부담 | 낮음 | 중간 | heartbeat/scale-out 부담 높음 | proto 배포/proxy/deadline 설계 필요 |
| 대표 사용처 | CRUD, 공개 API | 진행률, 로그, 알림 | 채팅, 협업, 위치, 게임 | 내부 서비스, 스트리밍, 고빈도 RPC |
다음 조건이 함께 있으면 WebSocket을 검토한다.
반대로 이벤트가 드물고 사용자가 몇 분 늦게 알아도 되는 백오피스 화면이라면 polling이 충분할 수 있다. 예를 들어 하루에 몇 번 바뀌는 정산 상태를 5분마다 조회하는 것은 단순하고 안정적이다. WebSocket을 붙이면 heartbeat, 재연결, fan-out, 권한 갱신까지 책임져야 한다.
SSE는 서버에서 클라이언트로만 이벤트를 보낸다. 브라우저에는 EventSource가 있고 자동 재연결도 지원한다.
서버 -> 브라우저: build progress 10%서버 -> 브라우저: build progress 40%서버 -> 브라우저: build progress 100%클라이언트가 같은 연결로 서버에게 자주 메시지를 보낼 필요가 없다면 SSE가 단순하다. 진행률 표시, 로그 tail, 알림 feed는 SSE 후보가 된다. 양방향 채팅, 협업 편집, 실시간 입력 전송은 WebSocket 후보가 된다.
gRPC는 다음 조건에서 강하다.
정량 감각도 함께 봐야 한다. Protobuf payload는 JSON보다 작을 수 있고, HTTP/2 multiplexing은 동시 호출에서 유리하다. Uber가 M3 메트릭 인제스트를 gRPC로 전환하며 p99 쓰기 레이턴시를 34.1ms에서 13.6ms로 줄였다는 사례처럼, 고빈도·고차원·대용량 요청에서는 이득이 커질 수 있다.
하지만 작은 단건 조회에서는 JSON/REST의 단순성이 더 큰 가치일 수 있다. 수치 사례는 “gRPC가 항상 빠르다”는 결론이 아니라, 어떤 비용이 병목인지 찾는 힌트로 읽어야 한다.
Uber 사례를 조금 더 쪼개면 이 경계가 보인다. M3 메트릭 인제스트는 p99 쓰기 레이턴시가 34.1ms에서 13.6ms로 줄었지만, Eats 추천 검색의 p99는 205ms에서 176ms로 줄어 개선폭이 훨씬 작았다. 고차원 벡터 검색 payload는 약 88.7% 줄었지만, tail latency는 GC, DB, 네트워크 같은 다른 병목을 함께 받는다. 따라서 후보를 고를 때는 “요청 빈도 × payload 크기 × p95/p99 병목 × 계약 안정성”을 함께 본다.
| 관찰값 | gRPC 후보 신호 | 해석 |
|---|---|---|
| payload가 크고 필드명이 반복된다 | Protobuf로 줄일 여지가 있다 | JSON 파싱과 네트워크 비용이 함께 줄 수 있다 |
| p95보다 p99가 크게 튄다 | deadline·streaming·connection reuse를 함께 본다 | DB tail latency가 원인이면 gRPC만으로 해결되지 않는다 |
| 호출 수가 많고 schema drift가 잦다 | generated client와 proto review 가치가 커진다 | 성능보다 계약 안정성이 주된 이득일 수 있다 |
| 워크로드 | gRPC 기대 이득 | REST가 나을 수 있는 이유 |
|---|---|---|
| 내부 메트릭 인제스트 | payload 크기, streaming, p99 latency | 운영팀이 proto/tooling을 감당해야 한다 |
| 고차원 벡터 검색 | 큰 payload에서 binary encoding 이득 | 외부 공개 API라면 JSON 생태계가 중요하다 |
| 사용자 프로필 단건 조회 | 타입 생성 정도의 이득 | latency 대부분이 DB라면 체감 이득이 작다 |
| 파트너사 결제 API | 이론상 타입 계약 가능 | 파트너가 REST/JSON만 지원하면 REST가 맞다 |
| 선택 실수 | 나타나는 증상 | 더 나은 질문 |
|---|---|---|
| 단방향 알림에 WebSocket 도입 | 연결 운영 부담이 큰데 클라이언트 송신은 거의 없음 | SSE로 충분한가? |
| 브라우저 공개 API에 일반 gRPC 도입 | gRPC-Web 프록시와 tooling 문제가 먼저 터짐 | REST API가 더 자연스러운가? |
| 고빈도 내부 JSON 호출을 계속 REST로 유지 | CPU가 JSON 파싱에 쓰이고 schema drift가 잦음 | gRPC 후보를 p95/p99와 payload 크기로 정렬했나? |
| WebSocket scale-out에 sticky만 적용 | 일부 인스턴스의 클라이언트만 broadcast 누락 | backplane/fan-out이 필요한가? |
| 상태 변경 RPC에 무제한 retry | 결제/예약/이메일이 중복 처리됨 | 멱등성 키와 retry 가능 code를 분리했나? |
퀴즈
힌트: 먼저 통신 방향을 본다.
아니다. 단방향 서버 push면 SSE가 더 단순할 수 있다. WebSocket은 heartbeat, 재연결, sticky session, fan-out 같은 운영 부담을 함께 가져온다.
주문 상태 화면을 예로 전체 흐름을 묶어보자.
1. 결제사 -> 우리 서버: Webhook으로 결제 완료 통보2. Order API -> Payment Event 저장3. Order Service -> Inventory Service: gRPC로 재고 차감 요청4. Order Service -> WebSocket gateway: 주문 상태 변경 이벤트 발행5. WebSocket gateway -> 고객 브라우저: order.status.changed push각 기술은 같은 “push”라는 단어 아래에 있지만 역할이 다르다.
| 단계 | 기술 | 이유 |
|---|---|---|
| 결제사 이벤트 수신 | Webhook | 외부 시스템이 이벤트 발생 시 우리 HTTP endpoint 호출 |
| 재고 차감 | gRPC | 내부 서비스 간 typed RPC와 deadline/retry 정책 필요 |
| 고객 화면 갱신 | WebSocket | 서버가 브라우저에 상태 변경을 즉시 알려야 함 |
| 주문 상세 재조회 | REST | 화면 진입 시 현재 상태를 한 번 조회하면 충분 |
이 예시에서 중요한 경계는 상태 복원이다. 사용자가 WebSocket 메시지를 놓쳤더라도 REST GET /orders/{id}로 현재 상태를 다시 가져올 수 있어야 한다. WebSocket 메시지는 빠른 알림이고, 영구 상태의 원본은 DB와 API다.
사례 A: 결제는 됐는데 화면이 갱신되지 않는다
Payment provider -> Webhook 200 OKOrder DB status = PAIDBrowser WebSocket connectedUI still shows PENDING이때 결제사 Webhook만 보면 부족하다. DB 상태는 바뀌었으므로 Webhook 수신은 성공했다. 다음으로 Order Service가 WebSocket gateway에 이벤트를 발행했는지, gateway가 올바른 room/user connection을 찾았는지, 해당 사용자가 다른 인스턴스에 연결되어 있는데 fan-out이 빠졌는지를 본다.
사례 B: 배포 후 모든 클라이언트가 동시에 재연결한다
deployment restartactive websocket connections dropreconnect attempts spikeCPU and handshake latency spike이건 비즈니스 메시지 오류가 아니라 재연결 전략 문제다. backoff와 jitter가 없거나, 서버 readiness 전에 트래픽이 몰렸을 수 있다. 재연결 폭풍을 줄이고, 클라이언트가 최신 상태를 REST로 재조회하게 만들면 사용자 경험과 서버 보호를 함께 잡을 수 있다.
사례 C: 재고 차감 gRPC가 가끔 중복된다
Order Service -> ReserveInventoryclient deadline exceededclient retriesInventory Service processed both requestsDEADLINE_EXCEEDED는 “서버가 처리하지 않았다”가 아니다. 클라이언트가 기다리기를 포기했다는 뜻일 수 있다. 하위 서비스가 실제로 처리했을 가능성이 있으므로 상태 변경 RPC에는 idempotency key가 필요하다.
긴 NestJS 프로젝트 생성 절차보다 먼저, 각 개념이 코드에서 어디에 드러나는지만 확인한다. 전체 실행 환경은 프레임워크 문서를 보고 맞추면 된다.
IoAdapter(Socket.IO) gateway에서 보이는 핵심 경계아래 코드는 Nest의 기본 IoAdapter를 통해 Socket.IO를 사용하는 예제다. Socket, named event, room, namespace, pingInterval·pingTimeout은 Socket.IO API와 옵션이다. Socket.IO는 WebSocket을 전송 방식으로 사용할 수 있지만 표준 WebSocket과 같은 프로토콜은 아니다. Nest WsAdapter도 { event, data } 메시지를 파싱해 @SubscribeMessage()로 라우팅하지만 room과 namespace는 제공하지 않는다. 따라서 이 예제를 표준 WebSocket이나 WsAdapter API 예제로 읽으면 안 된다.
@WebSocketGateway({ namespace: "/orders", pingInterval: 25000, pingTimeout: 20000,})export class OrderGateway { @WebSocketServer() server: Server;
handleConnection(client: Socket) { const userId = authenticateHandshake(client.handshake); client.join("user:" + userId); }
pushOrderStatus(userId: string, event: OrderStatusChanged) { this.server.to("user:" + userId).emit("order.status.changed", event); }}이 예제에서 봐야 할 것은 데코레이터 이름보다 Socket.IO adapter가 추가한 경계다.
handleConnection: Socket.IO handshake를 마친 연결을 사용자와 연결한다.pingInterval, pingTimeout: Socket.IO heartbeat로 idle 연결과 dead connection을 구분한다.namespace: Socket.IO 연결 경로를 논리적으로 분리한다.room: 서버가 인증된 연결을 그룹화하는 Socket.IO API다. 클라이언트가 room 이름을 직접 선택해 가입하는 구조가 아니다.syntax = "proto3";package inventory;
service InventoryService { rpc Reserve (ReserveRequest) returns (ReserveReply); rpc WatchReservation (WatchRequest) returns (stream ReservationEvent);}
message ReserveRequest { string request_id = 1; string order_id = 2; string sku = 3; int32 quantity = 4;}
message ReserveReply { bool accepted = 1; string reason = 2;}
message WatchRequest { string order_id = 1;}
message ReservationEvent { string order_id = 1; string status = 2;}학습 포인트는 네 가지다.
request_id는 retry 중복 처리를 위한 멱등성 키가 될 수 있다.WatchReservation의 stream은 server streaming이다.accepted=false 같은 비즈니스 거절과 UNAVAILABLE 같은 RPC 실패는 다른 층이다.if (!isValidSku(input.sku)) { throw new RpcException({ code: status.INVALID_ARGUMENT, message: "sku is invalid", });}
if (!inventory) { throw new RpcException({ code: status.NOT_FOUND, message: "inventory not found", });}일반 Error를 던지면 클라이언트가 대부분 INTERNAL만 볼 수 있다. 그러면 요청이 잘못됐는지, 리소스가 없는지, 서버가 죽었는지를 구분하기 어렵다. gRPC에서는 HTTP status가 아니라 RPC status를 의식적으로 매핑해야 한다.
socket.io-client와 grpcurl은 유용하지만, 이 문서의 본문 목적은 설치 절차가 아니다. 도구로 확인할 때는 다음만 보면 된다.
Socket.IO:- `/orders` namespace에 Socket.IO protocol로 연결했는가?- WebSocket transport를 강제했다면 handshake가 101로 끝났는가?- `order.status.changed` named event를 Socket.IO client가 수신하는가?
gRPC:- proto를 기준으로 호출했는가?- 응답 payload와 status code를 함께 봤는가?- deadline을 걸었을 때 어떤 code가 오는가?운영 절차를 외우기보다 증상, 의미, 먼저 볼 개념을 연결한다.
| 증상 | 의미 | 먼저 볼 개념 |
|---|---|---|
101 Switching Protocols가 없다 | Upgrade 전 HTTP 단계에서 실패 | proxy header, auth, route |
| 60초 안팎으로 간헐적 disconnect | idle timeout과 heartbeat 간격 충돌 가능 | ALB/Nginx timeout, ping interval |
| 단일 서버는 정상, 다중 서버에서 일부 누락 | 인스턴스 간 fan-out 부재 가능 | Redis adapter, room, sticky session |
| 연결 수는 정상인데 화면이 늦다 | backpressure 또는 stale event 처리 문제 | buffer, drop/coalesce policy |
| 배포 직후 재연결 시도가 폭증 | backoff/jitter 또는 readiness 문제 | reconnect strategy, server capacity |
| 권한 회수 후에도 이벤트를 받는다 | 장시간 연결의 auth/session 갱신 누락 | session TTL, auth version, disconnect |
| 증상 | 의미 | 먼저 볼 개념 |
|---|---|---|
UNAVAILABLE이 특정 배포 후 증가 | 연결 대상, readiness, 프록시 문제가 흔함 | service discovery, LB, backoff |
DEADLINE_EXCEEDED가 tail에서 증가 | timeout budget 초과 | deadline, DB/API latency |
RESOURCE_EXHAUSTED가 burst 때 증가 | quota, rate limit, connection pool 부족 | concurrency limit, retry policy |
INTERNAL만 보인다 | status mapping이 뭉개졌을 수 있음 | RpcException, server interceptor |
| proto 변경 뒤 일부 클라이언트만 실패 | 생성 코드 버전이나 field number 불일치 | proto package version, reserved |
| streaming 결과가 끝나지 않는다 | stream completion/half-close 누락 | client close, server complete |
| 증상 | 의미 | 먼저 볼 개념 |
|---|---|---|
| 서명 검증이 항상 실패 | raw body가 아니라 파싱 후 body로 검증 | raw request body, HMAC |
| 같은 이벤트가 여러 번 처리됨 | 외부 시스템 retry와 멱등성 누락 | event id, idempotency |
| Webhook은 성공인데 화면 미갱신 | push 수신 이후 내부 이벤트 전파가 끊김 | DB update, WebSocket fan-out |
| 헷갈리는 쌍 | 구분 |
|---|---|
| WebSocket vs Socket.IO | WebSocket은 표준 프로토콜이고, Socket.IO는 재연결·room·fallback 등을 얹은 라이브러리다 |
| WebSocket frame vs app message | frame은 프로토콜 전송 단위이고, app message는 서비스가 의미를 붙인 payload다 |
| heartbeat vs business event | heartbeat는 연결 생존 확인이고, business event는 사용자 상태 변경이다 |
| sticky session vs Redis adapter | sticky는 연결 라우팅, Redis adapter는 인스턴스 간 메시지 fan-out이다 |
| deadline vs timeout | deadline은 끝나야 하는 시각, timeout은 대기 기간으로 표현되는 경우가 많다 |
| gRPC status vs HTTP status | RPC 결과는 gRPC status로 읽고, HTTP/2는 전송 계층에 가깝다 |
| Protobuf field name vs field number | 이름은 사람이 읽는 계약, 번호는 wire format의 핵심 계약이다 |
| REST streaming vs gRPC streaming | REST도 chunk/SSE가 가능하지만 gRPC는 typed RPC stream을 기본 모델로 제공한다 |
| Webhook vs WebSocket | Webhook은 이벤트마다 들어오는 HTTP 요청, WebSocket은 지속 양방향 연결이다 |
이 부록은 본문 이해에 필수는 아니다. 개념을 손으로 확인하고 싶을 때만 사용한다.
import { io } from "socket.io-client";
const socket = io("http://localhost:3000/orders", { transports: ["websocket"],});
socket.on("connect", () => { console.log("connected", socket.id);});
socket.on("order.status.changed", (event) => { console.log(event);});
socket.on("connect_error", (error) => { console.error(error.message);});확인할 것:
io("http://localhost:3000/orders")의 /orders는 raw WebSocket 경로가 아니라 Socket.IO namespace다.transports: ["websocket"]을 지정하면 polling 없이 WebSocket transport로 연결하며, 실제 HTTP Upgrade 요청은 기본 /socket.io/ path로 간다.pushOrderStatus()가 order.status.changed를 emit하면 해당 사용자 room에 가입된 클라이언트의 listener가 event를 받는다.authenticateHandshake() 계약에 맞는 cookie나 auth 값을 client 옵션에 추가한다.이 예제에서 클라이언트는 room 이름을 보내지 않는다. 서버의 handleConnection()이 인증 결과로 room을 정하고 client.join()을 호출한다. 브라우저 개발자 도구 Network 탭에서는 /socket.io/?EIO=...&transport=websocket 요청의 101과 이후 frame을 확인할 수 있다.
grpcurl -plaintext \ -proto inventory.proto \ -d '{"requestId":"r-1","orderId":"o-7","sku":"keyboard","quantity":1}' \ localhost:5000 \ inventory.InventoryService/Reserve확인할 것:
request_id 같은 멱등성 키가 필요하다.client deadline: 300msserver DB call: p95 250ms, p99 600ms이 설정에서는 p99 요청 일부가 정상적으로 DEADLINE_EXCEEDED가 된다. 해결은 무조건 deadline을 늘리는 것이 아니다. 호출자의 전체 latency budget, DB tail latency, fallback 가능성, retry가 중복 처리를 만들지 않는지를 함께 봐야 한다.
첫 회독은 아래 완료 체크만 사용한다. 이어지는 두 체크리스트는 설계·운영 시 다시 보는 선택 심화이며, 모두 답하지 못해도 이 토픽의 첫 회독을 마칠 수 있다.
WebSocket, HTTP Upgrade, 101 Switching Protocols, Persistent Connection, Full-Duplex, Frame, Message Boundary, Backpressure, Ping, Pong, Heartbeat, Reconnect, Exponential Backoff, Jitter, Sticky Session, Redis Adapter, Backplane, Fan-out, gRPC, RPC, Protocol Buffers, Protobuf, Field Number, reserved, HTTP/2, Stream Multiplexing, Unary, Server Streaming, Client Streaming, Bidirectional Streaming, Deadline, Timeout, Retry, gRPC Status Code, gRPC-Web, Webhook, HMAC, rawBody, SSE, Polling
@WebSocketGateway, namespace, lifecycle hook, adapter 설정..proto, @GrpcMethod, streaming method 연결 방식.| 기술 | 핵심 원리 | 가장 자연스러운 곳 | 실패 신호 |
|---|---|---|---|
| REST | HTTP 요청-응답, 리소스, 상태 코드 | 공개 API, 단순 조회/수정, 브라우저/파트너 | polling 폭증, schema drift |
| SSE | HTTP 지속 연결의 단방향 서버 이벤트 | 진행률, 로그, 알림 | 양방향 요구가 생기면 한계 |
| WebSocket | Upgrade 이후 지속 양방향 frame 통신 | 채팅, 협업, 위치, 실시간 상태 | idle disconnect, backpressure, fan-out 누락 |
| gRPC | Protobuf schema + HTTP/2 RPC | 내부 서비스 간 고빈도 호출과 streaming | deadline 초과, proto 불일치, status mapping 누락 |
| Webhook | 외부 시스템이 내 HTTP endpoint 호출 | 결제/배송/CI 이벤트 수신 | raw body 서명 실패, 중복 이벤트 |
WebSocket은 요청 수를 줄이는 대신 열린 연결의 생명주기를 책임지게 한다. 그래서 heartbeat, backpressure, 재연결, 인증 갱신, load balancing을 함께 설계해야 한다.
gRPC는 JSON REST보다 빠른 도구 하나가 아니라, Protobuf schema와 HTTP/2 stream을 기반으로 한 RPC 계약이다. field number, streaming 종류, deadline, status code, retry 가능성을 함께 이해해야 안전하게 쓸 수 있다.
첫 판단은 단순하다. 공개 API와 브라우저 기본값은 REST, 단방향 서버 push는 SSE, 양방향 실시간은 WebSocket, 내부 typed 고빈도 RPC는 gRPC다. 그다음 실제 선택은 운영 부담, 브라우저/프록시 경계, 상태 복원, 실패 신호까지 포함해 결정한다.