NestJS Microservices Transport

NestJS Microservices란?

NestJS의 @nestjs/microservices 패키지는 HTTP가 아닌 메시지 기반 통신으로 서비스 간 연결하는 프레임워크입니다. TCP, Redis, NATS, Kafka, gRPC, RabbitMQ 등 다양한 Transport Layer를 플러그인 방식으로 교체할 수 있으며, 동일한 데코레이터 패턴(@MessagePattern, @EventPattern)으로 통신합니다.

모놀리스에서 마이크로서비스로 전환할 때, NestJS Microservices는 코드 변경을 최소화하면서 서비스를 분리할 수 있는 강력한 방법입니다. 이 글에서는 Transport별 설정, 메시지 패턴, 하이브리드 앱, 에러 핸들링까지 심화 분석합니다.

TCP Transport: 기본 구조

가장 기본적인 TCP 기반 마이크로서비스입니다:

// main.ts — 마이크로서비스 서버
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        host: '0.0.0.0',
        port: 3001,
        retryAttempts: 5,
        retryDelay: 3000,
      },
    },
  );
  await app.listen();
  console.log('Microservice is listening on port 3001');
}
bootstrap();

@MessagePattern으로 요청-응답 패턴, @EventPattern으로 이벤트 기반 패턴을 구현합니다:

// user.controller.ts — 마이크로서비스 핸들러
@Controller()
export class UserController {

  constructor(private readonly userService: UserService) {}

  // 요청-응답: 클라이언트가 응답을 기다림
  @MessagePattern({ cmd: 'get_user' })
  async getUser(@Payload() data: { userId: number }) {
    return this.userService.findById(data.userId);
  }

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

  // 이벤트: fire-and-forget (응답 없음)
  @EventPattern('user_created')
  async handleUserCreated(@Payload() data: { userId: number; email: string }) {
    await this.emailService.sendWelcome(data.email);
    await this.analyticsService.trackSignup(data.userId);
  }

  @EventPattern('user_deleted')
  async handleUserDeleted(
    @Payload() data: { userId: number },
    @Ctx() context: TcpContext,
  ) {
    console.log('Pattern:', context.getPattern());
    await this.cleanupService.removeUserData(data.userId);
  }
}

클라이언트: 다른 서비스 호출

// order.module.ts — 클라이언트 등록
@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USER_SERVICE',
        transport: Transport.TCP,
        options: {
          host: 'user-service',   // K8s 서비스 이름
          port: 3001,
        },
      },
    ]),
  ],
  controllers: [OrderController],
  providers: [OrderService],
})
export class OrderModule {}

// order.service.ts — 클라이언트 사용
@Injectable()
export class OrderService {
  constructor(
    @Inject('USER_SERVICE') private readonly userClient: ClientProxy,
  ) {}

  async createOrder(dto: CreateOrderDto) {
    // 요청-응답: Observable → Promise
    const user = await firstValueFrom(
      this.userClient.send<UserDto>({ cmd: 'get_user' }, { userId: dto.userId }),
    );

    if (!user) throw new NotFoundException('User not found');

    const order = await this.orderRepo.save(new Order(dto, user));

    // 이벤트 발행: fire-and-forget
    this.userClient.emit('order_created', {
      orderId: order.id,
      userId: user.id,
      amount: order.totalAmount,
    });

    return order;
  }

  // 앱 시작 시 연결 초기화
  async onModuleInit() {
    await this.userClient.connect();
  }
}

Redis Transport: Pub/Sub 기반

// main.ts — Redis 마이크로서비스
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  AppModule,
  {
    transport: Transport.REDIS,
    options: {
      host: 'redis',
      port: 6379,
      password: process.env.REDIS_PASSWORD,
      retryAttempts: 10,
      retryDelay: 3000,
    },
  },
);

// 클라이언트 등록
ClientsModule.register([
  {
    name: 'NOTIFICATION_SERVICE',
    transport: Transport.REDIS,
    options: {
      host: 'redis',
      port: 6379,
      password: process.env.REDIS_PASSWORD,
    },
  },
])

Redis Transport는 내부적으로 Redis Pub/Sub을 사용합니다. @MessagePattern은 요청 채널과 응답 채널 두 개를 사용하고, @EventPattern은 발행 채널만 사용합니다.

gRPC Transport: 고성능 바이너리 통신

gRPC는 Protocol Buffers 기반 바이너리 직렬화로 높은 성능을 제공합니다:

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc FindOne (UserById) returns (User) {}
  rpc FindMany (UserFilter) returns (stream User) {}  // 서버 스트리밍
  rpc CreateUser (CreateUserRequest) returns (User) {}
}

message UserById {
  int32 id = 1;
}

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

message UserFilter {
  string role = 1;
  bool isActive = 2;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}
// main.ts — gRPC 서버
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  AppModule,
  {
    transport: Transport.GRPC,
    options: {
      package: 'user',
      protoPath: join(__dirname, 'proto/user.proto'),
      url: '0.0.0.0:5000',
      loader: {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
      },
    },
  },
);

// user.controller.ts — gRPC 핸들러
@Controller()
export class UserController {

  @GrpcMethod('UserService', 'FindOne')
  findOne(data: UserById): User {
    return this.userService.findById(data.id);
  }

  // 서버 스트리밍
  @GrpcStreamMethod('UserService', 'FindMany')
  findMany(data: UserFilter): Observable<User> {
    return from(this.userService.findByFilter(data)).pipe(
      mergeMap(users => from(users)),
    );
  }

  @GrpcMethod('UserService', 'CreateUser')
  async createUser(data: CreateUserRequest): Promise<User> {
    return this.userService.create(data);
  }
}

// 클라이언트 등록
ClientsModule.register([
  {
    name: 'USER_PACKAGE',
    transport: Transport.GRPC,
    options: {
      package: 'user',
      protoPath: join(__dirname, 'proto/user.proto'),
      url: 'user-service:5000',
    },
  },
])

// 클라이언트 사용
@Injectable()
export class OrderService implements OnModuleInit {
  private userService: UserServiceClient;

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

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

  async getUser(id: number): Promise<User> {
    return firstValueFrom(this.userService.findOne({ id }));
  }
}

하이브리드 앱: HTTP + Microservice 동시 실행

하나의 NestJS 앱에서 HTTP API와 마이크로서비스를 동시에 제공할 수 있습니다:

// main.ts — 하이브리드 앱
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // TCP 마이크로서비스 연결
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.TCP,
    options: { port: 3001 },
  });

  // Redis 마이크로서비스 연결
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.REDIS,
    options: { host: 'redis', port: 6379 },
  });

  // 모든 마이크로서비스 시작
  await app.startAllMicroservices();

  // HTTP 서버 시작
  await app.listen(3000);
  console.log('HTTP on 3000, TCP on 3001, Redis pub/sub active');
}

하이브리드 앱에서 같은 컨트롤러가 HTTP와 마이크로서비스 요청을 동시에 처리할 수 있습니다:

@Controller('users')
export class UserController {

  // HTTP GET /users/:id
  @Get(':id')
  async getUser(@Param('id') id: number) {
    return this.userService.findById(id);
  }

  // 마이크로서비스 메시지 패턴
  @MessagePattern({ cmd: 'get_user' })
  async getUserMsg(@Payload() data: { userId: number }) {
    return this.userService.findById(data.userId);
  }
}

에러 핸들링과 타임아웃

// 서버 측: RpcException 사용
@MessagePattern({ cmd: 'get_user' })
async getUser(@Payload() data: { userId: number }) {
  const user = await this.userService.findById(data.userId);
  if (!user) {
    throw new RpcException({
      status: 404,
      message: `User ${data.userId} not found`,
    });
  }
  return user;
}

// 글로벌 RPC 예외 필터
@Catch(RpcException)
export class RpcExceptionFilter implements ExceptionFilter {
  catch(exception: RpcException, host: ArgumentsHost) {
    const ctx = host.switchToRpc();
    const error = exception.getError();
    return throwError(() => error);
  }
}

// 클라이언트 측: 타임아웃 + 에러 핸들링
@Injectable()
export class OrderService {
  async getUser(userId: number): Promise<UserDto> {
    try {
      return await firstValueFrom(
        this.userClient
          .send<UserDto>({ cmd: 'get_user' }, { userId })
          .pipe(
            timeout(5000),              // 5초 타임아웃
            retry({ count: 2, delay: 1000 }), // 2회 재시도
            catchError(err => {
              this.logger.error('User service call failed', err);
              throw new ServiceUnavailableException('User service unavailable');
            }),
          ),
      );
    } catch (error) {
      if (error.status === 404) {
        throw new NotFoundException(error.message);
      }
      throw error;
    }
  }
}

비동기 클라이언트 등록

// ConfigService에서 동적으로 연결 정보 로드
ClientsModule.registerAsync([
  {
    name: 'USER_SERVICE',
    imports: [ConfigModule],
    useFactory: (config: ConfigService) => ({
      transport: Transport.TCP,
      options: {
        host: config.get('USER_SERVICE_HOST'),
        port: config.get('USER_SERVICE_PORT'),
      },
    }),
    inject: [ConfigService],
  },
  {
    name: 'NOTIFICATION_SERVICE',
    imports: [ConfigModule],
    useFactory: (config: ConfigService) => ({
      transport: Transport.REDIS,
      options: {
        host: config.get('REDIS_HOST'),
        port: config.get('REDIS_PORT'),
      },
    }),
    inject: [ConfigService],
  },
])

관련 글: NestJS Dynamic Module 설계에서 비동기 모듈 패턴을, NestJS DI Scope·Provider 심화에서 커스텀 프로바이더 등록을 함께 확인하세요.

마무리

NestJS Microservices는 Transport 교체만으로 TCP/Redis/gRPC/Kafka 간 전환이 가능한 유연한 마이크로서비스 프레임워크입니다. @MessagePattern으로 요청-응답, @EventPattern으로 이벤트 기반 통신을 구현하고, 하이브리드 앱으로 HTTP API와 동시 운영할 수 있습니다. 모놀리스를 점진적으로 분리하는 전략에 NestJS Microservices는 이상적인 선택입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux