2026년 NestJS 마이크로서비스: 아키텍처, gRPC, 면접 질문 완벽 가이드

NestJS 마이크로서비스 아키텍처와 gRPC 실전 가이드. 서비스 경계, 트랜스포트 레이어, 스트리밍 패턴, 2026년 면접 빈출 질문을 다룹니다.

NestJS 마이크로서비스 아키텍처와 gRPC 구성 가이드

NestJS 마이크로서비스는 2026년 현재 분산 Node.js 백엔드 구축의 표준 접근 방식으로 자리 잡았습니다. NestJS 12의 향상된 트랜스포터 레이어, 일급 gRPC 지원, RPC 호출용 예외 필터, 자동 트레이스 ID 전파를 통해 개발자 경험을 유지하면서 모놀리스를 적절히 경계가 구분된 서비스로 분할하는 데 필요한 모든 것을 제공합니다.

내부 서비스 간 gRPC vs REST

gRPC는 Protocol Buffers와 HTTP/2를 사용하여 JSON over REST 대비 최대 10배 빠른 직렬화와 네이티브 양방향 스트리밍을 제공합니다. 클러스터 내부의 서비스 간 통신에서 gRPC는 지연 시간을 줄이고 .proto 파일을 통해 엄격한 계약을 강제합니다. 브라우저와 서드파티가 사용하는 퍼블릭 API에는 REST가 더 적합한 선택입니다.

NestJS 마이크로서비스 트랜스포트 레이어 아키텍처

@nestjs/microservices 패키지는 HTTP 컨트롤러를 메시지 지향 핸들러로 대체합니다. URL 경로로 라우팅하는 대신 각 핸들러는 메시지 패턴(작업을 식별하는 문자열 또는 객체 키)에 응답합니다. 이 추상화를 통해 동일한 핸들러 로직이 코드 변경 없이 TCP, Redis, NATS, Kafka, RabbitMQ, gRPC에서 작동합니다.

NestJS 11에서는 모든 트랜스포터에 unwrap() 메서드가 도입되어 기본 클라이언트 인스턴스에 직접 접근할 수 있게 되었습니다. 이를 통해 연결 상태 확인, keepalive 간격 조정, NestJS 추상화에서 노출되지 않는 브로커별 기능 접근이라는 오랜 문제가 해결되었습니다. NestJS 12에서는 프리리퀘스트 훅이 추가되어 인증 확인이나 메트릭 수집과 같은 횡단 관심사를 위해 핸들러 호출 직전에 커스텀 로직을 실행할 수 있습니다.

NestJS 마이크로서비스를 구동하는 두 가지 통신 패턴:

  • 요청-응답(@MessagePattern): 클라이언트가 메시지를 보내고 응답을 기다립니다. 사용자 프로필 조회나 토큰 검증과 같은 동기 쿼리에 적합합니다.
  • 이벤트 기반(@EventPattern): 클라이언트가 이벤트를 발행하고 계속 진행합니다. 응답을 기대하지 않습니다. 감사 로그, 알림, 다운스트림 워크플로우 트리거에 이상적입니다.
orders.controller.tstypescript
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Controller()
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  // Request-response: caller waits for the created order
  @MessagePattern('order.create')
  async createOrder(@Payload() data: CreateOrderDto) {
    return this.ordersService.create(data);
  }

  // Event-based: fire and forget, no response returned
  @EventPattern('order.shipped')
  async handleOrderShipped(@Payload() data: { orderId: string }) {
    await this.ordersService.markAsShipped(data.orderId);
  }
}

@MessagePattern 핸들러는 반환 값을 자동으로 직렬화하여 트랜스포트를 통해 전송합니다. @EventPattern 핸들러는 아무것도 반환하지 않습니다: NestJS는 반환 값을 폐기합니다.

NestJS 12에서 EventPattern 폐기 예정

NestJS 12부터 TCP, Redis, NATS, MQTT, RabbitMQ 트랜스포트에서 @EventPattern 핸들러가 ClientProxy#send()에 응답할 때 폐기 경고가 로깅됩니다. 이 동작은 다음 메이저 버전에서 변경되어 Kafka의 기존 접근 방식과 일치하게 됩니다. 이벤트 핸들러를 진정한 파이어 앤 포겟 시맨틱으로 마이그레이션하거나 요청-응답 플로우에는 @MessagePattern으로 전환하십시오.

NestJS 마이크로서비스 트랜스포트로 gRPC 구성

NestJS에서 gRPC 통합은 서비스 계약을 정의하는 .proto 파일에서 시작합니다. Protocol Buffers는 와이어 레벨에서 타입 안전성을 강제합니다: 클라이언트와 서버 모두 네트워크를 통해 단일 바이트가 전송되기 전에 모든 메시지의 정확한 형태에 동의해야 합니다.

proto/users.protoprotobuf
syntax = "proto3";

package users;

service UsersService {
  rpc FindOne (UserById) returns (User);
  rpc FindMany (UserFilter) returns (stream User);
}

message UserById {
  string id = 1;
}

message UserFilter {
  string role = 1;
  int32 limit = 2;
}

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

서버 측에서 NestJS 애플리케이션은 proto 파일을 지정하고 네트워크 주소를 바인딩하여 gRPC 마이크로서비스를 부트스트랩합니다:

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.GRPC,
      options: {
        package: 'users',
        protoPath: join(__dirname, 'proto/users.proto'),
        url: '0.0.0.0:5000',
      },
    },
  );
  await app.listen();
}
bootstrap();

컨트롤러는 @MessagePattern 대신 @GrpcMethod를 사용합니다. 이 데코레이터는 proto 정의에 직접 매핑되는 서비스 이름과 RPC 메서드 이름을 인수로 받습니다:

users.controller.tstypescript
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { UsersService } from './users.service';

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

  @GrpcMethod('UsersService', 'FindOne')
  async findOne(data: { id: string }) {
    return this.usersService.findById(data.id);
  }
}

NestJS는 proto 로딩, 직렬화, HTTP/2 연결 관리를 자동으로 처리합니다. 컨트롤러 메서드는 일반 TypeScript 객체를 받으며 수동 proto 디코딩이 필요 없습니다.

NestJS에서 gRPC 스트리밍 패턴

서버 스트리밍은 단일 요청이 시간에 걸쳐 여러 결과를 생성하는 시나리오에 적합합니다: 실시간 가격 피드, 페이지네이션된 데이터베이스 커서, 장기 실행 작업 중 진행 상황 업데이트 등. 핸들러는 RxJS Observable을 반환하고 NestJS는 발행된 각 값을 개별 gRPC 메시지로 클라이언트에 스트리밍합니다:

users.controller.ts (server streaming)typescript
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { GrpcMethod } from '@nestjs/microservices';

@GrpcMethod('UsersService', 'FindMany')
findMany(data: { role: string; limit: number }): Observable<any> {
  // Stream users matching the filter one by one
  const users$ = from(this.usersService.findByRole(data.role, data.limit));
  return users$.pipe(
    map((user) => ({
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    })),
  );
}

양방향 스트리밍은 @GrpcStreamCall()을 사용합니다. 클라이언트와 서버가 독립적인 스트림을 보내므로 채팅 시스템, 협업 편집, 다중화된 텔레메트리 수집에 유용합니다. 핸들러는 grpc.ServerDuplexStream 인스턴스를 받고 읽기/쓰기 사이클을 수동으로 관리합니다.

NestJS 12에서는 백프레셔 처리가 추가되었습니다: 응답 쓰기를 기다리는 동안 요청 스트림이 일시 중지됩니다. 이를 통해 느린 소비자가 빠른 생산자를 따라잡지 못할 때 버퍼 오버플로우를 방지합니다.

스트리밍 오류 처리

gRPC 스트림 내에서 Observable이 크래시하면 전체 호출이 CANCELLED 상태로 종료됩니다. 스트림 로직을 catchError 연산자로 래핑하고 gRPC에 적합한 오류 메타데이터를 발행하십시오. 처리되지 않은 스트림 오류는 프로덕션 NestJS gRPC 서비스에서 연결 끊김의 가장 흔한 원인입니다.

NestJS 12의 gRPC 예외 필터

NestJS 12에서는 익숙한 HTTP 예외 필터 패턴을 반영하는 gRPC 전용 예외 처리가 도입되었습니다. 상태별 예외 클래스를 통해 핸들러가 일반 실패 대신 gRPC 네이티브 오류 코드를 반환할 수 있습니다:

grpc-exception.filter.tstypescript
import { Catch, RpcExceptionFilter, ArgumentsHost } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { RpcException } from '@nestjs/microservices';
import { status } from '@grpc/grpc-js';

@Catch(RpcException)
export class GrpcExceptionFilter implements RpcExceptionFilter<RpcException> {
  catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
    const error = exception.getError() as { code?: number; message?: string };
    return throwError(() => ({
      code: error.code ?? status.INTERNAL,
      message: error.message ?? 'Internal error',
    }));
  }
}

이 필터를 전역 또는 컨트롤러별로 적용하면 모든 gRPC 엔드포인트에서 일관된 오류 응답을 보장할 수 있습니다. 기존 RpcException 클래스와 결합하면 HTTP와 gRPC 트랜스포트 간 오류 처리 동등성이 실현됩니다.

하이브리드 애플리케이션: 동일 서비스에서 HTTP와 gRPC

많은 실제 NestJS 서비스는 퍼블릭 REST API와 내부 gRPC 엔드포인트가 모두 필요합니다. 하이브리드 애플리케이션 패턴은 여러 트랜스포트를 단일 NestJS 인스턴스에 연결합니다:

main.ts (hybrid application)typescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  // HTTP server on port 3000
  const app = await NestFactory.create(AppModule);

  // gRPC microservice on port 5000
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.GRPC,
    options: {
      package: 'users',
      protoPath: join(__dirname, 'proto/users.proto'),
      url: '0.0.0.0:5000',
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}
bootstrap();

이 접근 방식은 단일 바운디드 컨텍스트가 외부 및 내부 인터페이스를 모두 필요로 할 때 별도의 서비스를 배포할 필요성을 없앱니다. 동일한 의존성 주입 컨테이너, 가드, 인터셉터, 파이프가 두 트랜스포트에 모두 적용되어 중복을 줄이고 동작 일관성을 유지합니다.

Node.js / NestJS 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

NestJS 12의 Kafka 정규식 패턴

NestJS 12에서는 @MessagePattern()과 @EventPattern()이 Kafka 토픽에 대한 정규식을 받을 수 있습니다. 단일 핸들러로 여러 토픽에 동적으로 구독할 수 있습니다:

multi-topic.controller.tstypescript
import { Controller } from '@nestjs/common';
import { EventPattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices';

@Controller()
export class AnalyticsController {
  // Matches analytics.pageview, analytics.click, analytics.purchase, etc.
  @EventPattern(/^analytics..*/)
  handleAnalyticsEvent(
    @Payload() data: Record<string, unknown>,
    @Ctx() context: KafkaContext,
  ) {
    const topic = context.getTopic();
    console.log(`Received event on ${topic}`, data);
  }
}

이를 통해 여러 토픽이 동일한 핸들러 로직을 공유할 때 보일러플레이트가 줄어듭니다. NestJS 12의 패턴 키 검증은 256자 최대 길이를 강제합니다: 더 긴 패턴은 조용히 실패하고 핸들러와 매칭되지 않습니다.

NestJS 12의 NATS v3 마이그레이션

NestJS 12에서는 NATS 클라이언트가 공식 @nats-io/transport-node 패키지로 대체되었습니다. 이것은 브레이킹 체인지입니다: 디시리얼라이저가 이제 원시 페이로드 대신 전체 메시지 객체를 받습니다.

typescript
// Before (NestJS 11)
deserialize(value: any): any {
  return JSON.parse(value);
}

// After (NestJS 12): read payload via msg.json()
deserialize(msg: any): any {
  return msg.json();
}

NestJS 12로 마이그레이션할 때 모든 커스텀 NATS 디시리얼라이저를 업데이트하십시오. 새 클라이언트는 향상된 성능과 공식 NATS 에코시스템과의 정렬을 제공합니다.

서비스 경계와 NestJS를 활용한 도메인 주도 설계

명확한 경계 없이 모놀리스를 마이크로서비스로 분할하면 분산 모놀리스가 만들어집니다. 이는 두 아키텍처의 단점을 모두 가집니다. 각 NestJS 마이크로서비스는 단일 바운디드 컨텍스트를 소유해야 합니다: 자체 데이터베이스, 자체 도메인 모델, 자체 배포 라이프사이클을 갖습니다.

NestJS에서 서비스 경계를 설정하기 위한 실용적인 가이드라인:

  • 애그리게이트 루트당 하나의 NestJS 모듈. Order와 OrderLine이 항상 함께 변경되면 같은 서비스에 속합니다. User와 Order가 독립적으로 변경되면 별도의 서비스로 분리할 가치가 있습니다.
  • 도메인 간 부수 효과에는 비동기 이벤트. 주문이 완료되면 Kafka 또는 RabbitMQ를 통해 order.created 이벤트를 발행합니다. 재고 서비스는 주문 서비스의 존재를 알지 못한 채 반응합니다.
  • 계약에는 공유 proto 패키지. .proto 파일을 공유 저장소에 저장합니다. 프로듀서와 컨슈머 모두 동일한 신뢰할 수 있는 소스에서 타입을 생성하므로 드리프트는 프로덕션 인시던트가 아닌 CI 실패가 됩니다.

NestJS 12의 DI 컨테이너에서 마이크로서비스 옵션을 통해 서비스 구성을 테스트할 수 있습니다. main.ts에서 브로커 URL을 하드코딩하는 대신 ConfigService를 주입하고 재빌드 없이 환경별로 연결 문자열을 교체합니다.

신뢰성 패턴: 타임아웃, 재시도, 서킷 브레이커

데드라인 없는 내부 gRPC 호출은 단일 느린 종속성을 시스템 전체 장애로 바꿉니다. 모든 ClientProxy.send() 호출에는 타임아웃이 포함되어야 합니다:

orders.service.tstypescript
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom, timeout, retry } from 'rxjs';

@Injectable()
export class OrdersService {
  private usersService: any;

  constructor(@Inject('USERS_PACKAGE') private client: ClientGrpc) {}

  onModuleInit() {
    this.usersService = this.client.getService('UsersService');
  }

  async getOrderWithUser(orderId: string, userId: string) {
    // 3-second deadline, 2 retries with exponential backoff
    const user = await firstValueFrom(
      this.usersService.findOne({ id: userId }).pipe(
        timeout(3000),
        retry({ count: 2, delay: (err, retryCount) => {
          const jitter = Math.random() * 100;
          return new Promise(r => setTimeout(r, 1000 * retryCount + jitter));
        }}),
      ),
    );
    return { orderId, user };
  }
}

재시도는 멱등 연산에만 적용해야 합니다. CreateOrder 호출을 재시도하면 중복 주문 위험이 있습니다. FindOne 쿼리는 부수 효과가 없으므로 재시도해도 안전합니다.

서킷 브레이커 기능에는 opossum 라이브러리가 NestJS와 잘 통합됩니다. N회 연속 실패 후 열리고 설정 가능한 리셋 타임아웃 후 닫히는 서킷 브레이커로 ClientGrpc 호출을 래핑합니다.

NestJS 12 트레이스 전파

NestJS 12는 NestJS 11에서 도입된 Kafka, RabbitMQ, gRPC 트랜스포트 간 자동 트레이스 ID 전파를 계속합니다. OpenTelemetry와 결합하면 커스텀 인터셉터 없이 완전한 분산 트레이싱이 가능합니다.

NestJS 마이크로서비스 면접 질문

백엔드 포지션 기술 면접에서는 프레임워크별 패턴과 함께 마이크로서비스 지식이 점점 더 테스트됩니다. 이 질문들은 NestJS 중심 면접에서 가장 자주 묻는 개념을 다룹니다.

@MessagePattern과 @EventPattern의 차이점은 무엇입니까?

@MessagePattern은 요청-응답을 구현합니다: 클라이언트가 메시지를 보내고 응답을 받을 때까지 블록됩니다. @EventPattern은 파이어 앤 포겟을 구현합니다: 클라이언트가 이벤트를 발행하고 즉시 실행을 계속합니다. 내부적으로 @MessagePattern은 트랜스포트를 통해 핸들러의 결과를 반환하고 @EventPattern은 반환 값을 폐기합니다. 대부분의 트랜스포트에서 @EventPattern 핸들러로부터의 응답이 NestJS 12에서 폐기 예정임에 주의하십시오.

NestJS는 gRPC용 .proto 파일을 어떻게 로드하고 바인딩합니까?

NestJS는 @grpc/proto-loader 패키지를 사용하여 시작 시 .proto 파일을 파싱하고 서비스 디스크립터를 생성합니다. GrpcOptions 구성에서 proto 경로와 패키지 이름을 지정합니다. @GrpcMethod('ServiceName', 'MethodName')으로 어노테이션된 컨트롤러가 파싱된 디스크립터와 매칭됩니다. proto에 선언된 메서드에 대응하는 핸들러가 없으면 NestJS는 부트스트랩 시 에러를 발생시킵니다.

마이크로서비스 간에 gRPC가 REST를 대체해야 하는 경우는 언제입니까?

gRPC는 타입 안전성, 성능, 스트리밍이 중요한 내부 서비스 간 호출에 적합합니다. Protocol Buffers는 JSON보다 작은 페이로드와 빠른 직렬화를 생성합니다. HTTP/2 멀티플렉싱으로 연결 오버헤드가 줄어듭니다. REST는 인간 가독성과 광범위한 도구 지원이 원시 성능보다 중요한 퍼블릭 API, 브라우저 클라이언트, 서드파티 통합에 더 적합합니다.

하이브리드 애플리케이션 패턴은 어떻게 작동합니까?

NestFactory.create()가 HTTP 서버를 부팅합니다. app.connectMicroservice()를 호출하면 추가 트랜스포트(gRPC, Kafka, Redis 또는 지원되는 모든 트랜스포터)가 동일한 NestJS 애플리케이션 인스턴스에 연결됩니다. 모든 트랜스포트는 동일한 모듈 트리, DI 컨테이너, 미들웨어 파이프라인을 공유합니다. app.startAllMicroservices()가 연결된 모든 트랜스포터를 시작하고 app.listen()이 HTTP 레이어를 시작합니다.

캐스케이드 장애를 방지하는 신뢰성 패턴은 무엇입니까?

세 가지 패턴이 가장 중요합니다: 타임아웃(모든 RPC 호출에는 데드라인이 필요), 지터가 포함된 재시도(멱등 연산에만, 썬더링 허드를 피하기 위한 지수 백오프와 랜덤 지터), 서킷 브레이커(N회 실패 후 열림, 쿨다운 후 하프 오픈, 헬스 체크 통과 후 닫힘). NestJS의 RxJS 통합으로 타임아웃과 재시도가 파이프 연산자를 통해 조합 가능합니다.

NestJS 12에서 마이크로서비스에 대해 무엇이 변경되었습니까?

NestJS 12에서는 상태별 오류 처리를 위한 gRPC 예외 필터, 멀티 토픽 핸들러를 위한 Kafka 정규식 패턴, 새 클라이언트 패키지를 사용한 NATS v3 지원, 마이크로서비스 핸들러용 프리리퀘스트 훅, 개선된 gRPC 스트림 백프레셔가 추가되었습니다. @EventPattern 폐기 경고는 이벤트 핸들러가 send() 호출에 더 이상 응답하지 않는 향후 동작 변경을 알립니다.

NestJS 아키텍처 패턴을 더 깊이 연습하려면 NestJS 모듈과 의존성 주입 모듈이 마이크로서비스 구성의 기반이 되는 DI 기초를 다룹니다. 미들웨어와 인터셉터 모듈은 트랜스포트 간에 공유되는 횡단 관심사를 다룹니다. NestJS와 GraphQL 가이드는 타입이 있는 서비스 API를 위한 gRPC 대안을 다룹니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

소스

2026년 NestJS 마이크로서비스 핵심 포인트

  • NestJS 12의 트랜스포트 추상화로 핸들러 로직을 다시 작성하지 않고 TCP, gRPC, Kafka, NATS 간에 서비스를 전환 가능: @MessagePattern과 @EventPattern 데코레이터는 모든 트랜스포트에서 동일하게 작동
  • Protocol Buffers를 사용한 gRPC는 .proto 파일을 통해 서비스 간 엄격한 계약을 강제하여 프로덕션이 아닌 컴파일 타임에 통합 오류를 포착
  • 하이브리드 애플리케이션은 동일한 NestJS 인스턴스에서 HTTP와 gRPC를 결합하여 퍼블릭 및 내부 인터페이스용 별도 서비스 배포 필요성을 제거
  • RxJS Observable을 통한 서버 스트리밍으로 WebSocket 복잡성 없이 실시간 데이터 피드가 가능하며 NestJS 12에서는 느린 소비자를 위한 백프레셔 처리 추가
  • 모든 내부 RPC 호출에는 데드라인이 필요하고 재시도는 멱등 연산에만 적용되며 서킷 브레이커는 단일 느린 서비스가 전체 장애로 캐스케이드되는 것을 방지
  • 서비스 경계는 도메인 주도 설계를 따름: 서비스당 하나의 바운디드 컨텍스트, 도메인 간 통신에는 비동기 이벤트, 계약 강제에는 공유 proto 패키지

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Node.js / NestJS 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 10월 22일 업데이트

태그

#nestjs
#microservices
#grpc
#node.js
#typescript
#architecture

공유

관련 기사