WebSocket & gRPC basics
분류: Layer 1 - 백엔드 기초 | 작성일: 2026-04-02
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”WebSocket은 HTTP Upgrade 핸드셰이크로 연결을 맺은 뒤 단일 TCP 연결 위에서 서버와 클라이언트가 동시에 데이터를 주고받는 전이중(Full-Duplex) 통신 프로토콜이고, gRPC는 Protocol Buffers 이진 직렬화와 HTTP/2를 기반으로 언어에 무관한 고성능 원격 프로시저 호출(RPC)을 제공하는 프레임워크이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”- 실시간성 요구 증가: 채팅, 알림, 주문 상태 업데이트 등 “서버가 먼저 말을 거는” 시나리오가 늘어났다. HTTP 폴링으로 이를 구현하면 불필요한 요청이 폭발적으로 늘어난다.
- 마이크로서비스 내부 통신: REST는 JSON 파싱 오버헤드 때문에 수천 TPS 규모에서 병목이 된다. gRPC는 바이너리 직렬화와 HTTP/2 멀티플렉싱으로 서비스 간 통신 지연을 절반 이하로 줄인다 (2025년 벤치마크 기준 평균 48% 레이턴시 감소).
- BackOps 관점: 외부 시스템(결제사, 배송사)이 Webhook으로 상태를 Push하는 방식과, 내부 마이크로서비스가 gRPC로 통신하는 방식을 함께 이해해야 장애 시 원인 추적이 가능하다.
- Sticky Session 문제: WebSocket 서버를 수평 확장하면 ALB가 세션을 엉뚱한 인스턴스로 보내 연결이 끊긴다. 이 문제를 모르면 로드밸런서 앞에서 WebSocket이 왜 작동 안 하는지 알 수 없다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3-1. WebSocket: HTTP Upgrade 핸드셰이크와 전이중 통신
섹션 제목: “3-1. WebSocket: HTTP Upgrade 핸드셰이크와 전이중 통신”전화 통화를 생각해보자. HTTP는 편지와 같다 — 내가 편지를 보내면 상대가 답장을 보내고, 그 편지가 도착하기 전까지 둘 다 기다린다. WebSocket은 전화다 — 연결이 한 번 맺어지면 누구든 언제든 말을 걸 수 있고, 상대방이 먼저 말할 수도 있다.
원리: HTTP Upgrade 핸드셰이크
섹션 제목: “원리: HTTP Upgrade 핸드셰이크”WebSocket은 HTTP 위에서 시작한다. 클라이언트가 일반 HTTP 요청을 보내면서 “지금부터 WebSocket으로 업그레이드해도 될까요?”라고 묻는다.
# 1단계: 클라이언트 → 서버 (HTTP Upgrade 요청)GET /chat HTTP/1.1Host: example.comUpgrade: websocket ← "프로토콜 바꿔달라"Connection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ← 핸드셰이크 검증 키Sec-WebSocket-Version: 13
# 2단계: 서버 → 클라이언트 (101 Switching Protocols)HTTP/1.1 101 Switching Protocols ← "OK, 이제 WebSocket이다"Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=101 응답 이후, TCP 연결은 그대로 유지되고 HTTP가 아닌 WebSocket 프레임 형식으로 데이터가 오간다. 이 순간부터 서버도 클라이언트에게 먼저 데이터를 밀어넣을 수 있다 (서버 Push).
전이중(Full-Duplex) 통신
섹션 제목: “전이중(Full-Duplex) 통신”반이중(Half-Duplex)인 일반 무전기는 “끝(over)“을 말해야 상대가 말할 수 있다. WebSocket은 양쪽이 동시에 송·수신하는 전화처럼 동작한다. 단일 TCP 연결 하나로 양방향 데이터 스트림이 흐른다.
NestJS 실무 예시: @WebSocketGateway()
섹션 제목: “NestJS 실무 예시: @WebSocketGateway()”npm install @nestjs/websockets @nestjs/platform-socket.ioimport { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect,} from "@nestjs/websockets";import { Server, Socket } from "socket.io";
@WebSocketGateway({ namespace: "/chat", // ws://localhost:3000/chat 으로 연결 cors: { origin: "*" },})export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server;
// 클라이언트 연결 시 자동 호출 handleConnection(client: Socket) { console.log(`클라이언트 연결: ${client.id}`); }
// 클라이언트 연결 해제 시 자동 호출 handleDisconnect(client: Socket) { console.log(`클라이언트 해제: ${client.id}`); }
// 'sendMessage' 이벤트를 구독 @SubscribeMessage("sendMessage") handleMessage( @MessageBody() message: string, @ConnectedSocket() client: Socket, ): void { // 발신자를 제외한 모든 클라이언트에게 브로드캐스트 client.broadcast.emit("receiveMessage", { sender: client.id, message, timestamp: new Date().toISOString(), }); }}import { Module } from "@nestjs/common";import { ChatGateway } from "./chat.gateway";
@Module({ providers: [ChatGateway],})export class AppModule {}예상 출력 (서버 콘솔):
클라이언트 연결: abc123xyz클라이언트 연결: def456uvw클라이언트 해제: abc123xyz클라이언트(브라우저)에서 테스트:
const socket = io("http://localhost:3000/chat");socket.emit("sendMessage", "Hello!");socket.on("receiveMessage", (data) => console.log(data));// 출력: { sender: 'abc123xyz', message: 'Hello!', timestamp: '2026-04-02T...' }3-2. gRPC: Protocol Buffers 직렬화와 HTTP/2
섹션 제목: “3-2. gRPC: Protocol Buffers 직렬화와 HTTP/2”REST는 Word 문서를 이메일로 주고받는 것과 같다 — 텍스트(JSON)라서 읽기 쉽지만 파일 크기가 크고 파싱이 느리다. gRPC는 ZIP으로 압축된 이진 파일을 직접 전달하는 것과 같다 — 훨씬 작고 빠르지만, 압축을 풀 도구(proto 정의)를 양쪽이 모두 알고 있어야 한다.
원리: Protocol Buffers 직렬화
섹션 제목: “원리: Protocol Buffers 직렬화”Protocol Buffers(이하 Protobuf)는 구글이 만든 이진 직렬화 포맷이다. JSON {"name":"Alice","age":30}을 보내면 약 23바이트이지만, 같은 데이터를 Protobuf로 직렬화하면 약 8바이트로 줄어든다. 숫자를 텍스트가 아닌 실제 이진수로 표현하기 때문이다.
// user.proto — 데이터 스키마 정의syntax = "proto3";package user;
message User { string name = 1; // 필드 번호 1 int32 age = 2; // 필드 번호 2}// JSON: {"name":"Alice","age":30} → 23 bytes// Protobuf: 0a 05 41 6c 69 63 65 10 1e → ~9 bytes= 1, = 2는 값이 아니라 필드 번호다. 와이어 포맷에서 키 역할을 한다. 필드명 대신 필드 번호가 전송되므로 크기가 줄어든다.
원리: HTTP/2 기반 4가지 통신 패턴
섹션 제목: “원리: HTTP/2 기반 4가지 통신 패턴”| 패턴 | 방향 | 설명 | 예시 |
|---|---|---|---|
| Unary | 1 요청 → 1 응답 | 일반 함수 호출처럼 | 유저 정보 조회 |
| Server Streaming | 1 요청 → N 응답 | 서버가 데이터를 스트림으로 | 실시간 로그, 주식 시세 |
| Client Streaming | N 요청 → 1 응답 | 클라이언트가 여러 데이터 전송 후 서버가 결과 | 파일 청크 업로드 |
| Bidirectional Streaming | N 요청 ↔ N 응답 | 양방향 스트림 동시 | 실시간 채팅, 게임 |
HTTP/2의 스트림 멀티플렉싱 덕분에 하나의 TCP 연결 위에서 여러 RPC를 동시에 처리할 수 있다. HTTP/1.1에서 발생하던 HOL(Head-of-Line) 블로킹 문제가 해결된다.
.proto 파일 → NestJS gRPC 서비스 코드
섹션 제목: “.proto 파일 → NestJS gRPC 서비스 코드”npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loadersyntax = "proto3";package hero;
service HeroService { rpc FindOne (HeroById) returns (Hero); // Unary rpc FindAll (Empty) returns (stream Hero); // Server Streaming}
message HeroById { int32 id = 1; }message Hero { int32 id = 1; string name = 2; }message Empty {}// hero.controller.ts (gRPC 서버)import { Controller } from "@nestjs/common";import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";import { Observable, Subject } from "rxjs";
const heroes = [ { id: 1, name: "Lancelot" }, { id: 2, name: "Gawain" },];
@Controller()export class HeroController { // Unary: 단건 조회 @GrpcMethod("HeroService", "FindOne") findOne(data: { id: number }): { id: number; name: string } { return heroes.find((h) => h.id === data.id); }
// Server Streaming: 전체 목록을 스트림으로 전달 @GrpcMethod("HeroService", "FindAll") findAll(): Observable<{ id: number; name: string }> { const subject = new Subject<{ id: number; name: string }>(); // 비동기로 하나씩 emit heroes.forEach((hero, idx) => { setTimeout(() => { subject.next(hero); if (idx === heroes.length - 1) subject.complete(); }, idx * 100); }); return subject.asObservable(); }}// main.ts (마이크로서비스로 부팅)import { NestFactory } from "@nestjs/core";import { Transport, MicroserviceOptions } from "@nestjs/microservices";import { AppModule } from "./app.module";import { join } from "path";
async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.GRPC, options: { url: "localhost:5000", package: "hero", protoPath: join(__dirname, "hero.proto"), }, }, ); await app.listen(); console.log("gRPC 서버 시작: localhost:5000");}bootstrap();// client-app/hero.module.ts (gRPC 클라이언트)import { Module } from "@nestjs/common";import { ClientsModule, Transport } from "@nestjs/microservices";import { join } from "path";import { HeroClientController } from "./hero-client.controller";
@Module({ imports: [ ClientsModule.register([ { name: "HERO_PACKAGE", transport: Transport.GRPC, options: { url: "localhost:5000", package: "hero", protoPath: join(__dirname, "hero.proto"), }, }, ]), ], controllers: [HeroClientController],})export class HeroModule {}예상 출력:
# gRPC 서버 시작 시gRPC 서버 시작: localhost:5000
# Unary FindOne 호출 결과{ id: 1, name: 'Lancelot' }
# Server Streaming FindAll 호출 결과 (순차 수신){ id: 1, name: 'Lancelot' }{ id: 2, name: 'Gawain' }스트림 종료3-2-심화. gRPC 타임아웃·재시도·회로차단기
섹션 제목: “3-2-심화. gRPC 타임아웃·재시도·회로차단기”프로덕션에서 gRPC를 안정적으로 운영하려면 세 가지 패턴을 알아야 한다. **타임아웃(Deadline)**은 모든 RPC 호출에 반드시 설정해야 한다 — 무기한 대기 중인 호출이 스레드풀을 소진시켜 연쇄 장애로 이어진다. **재시도(Retry)**는 UNAVAILABLE 같은 일시적 오류에만 적용하고, 지수 백오프 + 지터(Jitter)로 재시도 시점을 분산해야 Thundering Herd를 막을 수 있다. **회로차단기(Circuit Breaker)**는 실패율이 임계치를 넘으면 요청을 즉시 차단해 하위 서비스가 회복할 시간을 준다(CLOSED → OPEN → HALF-OPEN 상태 전환). NestJS에서는 RxJS의 timeout() + retry() 연산자, @nestjs-resilience/core의 @CircuitBreaker() 데코레이터로 구현한다.
gRPC 장애 복구 절차 (체계적 순서)
섹션 제목: “gRPC 장애 복구 절차 (체계적 순서)”장애 발생 시 아래 순서대로 대응한다:
1. 감지: gRPC status code 확인 ├── UNAVAILABLE (14) → 일시적 장애, 재시도 가능 ├── DEADLINE_EXCEEDED (4) → 타임아웃, 재시도 가능 (멱등 호출만) ├── INTERNAL (13) → 서버 내부 오류, 재시도 불가 └── RESOURCE_EXHAUSTED (8) → 속도 제한, 백오프 후 재시도
2. 재시도: gRPC Connection Backoff 스펙 (grpc/grpc GitHub 공식) INITIAL_BACKOFF = 1s MULTIPLIER = 1.6 JITTER = 0.2 (±20% 랜덤) MAX_BACKOFF = 120s → 예: 1s → 1.6s → 2.56s → ... → 120s
3. 회로차단기 개입: 연속 실패 N회 초과 시 OPEN 상태 전환 OPEN → 요청 즉시 실패 반환 (하위 서비스 회복 시간 확보) HALF-OPEN → probe 요청 1개 허용 → 성공 시 CLOSED 복귀
4. 모니터링 지표 - grpc_client_handled_total{grpc_code="UNAVAILABLE"} — 상태 코드별 에러율 - grpc_client_handling_seconds — 레이턴시 분포 - circuit_breaker_state — CLOSED/OPEN/HALF-OPEN 상태출처: gRPC Connection Backoff Protocol (grpc.io 공식), gRPC Retry Policies — OneUptime
3-3. Webhook: 푸시 기반 통신 원리
섹션 제목: “3-3. Webhook: 푸시 기반 통신 원리”음식 배달을 생각해보자. 폴링은 “아직 배달 왔어요?”를 5분마다 전화로 묻는 것이다. Webhook은 배달원이 도착하면 벨을 눌러주는 것이다 — 내가 묻지 않아도 이벤트가 발생하면 서버가 내 URL을 직접 호출한다.
Webhook은 “역방향 API”다. 일반 API는 내가 서버에 요청하지만, Webhook은 외부 서버(GitHub, Slack, 결제사 등)가 이벤트 발생 시 내 서버의 HTTP 엔드포인트를 호출한다.
일반 API (Polling): 나 → GitHub "최근 커밋 알려줘" (반복 요청) → GitHub 응답
Webhook (Push): GitHub → (커밋 발생 시) → 내 서버 POST /webhook/githubimport { Controller, Post, Body, Headers } from "@nestjs/common";import * as crypto from "crypto";
@Controller("webhook")export class WebhookController { // GitHub Webhook 수신 엔드포인트 @Post("github") handleGithub( @Body() payload: any, @Headers("x-hub-signature-256") signature: string, ): { status: string } { // 1. 서명 검증 (Webhook 보안의 핵심) const secret = process.env.GITHUB_WEBHOOK_SECRET; const hmac = crypto .createHmac("sha256", secret) .update(JSON.stringify(payload)) .digest("hex"); const expected = `sha256=${hmac}`;
if (signature !== expected) { throw new Error("Invalid signature"); }
// 2. 이벤트 처리 const { action, repository, pusher } = payload; console.log(`Push 이벤트: ${pusher.name}이 ${repository.name}에 커밋`); return { status: "ok" }; }}실무 Webhook 예시:
- GitHub: PR 머지 시 CI/CD 파이프라인 트리거
- Slack: 알림 메시지 수신 (Incoming Webhooks)
- 결제사(Toss, KG이니시스): 결제 완료/실패 상태 통보
3-2-심화2. 실무 선택 기준 매트릭스
섹션 제목: “3-2-심화2. 실무 선택 기준 매트릭스”프론트엔드 개발자 출신이라면 “이미 WebSocket을 써봤는데 gRPC는 언제 쓰는 건가요?” 라는 질문이 자연스럽다. 아래 매트릭스로 상황별 선택을 빠르게 판단할 수 있다.
| 상황 | REST | WebSocket | gRPC | Webhook |
|---|---|---|---|---|
| 외부 공개 API (파트너사, 앱 클라이언트) | ✅ 최우선 | ⚠️ 가능 | ❌ 브라우저 미지원 | ❌ |
| 내부 마이크로서비스 간 통신 | ⚠️ 가능 | ❌ 비효율 | ✅ 최우선 | ❌ |
| 실시간 채팅·알림 (브라우저 포함) | ❌ 폴링은 낭비 | ✅ 최우선 | ❌ | ❌ |
| 외부 시스템이 이벤트를 Push할 때 | ❌ | ❌ | ❌ | ✅ 최우선 |
| 단순 데이터 조회 (초당 수십 건) | ✅ | ❌ 오버킬 | ⚠️ 가능 | ❌ |
| 스트리밍 로그·지표 수집 | ❌ | ⚠️ 가능 | ✅ Server Streaming | ❌ |
| 파일 업로드 (청크 단위) | ⚠️ 가능 | ❌ | ✅ Client Streaming | ❌ |
결정 흐름도:
브라우저/모바일 클라이언트와 통신?├── YES → REST (공개 API) 또는 WebSocket (실시간)│ └── 1초 이상 간격으로 폴링 중? → WebSocket으로 전환└── NO (서버↔서버) → 성능/타입 안정성 중요? ├── YES → gRPC └── NO → REST (단순하고 디버깅 쉬움)
외부 시스템이 이벤트를 "알려준다"? → Webhook📖 더 보기: gRPC vs WebSocket — Ably — 두 프로토콜의 use case를 실제 아키텍처 다이어그램으로 비교 (중급)
3-2-심화3. WebSocket Heartbeat — 연결이 살아있는지 어떻게 확인하는가
섹션 제목: “3-2-심화3. WebSocket Heartbeat — 연결이 살아있는지 어떻게 확인하는가”전화를 하다 보면 상대방이 말이 없어서 “여보세요?” 하고 물어보는 경우가 있다. WebSocket의 Ping-Pong이 바로 이 역할이다. 서버가 주기적으로 “여보세요?” 신호를 보내고, 클라이언트가 “네, 있어요”라고 응답하지 않으면 연결이 끊어진 것으로 판단한다.
원리: RFC 6455 Ping-Pong 프레임
섹션 제목: “원리: RFC 6455 Ping-Pong 프레임”WebSocket 프로토콜은 두 가지 제어 프레임을 정의한다:
- Ping 프레임 (opcode 0x9): 서버가 클라이언트에게 “살아있냐?” 신호 전송
- Pong 프레임 (opcode 0xA): 클라이언트가 Ping을 받으면 동일한 페이로드로 즉시 응답
이 제어 프레임은 애플리케이션 메시지와 다른 채널로 흐른다. 채팅 메시지를 주고받는 중에도 Ping-Pong은 독립적으로 동작한다.
[서버] ──── Ping (opcode 0x9, payload: "alive?") ───→ [클라이언트][서버] ←─── Pong (opcode 0xA, payload: "alive?") ──── [클라이언트] (클라이언트는 Ping 페이로드를 그대로 Pong에 담아 응답)
타임아웃 시나리오:[서버] ──── Ping ───→ [클라이언트 (네트워크 끊김)][서버] ─── pingTimeout ms 대기 ─── Pong 없음 ─── 연결 강제 종료왜 Heartbeat가 필요한가
섹션 제목: “왜 Heartbeat가 필요한가”TCP 연결은 데이터를 보내지 않아도 겉으로는 살아있어 보일 수 있다. 특히:
- 방화벽/NAT 장비는 유휴 TCP 연결을 일정 시간 후 조용히 끊는다 (NAT timeout)
- AWS ALB의 기본 idle timeout은 60초 — 60초간 데이터 없으면 연결 종료
- 모바일 기기의 슬립 모드 진입 시 OS가 소켓을 닫을 수 있다
Heartbeat는 이 문제를 두 가지 방식으로 해결한다:
- Keepalive: 작은 Ping 패킷을 주기적으로 보내 방화벽/NAT이 연결을 살아있다고 인식하게 함
- Dead Connection 감지: 응답이 없는 클라이언트를 감지해 서버 리소스 낭비 방지
NestJS Socket.IO에서 Heartbeat 설정
섹션 제목: “NestJS Socket.IO에서 Heartbeat 설정”@WebSocketGateway({ namespace: "/chat", cors: { origin: "*" }, // Socket.IO Heartbeat 설정 pingInterval: 25000, // 25초마다 서버가 Ping 전송 pingTimeout: 20000, // 20초 안에 Pong 없으면 연결 끊음 // ※ AWS ALB idle timeout(60초)보다 pingInterval이 짧아야 // ALB가 연결을 조용히 끊는 것을 막을 수 있다})export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server;
handleConnection(client: Socket) { console.log(`연결: ${client.id}`); // 연결 시 핸드셰이크 응답에 pingInterval/pingTimeout 포함 // 클라이언트(Socket.IO)는 이 값을 받아 자동으로 Pong 처리 }
handleDisconnect(client: Socket) { // pingTimeout 초과 또는 명시적 disconnect 시 여기서 정리 console.log(`해제: ${client.id} (이유: ${client.disconnected})`); }}예상 동작 (서버 로그):
[연결] abc123xyz 접속[Ping] → abc123xyz (25초 경과)[Pong] ← abc123xyz (2ms 응답)[Ping] → abc123xyz (25초 경과)[Pong] ← abc123xyz (3ms 응답)[Ping] → abc123xyz (25초 경과)[타임아웃] abc123xyz — 20초 내 Pong 없음 → 연결 강제 종료[해제] abc123xyzSocket.IO Handshake에서 Heartbeat 파라미터 확인:
// 브라우저 개발자 도구 → Network → WebSocket → Messages// 첫 번째 메시지 (엔진.io 초기화):// 0{"sid":"abc123","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}// ↑ ↑// 서버 설정값이 클라이언트에게 전달됨ALB idle timeout과 Heartbeat의 관계
섹션 제목: “ALB idle timeout과 Heartbeat의 관계”잘못된 설정:pingInterval = 60000 (60초)ALB idle timeout = 60초→ ALB가 60초에 연결을 끊으려는 타이밍과 Ping이 겹쳐 간헐적 연결 끊김 발생
올바른 설정:pingInterval = 25000 (25초) ← ALB idle timeout의 절반 이하로ALB idle timeout = 60초→ 25초마다 Ping이 전송되므로 ALB는 항상 "활성 연결"로 인식📖 더 보기: WebSocket Heartbeat/Ping-Pong — OneUptime — Heartbeat 구현 방법과 각 언어별 예시 (입문)
WebSocket 장애 복구 절차 — Exponential Backoff Reconnect
섹션 제목: “WebSocket 장애 복구 절차 — Exponential Backoff Reconnect”RFC 6455는 재연결 전략을 애플리케이션 레이어에 위임한다. 업계 표준으로 정착된 패턴은 다음과 같다 (WebSocket.org 재연결 가이드):
1. Heartbeat timeout 감지 (pingTimeout 초과 → Pong 미수신)2. 연결 종료 (close event)3. Exponential Backoff 재연결 시도: delay(attempt) = min(base × 2^attempt + jitter, maxDelay) base = 1000ms (1초) jitter = random(0, 500ms) ← Thundering Herd 방지 maxDelay = 30000ms (30초)
attempt 0: ~1s attempt 1: ~2s attempt 2: ~4s attempt 3: ~8s ... attempt ≥5: ~30s (cap)
4. Connection pool 고갈 체크 (서버 측 active connection 수 모니터링)5. 재연결 성공 → backoff 카운터 리셋6. N회(예: 10회) 실패 → 사용자에게 "연결 오류" 알림 표시 + 수동 재시도 유도왜 Jitter가 필요한가: 서버 재시작 시 수천 개 클라이언트가 동일한 backoff 스케줄로 동시 재연결을 시도하면 재연결 폭풍(Reconnection Storm)이 발생한다. Jitter로 재연결 시점을 분산하면 서버가 점진적으로 회복할 수 있다.
// WebSocket 재연결 핵심 로직 (Socket.IO는 이를 자동 처리)let attempt = 0;const BASE_DELAY = 1000;const MAX_DELAY = 30000;
function reconnect() { const delay = Math.min( BASE_DELAY * 2 ** attempt + Math.random() * 500, MAX_DELAY, ); attempt++; setTimeout(() => { connect(); // 재연결 시도 }, delay);}
ws.onclose = () => reconnect();ws.onopen = () => { attempt = 0;}; // 성공 시 리셋출처: WebSocket.org — Reconnection Guide, RFC 6455 — The WebSocket Protocol
3-2-심화4. 전이 가능 원리 — 이 패턴은 어디에서나 등장한다
섹션 제목: “3-2-심화4. 전이 가능 원리 — 이 패턴은 어디에서나 등장한다”WebSocket과 gRPC에서 배운 원리는 독립된 기술 지식이 아니다. 분산 시스템 전반에 반복 등장하는 범용 패턴이다.
| 원리 | WebSocket에서 | gRPC에서 | 다른 도메인에서 |
|---|---|---|---|
| Heartbeat / Keep-alive | Ping-Pong 프레임 (RFC 6455 opcode 0x9/0xA) | HTTP/2 PING 프레임 (연결 유지) | TCP Keepalive (SO_KEEPALIVE), MQTT PINGREQ/PINGRESP, Kubernetes Liveness Probe |
| Circuit Breaker | 재연결 N회 실패 → 30초 대기 (fail-fast) | UNAVAILABLE 연속 실패 → OPEN 상태 전환 | API Gateway (Kong, Envoy), Netflix Hystrix, AWS ALB 헬스체크 |
| Exponential Backoff + Jitter | 재연결 지연 (1s→2s→4s→30s cap) | 연결 백오프 (INITIAL=1s, MULTIPLIER=1.6, MAX=120s) | HTTP 재시도, AWS SDK 재시도 정책, 메시지 큐(SQS) 재처리 |
| Binary Serialization (Schema-first) | — | Protobuf (필드 번호 기반, ~3–10x 작은 페이로드) | MessagePack (JSON-compatible binary), Apache Avro (스키마 레지스트리), FlatBuffers (zero-copy) |
Binary Serialization 공통 원리: Protobuf·MessagePack·Avro·FlatBuffers 모두 동일한 철학을 따른다 — schema-first(스키마를 먼저 정의) + compact encoding(필드명 대신 숫자 태그 전송). 덕분에 JSON 대비 페이로드를 3–10배 압축하면서도 타입 안정성을 확보한다.
출처: gRPC Mobile Benchmarks (grpc.io 공식), gRPC Connection Backoff Protocol (grpc.io 공식)
3-3-심화. WebSocket 대규모 스케일링
섹션 제목: “3-3-심화. WebSocket 대규모 스케일링”WebSocket 서버를 수평 확장할 때 핵심 문제는 인스턴스 간 메시지 공유다. 클라이언트 A가 인스턴스 1에, 클라이언트 B가 인스턴스 2에 연결된 상태에서 인스턴스 1이 전체 emit을 호출하면 인스턴스 2의 클라이언트 B는 메시지를 받지 못한다. 이를 해결하는 패턴이 Redis Pub/Sub 백플레인이다 — 모든 인스턴스가 Redis를 통해 메시지를 공유하므로 어느 인스턴스가 emit해도 전체에 전달된다. Socket.IO에서는 @socket.io/redis-adapter를 연결하면 된다. 또한 server.emit()으로 전체 브로드캐스트하는 대신 룸(Room) 단위로 server.to('order:123').emit()을 사용하면 대상 클라이언트 수를 줄여 CPU 부하를 크게 낮출 수 있다.
3-4. Sticky Session: WebSocket + 로드밸런서에서 왜 필요한가
섹션 제목: “3-4. Sticky Session: WebSocket + 로드밸런서에서 왜 필요한가”콜센터를 생각해보자. 고객이 첫 번째 전화에서 상담원 A와 대화를 시작했는데, 두 번째 전화가 상담원 B에게 연결된다면 처음부터 다시 설명해야 한다. WebSocket도 마찬가지다 — 한 번 연결된 소켓은 특정 서버 인스턴스와 연결을 유지해야 한다.
HTTP는 무상태(Stateless)라 어느 서버에 요청이 가도 상관없다. 하지만 WebSocket은 상태가 있다(Stateful) — 클라이언트의 소켓 연결 객체가 특정 서버 프로세스 메모리에 저장된다. ALB가 다음 요청을 다른 인스턴스로 보내면 그 서버에는 연결 정보가 없어서 연결이 끊긴다.
문제 상황:클라이언트 ──── ALB ──┬── Instance A (소켓 연결 있음) ← 첫 요청 └── Instance B (소켓 연결 없음) ← 두 번째 요청 → 연결 끊김!
해결책 1: Sticky Session (세션 고착화)클라이언트 ──── ALB (쿠키로 인스턴스 지정) ──── Instance A만 사용
해결책 2: Redis Pub/Sub (권장)클라이언트 ──── ALB ──── Instance A ──┐ Instance B ──┤── Redis ← 모든 인스턴스가 메시지 공유 Instance C ──┘ALB + WebSocket에서 Sticky Session이 필요한 이유: WebSocket은 상태가 있는(Stateful) 프로토콜이다. 소켓 연결 객체가 특정 서버 프로세스 메모리에 저장되므로, ALB가 이후 요청을 다른 인스턴스로 보내면 그 서버에는 연결 정보가 없어 연결이 끊긴다. Sticky Session(lb_cookie)은 ALB가 쿠키를 심어 같은 클라이언트 요청을 항상 같은 인스턴스로 라우팅하게 한다.
Redis Adapter가 더 나은 이유: Sticky Session은 특정 인스턴스에 트래픽이 쏠리고, 인스턴스 장애 시 해당 클라이언트 모두 재연결이 필요하다는 단점이 있다. 반면 Redis Adapter(@socket.io/redis-adapter)를 사용하면 클라이언트가 어느 인스턴스에 연결되든 Redis Pub/Sub을 통해 메시지가 모든 인스턴스에 공유된다. 따라서 프로덕션 환경에서는 Sticky Session 대신 Redis Adapter를 사용하는 것이 권장된다.
4. 실무에서 어떻게 쓰이나
섹션 제목: “4. 실무에서 어떻게 쓰이나”| 기술 | 실무 시나리오 | 특징 |
|---|---|---|
| WebSocket | 실시간 주문 상태 알림, 배달 위치 추적, 채팅 | 서버가 먼저 Push 필요할 때 |
| gRPC | 주문 서비스 ↔ 재고 서비스 내부 통신 | 마이크로서비스 간 고성능 통신 |
| Webhook | GitHub Actions 트리거, 결제 완료 콜백 | 외부 시스템이 이벤트를 Push할 때 |
| SSE | 서버 로그 스트리밍, 진행률 표시 | 단방향 서버→클라이언트 스트림 |
BackOps 실무 예시:
- 배송사 API가 배송 완료 시 우리 서버의
/webhook/delivery를 호출 → DB 업데이트 → WebSocket으로 고객 앱에 “배달 완료” Push - 주문 서비스가 gRPC로 재고 서비스에 “재고 차감” 요청 → 재고 서비스가 응답 → 주문 확정
5. 내 업무와 어떻게 연결되나
섹션 제목: “5. 내 업무와 어떻게 연결되나”- NestJS 마이크로서비스:
@nestjs/microservices패키지는 gRPC를 기본 전송 계층으로 지원한다. 서비스 간 REST API 대신 gRPC를 적용하면 레이턴시와 페이로드 크기를 모두 줄일 수 있다. - AWS ALB + WebSocket: 로드밸런서 앞에 WebSocket 서버를 놓을 때 Sticky Session 또는 Redis Adapter 설정 없이 배포하면 클라이언트 연결이 간헐적으로 끊기는 버그가 생긴다.
- Webhook 수신 서버: 외부 결제/배송 API가 Webhook을 보낼 때 서명 검증(HMAC-SHA256)을 빠뜨리면 보안 취약점이 된다. NestJS에서
RawBody를 활성화해야 서명 검증이 가능하다. - proto 파일 버전 관리: 팀 내 gRPC proto 파일을 별도 Git 레포(혹은 모노레포)로 관리하지 않으면 서비스 간 스키마 불일치(breaking change)가 발생한다.
6. 비교 / 대안
섹션 제목: “6. 비교 / 대안”WebSocket vs HTTP Polling vs SSE
섹션 제목: “WebSocket vs HTTP Polling vs SSE”| 항목 | HTTP Polling | SSE | WebSocket |
|---|---|---|---|
| 방향 | 클라이언트→서버 반복 요청 | 서버→클라이언트 단방향 | 양방향 전이중 |
| 연결 | 매 요청마다 새 연결 | 지속 연결 (단방향) | 지속 연결 (양방향) |
| 오버헤드 | 매우 높음 (HTTP 헤더 반복) | 낮음 | 가장 낮음 |
| 서버 Push | 불가 | 가능 | 가능 |
| 브라우저 지원 | 모든 브라우저 | 대부분 지원 | 대부분 지원 |
| 적합한 상황 | 변경 드문 데이터 | 서버→클라 단방향 알림 | 실시간 양방향 통신 |
| NestJS 지원 | axios/fetch | EventSource | @WebSocketGateway |
gRPC vs REST 선택 기준
섹션 제목: “gRPC vs REST 선택 기준”| 기준 | REST 선택 | gRPC 선택 |
|---|---|---|
| 클라이언트 | 브라우저, 외부 개발자 | 서버 간 통신, 내부 서비스 |
| 페이로드 | 사람이 읽어야 함 (JSON) | 크기/속도 최적화 필요 |
| 통신 패턴 | 단순 요청-응답 | 스트리밍, 양방향 필요 |
| 언어 | 다양한 언어, 레거시 | 여러 언어 팀, 강타입 계약 |
| 성능 | 초당 수백 요청 | 초당 수천~수만 요청 |
| 디버깅 | curl, Postman으로 쉽게 | grpcurl 필요, 러닝커브 있음 |
2025년 기준: 93% 팀이 REST 사용, 14%가 gRPC 사용 (Postman State of the API Report). 공개 API는 REST, 내부 마이크로서비스 통신은 gRPC가 업계 표준 패턴이다.
정량적 성능 비교 (벤치마크 기반)
섹션 제목: “정량적 성능 비교 (벤치마크 기반)”| 지표 | REST (JSON + HTTP/1.1) | gRPC (Protobuf + HTTP/2) | 출처 |
|---|---|---|---|
| 페이로드 크기 | 기준 (100%) | ~10–30% (70–90% 감소) | grpc.io Mobile Benchmarks |
| 직렬화 속도 | 기준 | ~3x 빠름 | Medium 벤치마크 |
| 처리량 (대용량) | 기준 | ~10x | Java Code Geeks 2024 |
| 레이턴시 (소용량) | REST 우위 | 비슷하거나 불리 | Sahibinden Technology |
| 동시 요청 처리 | 기준 (HOL 블로킹) | 2–3x (HTTP/2 멀티플렉싱) | AWS 비교 가이드 |
출처: gRPC Mobile Benchmarks (grpc.io 공식), Compare gRPC with HTTP APIs (Microsoft Learn), REST vs gRPC 2024 (Java Code Geeks)
gRPC가 REST보다 불리한 경계 조건
섹션 제목: “gRPC가 REST보다 불리한 경계 조건”아래 상황에서는 REST가 더 나은 선택이다:
| 상황 | 이유 | 권장 |
|---|---|---|
| 브라우저 직접 호출 | 브라우저는 HTTP/2 raw 프레임 접근 불가 → gRPC-Web 프록시(Envoy) 필수, 클라이언트 스트리밍 미지원 | REST 또는 WebSocket |
| 소용량 단순 조회 | 페이로드 <1KB 구간에서는 REST 레이턴시가 경쟁력 있음 | REST |
| 디버깅·운영 편의 | Protobuf는 이진 포맷 → curl·Postman 불가, grpcurl + proto 파일 필요 | REST |
| 스키마 변경 비용 | 필드 추가/삭제 시 proto 파일 재배포 → 모든 서비스 동시 업데이트 필요 | REST (하위 호환 JSON) |
| 레거시·외부 파트너 연동 | 파트너사 시스템이 JSON REST만 지원하는 경우 | REST |
WebSocket이 SSE보다 불리한 상황: 서버→클라이언트 단방향 스트리밍만 필요할 때는 SSE가 더 단순하다. SSE는 일반 HTTP를 재사용하므로 로드밸런서 설정이 훨씬 쉽고, 자동 재연결(EventSource)이 브라우저 내장 기능으로 지원된다. WebSocket은 이 단순한 시나리오에서 Sticky Session/Redis Adapter 설정까지 요구하는 오버킬이다.
출처: The state of gRPC in the browser (grpc.io 공식), Compare gRPC services with HTTP APIs (Microsoft Learn)
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”트러블슈팅 1: WebSocket 연결이 ALB 뒤에서 간헐적으로 끊김
섹션 제목: “트러블슈팅 1: WebSocket 연결이 ALB 뒤에서 간헐적으로 끊김”증상:
WebSocket connection to 'wss://api.example.com/chat' failed:Error in connection establishment: net::ERR_CONNECTION_RESET클라이언트 콘솔에서 위 에러가 랜덤하게 발생하고, 새로고침하면 잠시 연결됐다가 다시 끊긴다.
원인: AWS ALB의 기본 idle timeout은 60초인데, WebSocket 연결은 heartbeat 없이 60초가 넘으면 ALB가 연결을 강제 종료한다. 또는 Sticky Session 없이 다중 인스턴스가 실행 중이라 핸드셰이크와 이후 프레임이 다른 인스턴스로 라우팅된다.
해결:
- ALB Idle Timeout 증가 (기본 60초 → 3600초):
AWS 콘솔 → Load Balancers → 해당 ALB → Attributes → Idle timeout: 3600
- Sticky Session 활성화 (단기 해결):
Target Group → Attributes → Stickiness → Enable, Duration: 1day
- Socket.IO heartbeat 설정 (근본 해결):
@WebSocketGateway({pingInterval: 25000, // 25초마다 pingpingTimeout: 60000, // 60초 응답 없으면 연결 끊음})
트러블슈팅 2: gRPC 서비스 연결 시 “No address added out of total 1 resolved” 에러
섹션 제목: “트러블슈팅 2: gRPC 서비스 연결 시 “No address added out of total 1 resolved” 에러”증상:
Error: 14 UNAVAILABLE: No address added out of total 1 resolved at Object.callErrorFromStatus (.../grpc-js/src/call.ts:81:17)NestJS gRPC 클라이언트가 서버에 연결을 시도하지만 실패한다.
원인:
url옵션에localhost:5000대신http://localhost:5000또는grpc://localhost:5000같이 프로토콜을 포함했거나- proto 파일 경로(
protoPath)가 빌드된/dist기준이 아닌/src기준으로 설정되어 있음 - Docker Compose 환경에서 서비스명이 아닌
localhost를 사용
해결:
// 잘못된 설정options: { url: 'http://localhost:5000', // ❌ 프로토콜 포함 금지 protoPath: './src/hero.proto', // ❌ 빌드 후 경로 불일치}
// 올바른 설정options: { url: 'localhost:5000', // ✅ host:port 형식 protoPath: join(__dirname, '../hero.proto'), // ✅ __dirname 기준 절대 경로}
// Docker Compose 환경options: { url: 'hero-service:5000', // ✅ 서비스명 사용}트러블슈팅 3: Webhook 수신 시 HMAC 서명 검증 실패
섹션 제목: “트러블슈팅 3: Webhook 수신 시 HMAC 서명 검증 실패”증상:
Error: Invalid signature at WebhookController.handleGithub (webhook.controller.ts:24)GitHub에서 Webhook이 날아오는데 서명 검증이 항상 실패한다.
원인:
NestJS의 body-parser가 JSON을 파싱하면서 원본 raw body가 사라진다. HMAC 서명은 파싱 전 원본 바이트를 기반으로 계산하는데, JSON.stringify(parsedBody)로 재직렬화하면 공백·키 순서가 달라져 서명이 맞지 않는다.
해결:
// main.ts — rawBody 활성화const app = await NestFactory.create(AppModule, { rawBody: true });
// webhook.controller.ts — RawBody 사용import { RawBodyRequest } from '@nestjs/common';import { Request } from 'express';
@Post('github')handleGithub( @Req() req: RawBodyRequest<Request>, // rawBody 접근 @Headers('x-hub-signature-256') signature: string,) { const hmac = crypto .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET) .update(req.rawBody) // ✅ 원본 바이트로 계산 .digest('hex'); const expected = `sha256=${hmac}`; if (signature !== expected) throw new UnauthorizedException();}트러블슈팅 4: gRPC proto 변경 후 “Expected type string, got number” 타입 불일치
섹션 제목: “트러블슈팅 4: gRPC proto 변경 후 “Expected type string, got number” 타입 불일치”증상:
Error: Uncaught Error at /handler (grpc_call.ts): Expected type string for field user.id, got numberproto 파일을 수정했는데 클라이언트와 서버의 동작이 달라진다.
원인:
proto 파일을 서버 측에서만 수정하고 클라이언트(다른 서비스)에는 배포하지 않아서 필드 타입이 불일치한다. int32를 string으로 바꾸거나, 필드 번호를 재사용하면 이런 문제가 발생한다.
해결:
- proto 파일은 별도 Git 레포 또는 패키지(npm)로 관리하고 서버-클라이언트가 항상 같은 버전을 사용한다.
- 필드 번호는 절대 재사용하지 않는다 — 삭제한 필드 번호는
reserved로 표시한다:message User {reserved 2; // 과거에 age 였던 필드, 재사용 금지reserved "age";string id = 1;string name = 3;}
트러블슈팅 5: gRPC 에러가 HTTP 500으로만 반환됨 — RpcException 미사용
섹션 제목: “트러블슈팅 5: gRPC 에러가 HTTP 500으로만 반환됨 — RpcException 미사용”증상:
NestJS gRPC 서버에서 에러가 발생했을 때 클라이언트가 항상 INTERNAL (status code 13) 에러만 받는다. “Not Found”나 “Invalid Argument” 같은 정확한 gRPC 상태 코드가 전달되지 않는다.
원인:
일반 HttpException이나 Error를 throw하면 gRPC 레이어에서 모두 INTERNAL 코드로 변환된다. gRPC는 HTTP 상태 코드가 아닌 자체 상태 코드 체계(0=OK, 5=NOT_FOUND, 3=INVALID_ARGUMENT 등)를 사용한다.
해결:
import { RpcException } from '@nestjs/microservices';import { status } from '@grpc/grpc-js';
@GrpcMethod('HeroService', 'FindOne')findOne(data: { id: number }) { const hero = heroes.find((h) => h.id === data.id);
// ❌ 잘못된 방법 — HTTP 예외는 gRPC에서 INTERNAL로 변환됨 // if (!hero) throw new NotFoundException('Hero not found');
// ✅ 올바른 방법 — RpcException으로 gRPC 상태 코드 명시 if (!hero) { throw new RpcException({ code: status.NOT_FOUND, // gRPC 상태 코드 5 message: `Hero with id ${data.id} not found`, }); } return hero;}# gRPC 주요 상태 코드status.OK = 0 // 성공status.INVALID_ARGUMENT = 3 // 잘못된 입력 (HTTP 400)status.NOT_FOUND = 5 // 리소스 없음 (HTTP 404)status.ALREADY_EXISTS = 6 // 중복 (HTTP 409)status.UNAUTHENTICATED = 16 // 인증 실패 (HTTP 401)status.PERMISSION_DENIED = 7 // 권한 없음 (HTTP 403)status.INTERNAL = 13 // 서버 내부 오류 (HTTP 500)7. 체크리스트
섹션 제목: “7. 체크리스트”WebSocket
섹션 제목: “WebSocket”- HTTP Upgrade 핸드셰이크(101 Switching Protocols)의 흐름을 설명할 수 있다
-
@WebSocketGateway()+@SubscribeMessage()로 채팅 서버를 구현할 수 있다 - HTTP Polling / SSE / WebSocket의 차이와 선택 기준을 말할 수 있다
- ALB + WebSocket 환경에서 Sticky Session이 왜 필요한지 설명할 수 있다
- Redis Adapter를 사용하면 Sticky Session 없이도 멀티 인스턴스가 가능함을 이해한다
gRPC
섹션 제목: “gRPC”- Protocol Buffers가 JSON보다 작은 이유(이진 직렬화)를 설명할 수 있다
-
.proto파일의 필드 번호가 키 역할을 함을 이해한다 - Unary / Server Streaming / Client Streaming / Bidirectional Streaming 4가지 패턴을 구분할 수 있다
- NestJS에서
@GrpcMethod()로 gRPC 서버를 구현할 수 있다 - REST와 gRPC 중 상황에 맞는 것을 선택할 수 있다
Webhook
섹션 제목: “Webhook”- Webhook이 Polling과 다른 점(Push vs Pull)을 설명할 수 있다
- HMAC-SHA256 서명 검증의 목적과 NestJS rawBody 설정 방법을 안다
8. 키워드
섹션 제목: “8. 키워드”WebSocket, HTTP Upgrade, 101 Switching Protocols, Full-Duplex, @WebSocketGateway, @SubscribeMessage, Socket.IO, Sticky Session, Redis Adapter, gRPC, Protocol Buffers, Protobuf, HTTP/2, Multiplexing, Unary, Server Streaming, Client Streaming, Bidirectional Streaming, @GrpcMethod, .proto, Webhook, Push 기반, HMAC-SHA256, rawBody, SSE, Polling, ALB Sticky Session
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 NestJS 공식 문서 — WebSocket Gateways —
@WebSocketGateway설정부터 네임스페이스, 생명주기 훅까지 공식 API 레퍼런스 (입문) - 📖 NestJS 공식 문서 — gRPC Microservices —
.proto파일 정의부터@GrpcMethod,ClientsModule설정, 스트리밍 패턴까지 공식 레퍼런스 (중급) - 📖 RFC 6455 — The WebSocket Protocol — WebSocket 공식 프로토콜 스펙. Ping-Pong 프레임(opcode 0x9/0xA) 등 핵심 제어 프레임 정의 (공식)
- 📖 gRPC Connection Backoff Protocol — gRPC 공식 재연결 백오프 스펙. INITIAL_BACKOFF=1s, MULTIPLIER=1.6, MAX=120s 등 파라미터 정의 (공식)
- 📖 The state of gRPC in the browser — gRPC의 브라우저 직접 지원 불가 이유와 gRPC-Web 프록시 방식 설명 (grpc.io 공식)
- 📖 Compare gRPC services with HTTP APIs — Microsoft Learn의 gRPC vs REST 공식 비교. 브라우저 지원 제약, 디버깅 어려움 등 경계 조건 포함 (공식)
- 📖 WebSocket.org — Reconnection Guide — Exponential Backoff 재연결 패턴과 Jitter 필요성 설명 (중급)
- 📖 AWS — gRPC vs REST 차이점 비교 — AWS가 정리한 gRPC vs REST 선택 가이드, 각 기술의 장단점과 유스케이스를 간결하게 설명 (입문)
- 📖 Semaphore — Microservices Communication in NestJS With gRPC — NestJS + gRPC 마이크로서비스 실전 구현 튜토리얼, proto 파일부터 서버-클라이언트 연결까지 단계별 코드 포함 (중급)
- 🎬 gRPC with NestJS — DEV Community 입문 가이드 — gRPC 기초부터 NestJS 통합, 채팅 서비스 구현까지 단계별 설명 (입문)
9. 직접 확인해보기
섹션 제목: “9. 직접 확인해보기”WebSocket 서버 로컬 실행 + 연결 테스트
섹션 제목: “WebSocket 서버 로컬 실행 + 연결 테스트”# 1. NestJS 프로젝트 생성npx @nestjs/cli new ws-demo --package-manager npmcd ws-demo
# 2. 의존성 설치npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
# 3. 게이트웨이 생성npx nest g gateway chat
# 4. 서버 실행npm run start:dev예상 출력:[Nest] LOG [NestFactory] Starting Nest application...[Nest] LOG [InstanceLoader] AppModule dependencies initialized[Nest] LOG [WebSocketsController] ChatGateway subscribed to the "sendMessage" messages[Nest] LOG [NestApplication] Nest application successfully started# 5. WebSocket 연결 테스트 (wscat 사용)npm install -g wscatwscat -c ws://localhost:3000/chat
# 입력:{"event":"sendMessage","data":"Hello, WebSocket!"}
# 예상 출력 (다른 클라이언트 창에서):< {"event":"receiveMessage","data":{"sender":"abc123","message":"Hello, WebSocket!","timestamp":"2026-04-02T..."}}gRPC 서비스 로컬 테스트
섹션 제목: “gRPC 서비스 로컬 테스트”# grpcurl 설치 (macOS)brew install grpcurl
# gRPC 서버 실행 후npm run start:dev
# Unary 호출 테스트grpcurl -plaintext \ -proto hero.proto \ -d '{"id": 1}' \ localhost:5000 \ hero.HeroService/FindOne예상 출력:{ "id": 1, "name": "Lancelot"}# Server Streaming 테스트grpcurl -plaintext \ -proto hero.proto \ -d '{}' \ localhost:5000 \ hero.HeroService/FindAll예상 출력 (순차 스트림):{ "id": 1, "name": "Lancelot"}{ "id": 2, "name": "Gawain"}Webhook 서명 검증 테스트
섹션 제목: “Webhook 서명 검증 테스트”# GitHub Webhook 시뮬레이션 (curl)SECRET="my-webhook-secret"PAYLOAD='{"action":"push","repository":{"name":"my-repo"},"pusher":{"name":"alice"}}'SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print "sha256="$2}')
curl -X POST http://localhost:3000/webhook/github \ -H "Content-Type: application/json" \ -H "x-hub-signature-256: $SIGNATURE" \ -d "$PAYLOAD"예상 출력:{"status":"ok"}10. 요약
섹션 제목: “10. 요약”| 기술 | 핵심 원리 | 사용 시점 | NestJS 진입점 |
|---|---|---|---|
| WebSocket | HTTP Upgrade → 전이중 TCP 연결 유지 | 양방향 실시간 통신 | @WebSocketGateway |
| gRPC | Protobuf 이진 직렬화 + HTTP/2 멀티플렉싱 | 마이크로서비스 간 고성능 통신 | @GrpcMethod, ClientsModule |
| Webhook | 이벤트 발생 시 외부가 내 서버 HTTP 호출 | 외부 시스템 이벤트 수신 | @Post('/webhook/...') |
| Sticky Session | ALB가 쿠키로 동일 인스턴스에 라우팅 | WebSocket + 다중 인스턴스 | ALB Target Group 설정 |
핵심 판단 기준:
- 서버가 먼저 Push해야 한다 → WebSocket (양방향) 또는 SSE (단방향)
- 서비스 간 통신, 성능이 중요하다 → gRPC
- 외부 시스템이 이벤트를 알려준다 → Webhook
- WebSocket + 다중 서버 → Sticky Session + Redis Adapter
WebSocket은 “연결을 맺는 비용이 크지만 이후는 싸다”, gRPC는 “계약서(proto)를 먼저 정의하면 통신이 빠르다”는 철학을 이해하면 두 기술의 트레이드오프가 명확해진다.