WebSocket과 gRPC의 통신 선택 기준
WebSocket, gRPC, Webhook은 모두 요청과 응답을 다루지만, 연결을 유지하는 방식과 책임 경계가 다르다. 이 스크립트는 실시간 Push, 내부 서비스 통신, 외부 이벤트 수신, 로드밸런서와 장애 대응까지 핵심 판단 기준을 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
WebSocket과 gRPC를 함께 봐야 하는 이유는 실시간성과 내부 통신 성능 요구가 동시에 커졌기 때문이다. 채팅, 알림, 주문 상태 업데이트처럼 서버가 먼저 말을 걸어야 하는 시나리오를 HTTP 폴링으로 만들면 불필요한 요청이 늘어난다. 반대로 마이크로서비스 내부 통신에서는 REST의 JSON 파싱 오버헤드가 병목이 될 수 있고, gRPC는 바이너리 직렬화와 HTTP/2 멀티플렉싱으로 지연을 줄이는 선택지가 된다. Uber가 검색과 M3 시스템을 REST에서 gRPC로 전환한 사례에서는 p99 쓰기 레이턴시가 34.1ms에서 13.6ms로 약 60% 감소했다.
- 02
WebSocket은 HTTP로 시작하지만, 계속 HTTP로 동작하지는 않는다. 클라이언트가 HTTP Upgrade 핸드셰이크로 WebSocket 전환을 요청하고, 서버가 101 응답을 보내면 같은 TCP 연결 위에서 WebSocket 프레임이 오간다. 이때부터 서버도 클라이언트에게 먼저 데이터를 밀어넣을 수 있다. 일반 HTTP가 요청을 보내고 응답을 기다리는 편지에 가깝다면, WebSocket은 한 번 연결된 뒤 양쪽이 언제든 말할 수 있는 전화에 가깝다. 그래서 단일 TCP 연결 하나로 양방향 데이터 스트림이 흐르는 전이중 통신이 핵심이다.
- 03
WebSocket 운영에서 자주 놓치는 부분은 연결이 살아 있는지 확인하는 Heartbeat다. TCP 연결은 실제로 끊겼어도 겉으로는 살아 있어 보일 수 있고, 방화벽이나 NAT 장비는 유휴 연결을 조용히 끊을 수 있다. AWS ALB의 기본 idle timeout은 60초라서, 60초 동안 데이터가 없으면 연결이 종료될 수 있다. RFC 6455의 Ping 프레임과 Pong 프레임은 이 문제를 줄이기 위한 제어 프레임이다. 작은 Ping을 주기적으로 보내 Keepalive 역할을 하고, 응답이 없으면 죽은 연결로 판단해 서버 리소스를 정리할 수 있다.
- 04
WebSocket을 여러 서버 인스턴스로 확장할 때는 상태가 문제로 바뀐다. 소켓 연결 객체는 특정 서버 프로세스 메모리에 저장되기 때문에, 로드밸런서가 같은 클라이언트의 흐름을 다른 인스턴스로 보내면 그 서버에는 연결 정보가 없다. Sticky Session은 ALB가 쿠키로 같은 인스턴스에 계속 라우팅하게 만드는 단기 해결책이다. 하지만 특정 인스턴스에 트래픽이 쏠리고 장애 시 재연결 부담이 생긴다. 그래서 프로덕션에서는 Redis Pub/Sub 백플레인과 Socket.IO의 Redis Adapter로 인스턴스 간 메시지를 공유하는 방식이 더 권장된다.
- 05
gRPC의 핵심은 Protocol Buffers와 HTTP/2다. Protobuf는 스키마를 먼저 정의하고, 필드명 대신 필드 번호를 와이어 포맷의 키처럼 사용해 데이터를 작게 보낸다. 원문 예시에서는 JSON으로 약 23바이트인 name과 age 데이터가 Protobuf로는 약 8바이트까지 줄어든다. 숫자를 텍스트가 아니라 실제 이진수로 표현하기 때문이다. 다만 이 방식은 양쪽이 같은 proto 정의를 알고 있어야 한다. 그래서 gRPC는 계약서인 proto를 먼저 맞추면 빠르지만, 스키마 관리가 흐트러지면 서비스 간 타입 불일치가 바로 장애로 이어진다.
- 06
gRPC는 HTTP/2 위에서 네 가지 통신 패턴을 제공한다. Unary는 하나의 요청에 하나의 응답을 받는 일반 함수 호출과 비슷하고, Server Streaming은 하나의 요청 뒤 서버가 여러 응답을 흘려보낸다. Client Streaming은 클라이언트가 여러 데이터를 보낸 뒤 서버가 한 번 응답하고, Bidirectional Streaming은 양방향 스트림이 동시에 열린다. HTTP/2의 스트림 멀티플렉싱 덕분에 하나의 TCP 연결에서 여러 RPC를 동시에 처리할 수 있다. 다만 Client Streaming에서는 클라이언트가 complete, 즉 half-close를 호출하지 않으면 서버의 lastValueFrom이 끝나지 않아 deadline까지 hang할 수 있다.
- 07
프로덕션의 gRPC에는 타임아웃, 재시도, 회로차단기가 함께 따라와야 한다. Deadline 없이 호출을 무기한 기다리면 대기 중인 호출이 스레드풀을 소진시키고 연쇄 장애로 이어질 수 있다. Retry는 UNAVAILABLE 같은 일시적 오류에만 적용해야 하며, 지수 백오프와 지터로 재시도 시점을 분산해야 Thundering Herd를 줄일 수 있다. Circuit Breaker는 실패율이 임계치를 넘으면 요청을 즉시 차단해 하위 서비스가 회복할 시간을 준다. 상태는 CLOSED, OPEN, HALF-OPEN으로 전환되며, NestJS에서는 RxJS의 timeout과 retry, 그리고 @nestjs-resilience/core의 CircuitBreaker가 언급된다.
- 08
Webhook은 외부 시스템이 이벤트가 생겼을 때 내 서버의 HTTP 엔드포인트를 호출하는 역방향 API다. GitHub는 PR 머지 시 CI/CD 파이프라인을 트리거할 수 있고, Slack은 Incoming Webhooks로 알림을 받을 수 있으며, Toss나 KG이니시스 같은 결제사는 결제 완료나 실패 상태를 알려줄 수 있다. BackOps 관점에서는 이 흐름이 함께 이어진다. 배송사가 배송 완료 Webhook을 보내면 우리 서버가 DB를 업데이트하고, 고객 앱에는 WebSocket으로 배달 완료를 Push할 수 있다. 주문 서비스가 재고 서비스에 재고 차감을 요청하는 내부 통신은 gRPC가 담당할 수 있다.
- 09
선택 기준은 통신 방향과 책임 경계를 보면 정리된다. 외부 공개 API나 단순 데이터 조회는 REST가 자연스럽고, 브라우저를 포함한 실시간 채팅과 알림은 WebSocket이 우선이다. 내부 마이크로서비스 간 고성능 통신이나 스트리밍 로그, 청크 단위 파일 업로드는 gRPC가 강하다. 외부 시스템이 이벤트를 알려주는 구조라면 Webhook이 맞다. SSE는 서버에서 클라이언트로만 흐르는 단방향 스트림에 어울린다. WebSocket은 연결을 맺는 비용이 크지만 이후 통신이 싸고, gRPC는 proto라는 계약을 먼저 정의하는 대신 빠른 통신을 얻는 구조로 이해하면 된다.
- 10
트러블슈팅도 같은 원리에서 출발한다. ALB 뒤에서 WebSocket이 간헐적으로 끊기면 idle timeout 60초, Heartbeat 부재, Sticky Session 없는 다중 인스턴스를 먼저 의심한다. gRPC 클라이언트에서 No address added out of total 1 resolved가 보이면 url 옵션에 프로토콜을 포함했는지, protoPath가 dist 기준과 맞는지, Docker Compose에서 localhost를 잘못 썼는지 확인한다. Webhook의 HMAC 검증 실패는 파싱 전 원본 바이트가 사라졌을 때 자주 발생한다. proto 변경 후 타입 불일치가 생기면 필드 번호 재사용과 클라이언트 서버 버전 불일치를 의심해야 한다.
같은 레이어