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는 이상적인 선택입니다.