본문으로 건너뛰기

안동민 개발노트

본문 시작

gRPC를 이용한 서비스 간 통신

Protocol Buffers로 서비스 계약을 정의하고 NestJS gRPC 서버와 클라이언트를 연결해 타입 기반 RPC를 실행합니다.

지난 절에서는 NestJS로 기본 마이크로서비스를 만들고 TCP 기반 서비스 간 통신을 확인했습니다.

이번 절에서는 마이크로서비스 환경에서 gRPC를 NestJS에 적용하는 방법을 살펴봅니다.

마이크로서비스 간 통신은 아키텍처의 핵심 요소이며, 선택하는 통신 프로토콜은 시스템의 성능과 효율성에 큰 영향을 미칩니다.

HTTP/1.1 기반의 RESTful API도 많은 서비스에 충분하지만, 스키마 기반 이진 메시지와 다중 스트림이 필요한 서비스 간 경계에서는 다른 선택지를 함께 검토할 수 있습니다.

gRPC는 이러한 요구사항을 충족시키기 위해 설계된 현대적인 RPC(Remote Procedure Call) 프레임워크입니다.


gRPC란 무엇인가?

gRPC는 Google에서 개발한 오픈소스 고성능 RPC(Remote Procedure Call) 프레임워크입니다.

HTTP/2 프로토콜을 기반으로 하며, 프로토콜 버퍼(Protocol Buffers)를 인터페이스 정의 언어(IDL)로 사용하여 서비스 인터페이스를 정의합니다.

gRPC의 주요 특징
  • HTTP/2 기반: HTTP/2는 다음과 같은 이점을 제공합니다.
    • 멀티플렉싱(Multiplexing): 단일 TCP 연결에서 여러 요청/응답 스트림을 동시에 처리할 수 있어, 네트워크 효율성이 높습니다.
    • 헤더 압축(Header Compression): HTTP 헤더를 효율적으로 압축하여 대역폭 사용량을 줄입니다.
  • 프로토콜 버퍼(Protocol Buffers)
    • 언어 중립적이고 플랫폼 중립적인 직렬화 메커니즘입니다.
    • 데이터를 이진 형태로 직렬화하므로 많은 서비스 간 workload에서 JSON보다 payload와 처리 비용을 줄일 수 있지만, 실제 차이는 schema와 데이터 및 구현으로 측정해야 합니다.
    • .proto 파일을 사용하여 서비스 인터페이스와 메시지 구조를 정의하고, 다양한 언어로 클라이언트 및 서버 스텁 코드를 자동으로 생성할 수 있습니다.
  • 스트리밍(Streaming): 단항(Unary) 호출 외에도 클라이언트 스트리밍, 서버 스트리밍, 양방향 스트리밍을 지원하여 실시간 통신 및 대용량 데이터 전송에 효율적입니다.
  • 타입 시스템: .proto는 클라이언트-서버 사이의 명확한 schema 계약을 제공합니다. Code generation을 도입하면 생성 타입으로 컴파일 시점 검사를 강화할 수 있지만, 이 절처럼 proto를 동적으로 로드하고 TypeScript interface를 직접 작성하는 방식에서는 별도 계약 검증이 필요합니다.
  • 언어 다양성: Go, Java, Python, Node.js, C++ 등 다양한 프로그래밍 언어를 지원하여 마이크로서비스 환경에서 폴리글랏(Polyglot) 아키텍처 구현에 용이합니다.
gRPC가 RESTful API보다 유리한 경우
  • 서비스 간 지연 시간과 처리량 개선이 목표이고 실제 workload로 이점을 검증할 수 있는 경우
  • 대용량 데이터 스트리밍이 필요한 경우
  • 폴리글랏(Polyglot) 마이크로서비스 아키텍처를 구축하는 경우
  • 명확한 인터페이스 정의를 통해 팀 간 협업을 강화하려는 경우

gRPC의 장점은 .proto 계약이 서버 구현, 클라이언트 프록시, 런타임 호출까지 같은 기준으로 이어질 때 드러납니다.

다음 다이어그램은 외부 HTTP 요청이 공유 proto 계약을 따라 Users 서비스의 unary 응답으로 돌아오는 흐름과 이름·경로·endpoint 경계를 정리합니다.

공유 proto 계약이 Orders의 HTTP 컨트롤러와 ClientGrpc 프록시, HTTP/2 전송, Users의 GrpcMethod 핸들러를 연결하고 unary 응답과 오류가 HTTP 의미로 돌아오는 흐름

Nest · gRPC Unary Contract

같은 .proto 계약을 서로 다른 런타임 경계에 연결한다

파일 경로나 주소 문자열을 복사하는 것이 핵심은 아닙니다. 양쪽이 같은 package·service·rpc·message 계약을 해석하고, 클라이언트 dial target이 서버 bind endpoint에 실제로 도달해야 합니다.

Unary request–response

외부 HTTP 요청에서 Users 응답까지

  1. HTTP client

    GET /orders/1/user로 Orders의 HTTP 계약을 호출합니다. 일반 브라우저에는 API Gateway나 gRPC-Web 같은 경계가 필요합니다.

  2. OrdersController

    orderId를 검증하고 Users 호출을 시작합니다. 원격 status와 timeout은 여기서 적절한 HTTP 상태로 바꿉니다.

  3. ClientGrpc proxy

    getService에 타입 UserService와 서비스 이름 'UserService'를 지정해 프록시를 만들고, getUserById({ id })가 unary Observable<User>을 반환합니다.

  4. HTTP/2 + Protocol Buffers

    UserByIdRequest가 이진 message로 직렬화되어 도달 가능한 Users endpoint로 전송됩니다.

  5. Users @GrpcMethod()

    @GrpcMethod에 서비스 'UserService'와 메서드 'GetUserById'를 지정해 같은 rpc를 처리하고, User를 반환하거나 명시적인 gRPC status를 던집니다.

  6. Unary response → HTTP

    lastValueFrom()으로 하나의 응답을 기다린 뒤 성공 본문 또는 NOT_FOUND·UNAVAILABLE 같은 실패를 HTTP 의미로 변환합니다.

proto

package·service·rpc를 공유한다

proto의 packageusers와 양쪽 options.package'users'가 일치해야 합니다. service 이름 UserServicegetService@GrpcMethod에 지정한 같은 서비스 이름과 연결됩니다. proto의 GetUserById는 서버 decorator와 맞추되 client proxy에서는 lower camel-case getUserById를 호출합니다.

local wiring

DI token은 proto 이름이 아니다

ClientsModulename: 'USERS_SERVICE'@Inject('USERS_SERVICE')와만 맞추는 로컬 Nest 이름이며 proto의 UserService와 독립입니다.

filesystem

경로 문자열보다 같은 계약인지 본다

각 프로세스는 같은 논리 users.proto 계약을 읽어야 합니다. 배포 디렉터리가 다르면 양쪽 protoPath 문자열은 달라도 됩니다.

network

bind와 dial은 도달 가능해야 한다

서버는 bind endpoint에서 듣고 클라이언트는 dial target으로 연결합니다. 주소 문자열이 같을 필요는 없지만 실제 서비스와 포트까지 네트워크로 도달해야 합니다.

Internal RPC

서비스 간 unary 호출

명확한 schema, 이진 message, HTTP/2 multiplexing이 유리할 수 있습니다. 실제 지연과 크기는 workload로 측정합니다.

Browser boundary

일반 브라우저에는 변환 계층이 필요하다

외부 HTTP API를 유지하려면 Orders 같은 Gateway가 status와 payload를 변환하거나 gRPC-Web 계층을 둡니다.

추적 순서: HTTP 진입 → Orders controller → ClientGrpc proxy → HTTP/2 transport → Users handler → unary response. 같은 trace ID와 rpc 이름을 남기면 계약 불일치와 원격 장애를 구분하기 쉽습니다.


NestJS에서 gRPC 설정 및 구현

gRPC 서비스 간 통신은 proto 계약, 채널 설정, 메서드 타입, deadline, 오류 상태 매핑 기준으로 읽습니다.

NestJS는 @nestjs/microservices 패키지를 통해 gRPC 전송 계층을 지원합니다.

시나리오: 이전 절과 동일하게 Users 서비스와 Orders 서비스가 있으며, Orders 서비스가 gRPC를 통해 Users 서비스의 사용자 정보를 요청합니다.

Protobuf 파일 정의

가장 먼저 gRPC 서비스의 인터페이스와 메시지 구조를 정의하는 .proto 파일을 생성합니다.

이 파일은 클라이언트와 서버 간의 계약서 역할을 합니다.

// proto/users.proto (프로젝트 루트 또는 shared/proto 폴더에 생성)

syntax = "proto3"; // Protocol Buffers 3 문법 사용

package users; // 패키지 이름

// UserService 정의
service UserService {
  // GetUserById RPC 메서드 정의
  // UserByIdRequest 메시지를 받아 User 메시지를 반환
  rpc GetUserById (UserByIdRequest) returns (User);
  // CreateUser RPC 메서드 정의
  // CreateUserRequest 메시지를 받아 User 메시지를 반환
  rpc CreateUser (CreateUserRequest) returns (User);
}

// UserByIdRequest 메시지 정의
message UserByIdRequest {
  int32 id = 1; // 필드 번호 1
}

// CreateUserRequest 메시지 정의
message CreateUserRequest {
  string name = 1;
  string email = 2;
}

// User 메시지 정의 (응답 및 반환 타입)
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
  • syntax = "proto3";: Protocol Buffers 3 문법을 사용합니다.
  • package users;: 네임스페이스를 정의하여 메시지 이름 충돌을 방지합니다.
  • service UserService { ... }: gRPC 서비스와 그 안에 포함될 RPC 메서드를 정의합니다.
  • rpc MethodName (RequestMessage) returns (ResponseMessage);: RPC 메서드를 정의합니다.
  • message MessageName { ... }: 데이터 구조를 정의합니다. 각 필드는 타입, 이름, 그리고 고유한 필드 번호를 가집니다.

필드 번호는 wire contract의 핵심이므로 삭제한 번호를 다른 의미로 재사용하지 않고 reserved로 남겨야 합니다. 아래 다이어그램은 schema 변경과 dynamic loading/codegen 선택, deadline·재시도·status·trace 운영 계약을 함께 정리합니다.

users proto의 package, service, rpc, message tag 계약과 schema 변경 규칙, dynamic loading과 code generation 차이, deadline, 재시도, status 매핑, trace 운영 기준

Proto Evolution · Failure Budget

field tag를 보존하고 실패에는 시간 예산을 둔다

.proto는 wire contract입니다. TypeScript 타입 생성 여부와 별개로 tag·type·service 경로를 호환되게 진화시키고, 모든 원격 호출에는 deadline·status·trace·재시도 조건을 명시합니다.

Source contract

users.proto의 wire identity

syntax = "proto3";
package users;

service UserService {
  rpc GetUserById(
    UserByIdRequest
  ) returns (User);
}

message UserByIdRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

message field의 wire identity는 이름보다 tag 번호에 가깝습니다. service와 rpc 이름은 호출 경로를 구성하므로 변경 시 양쪽 배포의 호환 계획이 필요합니다.

add · rename

새 tag를 쓰고 이름 변경의 표면을 확인한다

필드 추가는 새 고유 tag를 사용합니다. 이전 reader는 unknown field를 건너뛰고 새 reader는 누락된 scalar 기본값 또는 명시적 optional presence를 처리합니다. 이름 변경은 같은 tag와 호환 type이면 binary wire를 유지할 수 있지만 생성 API와 JSON/text 소비자는 달라질 수 있습니다.

delete · tag · type

삭제한 이름과 번호를 재사용하지 않는다

삭제한 이름과 tag는 reserved로 남깁니다. tag를 다시 번호 매기거나 다른 의미에 재사용하지 않고, type 변경은 wire type과 도메인 의미 모두에서 breaking change로 검토합니다.

path · loading

service 경로와 타입 산출물을 함께 버전 관리한다

service·rpc 변경은 호출 경로를 바꾸므로 old/new method를 겹쳐 배포하거나 명시적 버전 경계를 두고 decorator·proxy·호환성 테스트를 갱신합니다. @grpc/proto-loader는 런타임 계약을 읽을 뿐 수기 interface를 검증하지 않으므로 codegen에는 별도 생성·검증 단계가 필요합니다.

deadline

원격 호출에 유한한 시간 예산을 둔다

클라이언트가 유한한 deadline을 설정하고 상위 HTTP budget 안에서 전파합니다. 초과는 DEADLINE_EXCEEDED로 구분하고 attempt·latency와 함께 관측합니다.

retry

일시 오류와 멱등성을 함께 확인한다

UNAVAILABLE 같은 일시 오류만 deadline 안에서 제한된 횟수와 backoff·jitter로 재시도합니다. 멱등 rpc 또는 idempotency key가 있는 작업만 후보이며 CreateUser를 조건 없이 반복하지 않습니다. circuit breaker는 반복 실패에 대한 별도 정책이고, fallback은 도메인상 허용되는 값에 degraded 신호를 붙일 때만 사용합니다.

local mapping · trace

Gateway의 HTTP 매핑과 추적을 명시한다

이 예시의 Gateway는 NOT_FOUND404, DEADLINE_EXCEEDED504, UNAVAILABLE503, UNIMPLEMENTED502로 변환하지만 이는 보편 표준이 아닌 로컬 정책입니다. UNIMPLEMENTED는 method 부재·미구현·미지원 가능성이므로 proto 경로와 배포 버전을 확인합니다. HTTP request부터 client proxy와 handler까지 같은 trace ID에 package/service/rpc, status, deadline, attempt, latency를 남깁니다.

배포 전 확인: tag 재사용 없음 → dynamic loader 또는 codegen 산출물 검증 → old/new 호환성 테스트 → deadline·retry budget → status mapping → 동일 trace로 장애 재현.

gRPC 사용자 서비스 구축

단계 1: 필요한 패키지 설치

users-service 프로젝트에서 설치합니다.

npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
  • @grpc/grpc-js: gRPC Node.js 핵심 라이브러리입니다.
  • @grpc/proto-loader: .proto 파일을 로드하고 JavaScript 객체로 변환하는 유틸리티입니다.
단계 2: main.ts 파일 수정 (gRPC 마이크로서비스 서버 설정)
users-service/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path'; // path 모듈 임포트

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.GRPC, // gRPC 전송 방식 사용
    options: {
      package: 'users', // .proto 파일에 정의된 패키지 이름
      protoPath: join(__dirname, '../../proto/users.proto'), // .proto 파일 경로
      url: 'localhost:50051', // gRPC 서버가 리스닝할 주소와 포트
      loader: {
        keepCase: true, // 필드 이름을 스네이크 케이스로 유지 (선택 사항)
        longs: String, // 64비트 정수를 문자열로 처리 (선택 사항, 데이터 정밀도 이슈 방지)
        enums: String, // enum 값을 문자열로 처리 (선택 사항)
        defaults: true, // 기본값 포함 (선택 사항)
        oneofs: true, // oneof 필드 처리 (선택 사항)
      },
    },
  });
  await app.listen();
  console.log('Users gRPC Microservice is listening on localhost:50051');
}
bootstrap();
  • transport: Transport.GRPC: gRPC 전송 방식을 사용합니다.
  • package: .proto 파일의 package 이름과 일치해야 합니다.
  • protoPath: 실행 시점의 __dirname을 기준으로 .proto 파일을 찾습니다. 빌드 산출물의 디렉터리 구조에 맞춰 경로를 조정하고, 배포 시에도 같은 계약 파일이 그 위치에 복사되도록 설정해야 합니다.
  • url: gRPC 서버가 바인딩될 주소입니다.
  • loader: proto-loader의 추가 옵션으로, 데이터 타입 변환 및 필드명 처리 방식을 설정할 수 있습니다.
단계 3: 사용자 마이크로서비스 컨트롤러 (gRPC 핸들러) 구현

@GrpcMethod() 데코레이터를 사용하여 gRPC 메서드를 구현합니다.

users-service/src/users/users.controller.ts (수정)
import { Controller } from '@nestjs/common';
import { status } from '@grpc/grpc-js';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { UsersService } from './users.service';

interface User { // proto 파일과 일치하도록 타입 정의 (필요시)
  id: number;
  name: string;
  email: string;
}

@Controller()
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  // .proto 파일의 UserService.GetUserById 메서드에 매핑
  @GrpcMethod('UserService', 'GetUserById')
  getUserById(data: { id: number }): User { // data는 UserByIdRequest 메시지
    console.log(`Users gRPC Service: Received request for user ID: ${data.id}`);
    const user = this.usersService.findOne(data.id);
    if (!user) {
      throw new RpcException({
        code: status.NOT_FOUND,
        message: `User ${data.id} not found`,
      });
    }
    return user;
  }

  // .proto 파일의 UserService.CreateUser 메서드에 매핑
  @GrpcMethod('UserService', 'CreateUser')
  createUser(data: { name: string; email: string }): User { // data는 CreateUserRequest 메시지
    console.log(`Users gRPC Service: Received request to create user: ${JSON.stringify(data)}`);
    const newUser = this.usersService.create(data);
    return newUser;
  }
}
  • @GrpcMethod('ServiceName', 'MethodName'): 첫 번째 인자는 .proto 파일에 정의된 서비스 이름(UserService), 두 번째 인자는 해당 서비스 내의 메서드 이름(GetUserById)입니다.
  • 메서드의 인자는 .proto 파일에 정의된 요청 메시지의 타입과 일치합니다.
  • 반환 타입도 .proto 파일에 정의된 응답 메시지의 타입과 일치해야 합니다.
  • 사용자가 없으면 id: 0 같은 sentinel 성공 응답을 만들지 않고 status.NOT_FOUND를 담은 RpcException을 던집니다. 0도 유효한 int32 값이므로 부재 의미로 임의 사용하면 안 됩니다.
단계 4: 사용자 서비스 구현 (동일)

users-service/src/users/users.service.ts 파일은 이전과 동일하게 유지됩니다.

단계 5: UsersModuleAppModule 구성 (동일)

이전 TCP 예제와 동일하게 UsersModuleAppModule에 임포트합니다.

주문 서비스(gRPC 클라이언트) 구축

단계 1: 필요한 패키지 설치

orders-service 프로젝트에서 설치합니다.

npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader rxjs
단계 2: orders.module.ts 파일 수정 (gRPC 클라이언트 설정)

ClientsModule을 사용하여 gRPC 클라이언트를 등록합니다.

orders-service/src/orders/orders.module.ts (수정)
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { join } from 'path'; // path 모듈 임포트

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USERS_SERVICE', // 클라이언트 프록시 토큰
        transport: Transport.GRPC, // gRPC 전송 방식 사용
        options: {
          package: 'users', // 연결할 gRPC 서비스의 패키지 이름
          protoPath: join(__dirname, '../../proto/users.proto'), // .proto 파일 경로
          url: 'localhost:50051', // 연결할 gRPC 서버의 주소와 포트
          loader: {
            keepCase: true,
            longs: String,
            enums: String,
            defaults: true,
            oneofs: true,
          },
        },
      },
    ]),
  ],
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}
  • transport: Transport.GRPC: gRPC 클라이언트를 설정합니다.
  • package는 proto의 package users;와 일치해야 합니다. protoPath는 각 배포에서 같은 논리 계약을 가리키면 로컬 경로 문자열이 달라도 됩니다.
  • 서버의 url은 bind endpoint이고 클라이언트의 url은 dial target입니다. 컨테이너나 서비스 DNS를 사용하면 문자열은 달라도 되며, 클라이언트가 실제 서버와 포트에 도달해야 합니다.
  • name: 'USERS_SERVICE'@Inject('USERS_SERVICE')와 연결되는 Nest 내부 DI token입니다. proto의 UserService 이름과 독립입니다.
단계 3: 주문 서비스 컨트롤러 (gRPC 클라이언트 호출)

주입받은 ClientGrpc에서 getService()로 서비스 프록시를 얻어 gRPC 메서드를 호출합니다.

orders-service/src/orders/orders.controller.ts (수정)
import {
  BadGatewayException,
  BadRequestException,
  Body,
  Controller,
  GatewayTimeoutException,
  Get,
  HttpException,
  Inject,
  NotFoundException,
  OnModuleInit,
  Param,
  ParseIntPipe,
  Post,
  ServiceUnavailableException,
} from '@nestjs/common';
import { status, type CallOptions } from '@grpc/grpc-js';
import { ClientGrpc } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { lastValueFrom, Observable } from 'rxjs';

// 현재 예제는 proto를 동적으로 로드하므로 이 interface를 직접 맞춥니다.
interface UserService {
  getUserById(data: { id: number }, options?: CallOptions): Observable<User>;
  createUser(
    data: { name: string; email: string },
    options?: CallOptions,
  ): Observable<User>;
}

interface User {
  id: number;
  name: string;
  email: string;
}

function mapGrpcError(error: unknown): HttpException {
  const grpcError = error as {
    code?: number;
    details?: string;
    message?: string;
  };
  const details = grpcError.details ?? grpcError.message ?? 'gRPC request failed';

  switch (grpcError.code) {
    case status.INVALID_ARGUMENT:
      return new BadRequestException(details);
    case status.NOT_FOUND:
      return new NotFoundException(details);
    case status.DEADLINE_EXCEEDED:
      return new GatewayTimeoutException(details);
    case status.UNAVAILABLE:
      return new ServiceUnavailableException(details);
    case status.UNIMPLEMENTED:
      return new BadGatewayException(details);
    default:
      return new BadGatewayException(details);
  }
}

@Controller('orders')
export class OrdersController implements OnModuleInit {
  private userService!: UserService;

  constructor(
    private readonly ordersService: OrdersService,
    @Inject('USERS_SERVICE') private readonly client: ClientGrpc,
  ) {}

  onModuleInit(): void {
    this.userService = this.client.getService<UserService>('UserService');
  }

  @Get(':orderId/user')
  async getOrderUser(
    @Param('orderId', ParseIntPipe) orderId: number,
  ): Promise<User> {
    try {
      return await lastValueFrom(
        this.userService.getUserById(
          { id: orderId },
          { deadline: Date.now() + 1_500 },
        ),
      );
    } catch (error) {
      throw mapGrpcError(error);
    }
  }

  @Post('/create-order-with-user-grpc')
  async createOrderWithUserGrpc(
    @Body() orderDto: {
      userId?: number;
      item: string;
      userName?: string;
      userEmail?: string;
    },
  ): Promise<unknown> {
    const { userId, item, userName, userEmail } = orderDto;
    let user: User;

    try {
      if (userId !== undefined) {
        user = await lastValueFrom(
          this.userService.getUserById(
            { id: userId },
            { deadline: Date.now() + 1_500 },
          ),
        );
      } else if (userName && userEmail) {
        user = await lastValueFrom(
          this.userService.createUser(
            { name: userName, email: userEmail },
            { deadline: Date.now() + 1_500 },
          ),
        );
      } else {
        throw new BadRequestException(
          'Either userId or (userName and userEmail) must be provided.',
        );
      }
    } catch (error) {
      if (error instanceof HttpException) throw error;
      throw mapGrpcError(error);
    }

    const order = this.ordersService.createOrder(user.id, item);
    return { order, user };
  }
}
  • implements OnModuleInit: onModuleInit() 라이프사이클 훅을 사용하여 모듈 초기화 시점에 gRPC 서비스를 주입받습니다.
  • @Inject('USERS_SERVICE') private readonly client: ClientGrpc: orders.module.ts에서 등록한 로컬 DI token으로 클라이언트를 주입받습니다.
  • this.client.getService<UserService>('UserService'): proto에 정의된 UserService의 프록시 객체를 가져옵니다. Proto의 GetUserByIdCreateUser는 Nest client proxy에서 getUserByIdcreateUser처럼 lower camel-case 메서드로 노출됩니다.
  • unary 메서드는 Observable을 반환하므로 lastValueFrom()으로 하나의 완료 응답을 기다릴 수 있습니다. Server/client streaming은 값을 순차 소비해야 하므로 같은 방식으로 일반화하면 안 됩니다.
  • gRPC는 기본 deadline을 설정하지 않습니다. 예제는 각 호출의 두 번째 인자인 CallOptions에 유한한 deadline을 넣으며, 실제 값은 상위 HTTP 시간 예산과 부하 테스트 결과에 맞춰 정합니다.
  • 서버 handler가 명시적인 RpcException status를 반환하면 Orders 경계가 이를 HTTP 예외로 매핑합니다. 위 status→HTTP 코드는 이 Gateway가 선택한 계약 예시이며 보편적인 표준 매핑은 아닙니다. 오류 문자열이나 { message } 객체를 성공 응답으로 반환해 HTTP 200으로 숨기지 않습니다.
  • 설정된 retry policy가 없으면 gRPC는 일반 status 실패를 원하는 정책으로 재시도하지 않고, 서버 애플리케이션이 처리하기 전의 제한된 경우에만 transparent retry를 할 수 있습니다. 조회처럼 멱등인 rpc는 deadline 안의 제한된 재시도 후보가 될 수 있지만, createUser처럼 비멱등 작업은 idempotency key와 중복 방지 계약 없이 자동 재시도하지 않습니다.
  • circuit breaker와 fallback은 gRPC status나 retry가 자동으로 제공하는 동작이 아닙니다. 애플리케이션·gateway·resilience 계층에서 별도 정책으로 구현하고, fallback은 오래된 값도 허용되는 등 도메인 계약이 있을 때만 명시적인 degraded 신호와 함께 반환합니다.
  • UNIMPLEMENTED는 method가 서버에 없거나 구현·지원·활성화되지 않았음을 뜻합니다. proto의 package/service/rpc와 배포 버전을 먼저 확인하고, 일시적인 UNAVAILABLE과 구분해 명시적인 5xx로 매핑합니다.
단계 4: 주문 서비스 구현 (동일)

orders-service/src/orders/orders.service.ts 파일은 이전과 동일하게 유지됩니다.

단계 5: AppModuleOrdersModule 임포트 (동일)

이전과 동일하게 OrdersModuleAppModule에 임포트합니다.


통신 경로 실행 검증

Users Service 시작
  • cd users-service
  • npm run start:dev
  • 콘솔에 Users gRPC Microservice is listening on localhost:50051 메시지 확인
Orders Service 시작
  • cd orders-service
  • npm run start:dev
  • 콘솔에 Orders Service is listening on port 3000 (HTTP) 메시지 확인
API 테스트 (Postman 또는 cURL)
  • 사용자 정보 가져오기 (gRPC 통신)
    • GET http://localhost:3000/orders/1/user
    • 응답: {"id":1,"name":"Alice","email":"alice@example.com"}
    • Users Service 콘솔: Users gRPC Service: Received request for user ID: 1 메시지가 출력되는 것을 확인하여 gRPC 통신이 성공적으로 이루어졌음을 확인합니다.
  • 새로운 사용자 생성 및 주문 처리 (gRPC 통신)
    • POST http://localhost:3000/orders/create-order-with-user-grpc
    • Headers: Content-Type: application/json
    • Body
      {
        "userName": "Charlie",
        "userEmail": "charlie@example.com",
        "item": "Keyboard"
      }
    • 응답: {"order":{"id":1,"userId":3,"item":"Keyboard","createdAt":"2023-06-23T...Z"},"user":{"id":3,"name":"Charlie","email":"charlie@example.com"}}
    • Users Service 콘솔: Users gRPC Service: Received request to create user: {"name":"Charlie","email":"charlie@example.com"} 메시지가 출력되는 것을 확인합니다.

gRPC에서는 HTTP 엔드포인트의 성공 여부만 보지 말고 .proto 계약, package/service/rpc 이름, 클라이언트 프록시, Observable 완료와 오류 매핑까지 같은 trace에서 확인해야 합니다.


이 예시는 NestJS를 사용하여 gRPC 기반 마이크로서비스를 구축하고 서비스 간 통신을 구현하는 방법을 보여줍니다.

.proto 파일은 서비스 인터페이스를 명시하고, HTTP/2 기반 통신은 내부 서비스 간 요청/응답과 스트리밍 흐름을 구성하는 데 사용됩니다.

gRPC는 내부 서비스 간 통신에 적합하지만, 외부 클라이언트(브라우저)와 통신하려면 API Gateway나 gRPC-Web 같은 추가 계층이 필요할 수 있습니다.

설계와 운영 단계에서는 메시지 스키마의 호환성, deadline, retry/backoff와 멱등성, gRPC status와 HTTP status의 매핑, 분산 trace 전파를 하나의 계약으로 함께 검토해야 합니다.

이것으로 7장 마이크로서비스 아키텍처의 두 번째 절을 마칩니다.