본문으로 건너뛰기
김신건의 로그

[NestJS] Microservices (Kafka, NATS, RabbitMQ, gRPC, TCP)

· 수정 · 📖 약 1분 · 433자/단어 #nestjs #microservices #messaging #kafka #rabbitmq #grpc
NestJS Microservices, ClientProxy, @MessagePattern, @EventPattern, NestJS Kafka, NestJS RabbitMQ, NestJS gRPC, NestJS NATS, NestJS 마이크로서비스

정의

NestJS Microservices 는 HTTP 대신 message broker (Kafka, RabbitMQ, NATS, Redis) 나 gRPC / TCP 로 통신하는 서비스를 같은 프로그래밍 모델로 작성하는 기능입니다. Controller 대신 Message pattern handler 를 정의하고, ClientProxy 로 다른 서비스에 요청합니다.

지원 Transport

  • TCP (기본): 단순 socket
  • Redis (pub/sub): 이벤트 유연
  • NATS: 가벼움, cloud native
  • MQTT: IoT
  • RabbitMQ (AMQP): 성숙, queue + routing
  • Kafka: 이벤트 스트리밍, 대규모
  • gRPC: HTTP/2 + Protocol Buffers, RPC
  • Custom: 자체 구현

Microservice 앱 생성

// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    transport: Transport.KAFKA,
    options: {
      client: {
        brokers: ['localhost:9092'],
        clientId: 'my-service',
      },
      consumer: {
        groupId: 'my-consumer-group',
      },
    },
  });
  await app.listen();
}
bootstrap();

Hybrid (HTTP + Microservice)

한 앱이 HTTP 도 서빙:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.connectMicroservice({
    transport: Transport.KAFKA,
    options: {...},
  });

  await app.startAllMicroservices();
  await app.listen(3000);   // HTTP
}

Message Pattern (Request-Response)

Controller 에서:

import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';

@Controller()
export class UsersController {
  @MessagePattern({cmd: 'get_user'})
  async getUser(@Payload() data: {id: number}) {
    return this.usersService.findOne(data.id);
  }

  @MessagePattern({cmd: 'create_user'})
  async createUser(@Payload() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}

Broker 에서 {cmd: 'get_user'} 패턴 매칭.

Event Pattern (Fire-and-forget)

응답 없음, side effect 만:

@EventPattern('user_created')
async handleUserCreated(@Payload() data: UserCreatedEvent) {
  await this.emailService.sendWelcome(data.email);
}

Client (다른 서비스 호출)

Module

import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USERS_SERVICE',
        transport: Transport.KAFKA,
        options: {
          client: {brokers: ['localhost:9092']},
          consumer: {groupId: 'orders-consumer'},
        },
      },
    ]),
  ],
})
export class OrdersModule {}

사용

import { ClientKafka, ClientProxy } from '@nestjs/microservices';

@Injectable()
export class OrdersService {
  constructor(
    @Inject('USERS_SERVICE') private usersClient: ClientKafka,
  ) {}

  async onModuleInit() {
    // Kafka 는 subscribe 명시적으로
    this.usersClient.subscribeToResponseOf('get_user');
    await this.usersClient.connect();
  }

  async getUser(id: number) {
    // Request-response (RxJS Observable 반환)
    return this.usersClient.send({cmd: 'get_user'}, {id}).toPromise();
  }

  async notifyUserCreated(user: User) {
    // Event (fire-and-forget)
    this.usersClient.emit('user_created', user);
  }
}

Kafka 심화

Producer 설정

{
  transport: Transport.KAFKA,
  options: {
    client: {
      brokers: ['localhost:9092'],
      clientId: 'producer-1',
    },
    producer: {
      allowAutoTopicCreation: true,
      transactionalId: 'my-txn',   // exactly-once
    },
    consumer: {
      groupId: 'my-group',
      allowAutoTopicCreation: true,
      sessionTimeout: 30000,
      heartbeatInterval: 3000,
    },
    subscribe: {
      fromBeginning: false,
    },
  },
}

커스텀 partition

this.kafka.emit('user_created', {
  key: user.id.toString(),   // partition key
  value: user,
  headers: {'x-source': 'nest'},
});

Exactly-once

Producer 트랜잭션 + consumer offset commit 관리. 자세한 것은 Kafka 문서.

RabbitMQ 심화

설정

{
  transport: Transport.RMQ,
  options: {
    urls: ['amqp://localhost:5672'],
    queue: 'user_queue',
    queueOptions: {
      durable: true,
    },
    noAck: false,        // manual ack
    prefetchCount: 10,   // per-worker
  },
}

Manual Ack

@MessagePattern('process_order')
async processOrder(@Payload() data: any, @Ctx() ctx: RmqContext) {
  const channel = ctx.getChannelRef();
  const originalMsg = ctx.getMessage();

  try {
    await this.processInternal(data);
    channel.ack(originalMsg);
  } catch (error) {
    channel.nack(originalMsg, false, false);   // DLQ 로 (false = requeue X)
  }
}

Dead Letter Exchange

queueOptions: {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx',
    'x-dead-letter-routing-key': 'user_dlq',
    'x-message-ttl': 60000,
  },
}

gRPC

Proto 파일

// users.proto
syntax = "proto3";
package users;

service UsersService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc StreamUsers (Empty) returns (stream User);
}

message GetUserRequest {
  int32 id = 1;
}

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

message Empty {}

Server

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

Controller

import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices';
import { Observable } from 'rxjs';

@Controller()
export class UsersController {
  @GrpcMethod('UsersService', 'GetUser')
  async getUser(data: {id: number}): Promise<User> {
    return this.usersService.findOne(data.id);
  }

  @GrpcStreamMethod('UsersService', 'StreamUsers')
  streamUsers(): Observable<User> {
    return this.usersService.stream();
  }
}

Client

import { ClientGrpc, GrpcMethod, Transport } from '@nestjs/microservices';

@Injectable()
export class OrdersService implements OnModuleInit {
  private usersService: UsersServiceClient;

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

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

  async getUser(id: number) {
    return firstValueFrom(this.usersService.getUser({id}));
  }
}

Message Serialization

기본 JSON. Custom serializer / deserializer:

class CustomSerializer implements Serializer {
  serialize(value: any): any {
    return {data: value, timestamp: Date.now()};
  }
}

Retry / DLQ

프레임워크 레벨 지원 제한적. Consumer 코드에서:

@MessagePattern('process')
async process(@Payload() data: any, @Ctx() ctx: RmqContext) {
  const retryCount = data.retry || 0;
  try {
    await this.handleMessage(data);
    ctx.getChannelRef().ack(ctx.getMessage());
  } catch (err) {
    if (retryCount < 3) {
      // republish with retry count
    } else {
      // DLQ
      ctx.getChannelRef().nack(ctx.getMessage(), false, false);
    }
  }
}

v11 신규

  • Status / Unwrap / On methods: transporter 상태
  • NATS queue per handler
  • NATS gracefull shutdown
  • DI container 로부터 microservice options

Interceptor / Guard / Pipe

같은 개념 적용:

@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
@MessagePattern('secure_op')
async secureOp(@Payload() data: any) { ... }

Guard 안:

canActivate(ctx: ExecutionContext): boolean {
  if (ctx.getType() === 'rpc') {
    const rpcCtx = ctx.switchToRpc();
    // ...
  }
}

함정

WARNING

Kafka subscribe 를 잊으면 응답 못 받음. subscribeToResponseOf('pattern') + connect().

CAUTION

Message ordering. Kafka 는 partition 안에서만 순서. RabbitMQ 는 queue 안에서 순서.

WARNING

Exactly-once vs at-least-once. 대부분 broker 는 at-least-once. Idempotent handler 필수.

IMPORTANT

Timeout 관리. Request-response 는 client 가 timeout 명시. 응답 안 오면 hang.

CAUTION

Serialization. 큰 payload 는 broker 부담. Reference (S3 URL) 만 전달하는 편이 나은 경우.

관련 위키

이 글의 용어 (9개)
[FastAPI] Background Tasksfastapi
정의 FastAPI 에서 응답 반환 후 실행하는 짧은 작업 은 , 장기/신뢰성 있는 작업 은 외부 task queue (Celery, Arq, RQ) 로 오프로드합니다. 두 카테…
[Framework] NestJSframeworks
정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
[NestJS] Config (Environment, Validation)frameworks
정의 NestJS Config 는 패키지로 환경변수를 관리합니다. 파일 로딩, TypeScript type-safe access, 검증 (Joi/Zod), 여러 환경 (dev/s…
[NestJS] Controllersframeworks
정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …
[NestJS] Deployment (Docker, Kubernetes, PM2)frameworks
정의 NestJS Deployment 는 프로덕션 환경에서 앱을 배포/운영하기 위한 절차입니다. Docker 이미지 빌드, Kubernetes 매니페스트, PM2 프로세스 관리,…
[NestJS] Exception Filtersframeworks
정의 NestJS Exception Filter 는 애플리케이션에서 발생한 예외를 catch 하여 클라이언트에게 응답 형식을 통일합니다. 데코레이터로 어떤 예외를 처리할지 지정합…
[NestJS] Modulesframeworks
정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
[NestJS] Providers & Dependency Injectionframeworks
정의 NestJS Provider 는 데코레이터가 붙은 클래스 (또는 값/factory) 로, DI container 에 등록되어 다른 클래스에 주입됩니다. Service, Re…
[NestJS] Testing (Jest, Supertest, e2e)frameworks
정의 NestJS Testing 은 Jest (기본) + Supertest 조합으로 unit test 와 e2e test 를 지원합니다. 의 로 module 을 격리된 형태로 만…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기