마이크로서비스(Microservice) 아키텍처란?
마이크로서비스 아키텍처는 하나의 애플리케이션을 여러개의 독립적으로 배포 가능한 작은 서비스로 나누고, 이들 사이의 결합을 느슨하게 유지하는 소프트웨어 아키텍처 방식입니다.
기존의 모놀리식(Monolithic) 아키텍처와 비교했을 때, 서비스별 코드의 규모가 작고 개별적으로 배포할 수 있다는 점이 큰 장점입니다. 이러한 특성 덕분에 서비스 확장성과 유지보수에 용이하며, 최근에는 대규모 애플리케이션을 운영하는 많은 조직에서 마이크로서비스 아키텍처를 도입하는 것을 볼 수 있습니다.
이번 글에서는 NestJS에서 마이크로서비스 아키텍처를 구성하는 방법을 알아보고, 각 마이크로서비스에서 API 서버와 내부 서비스간 통신을 함께 운영하는 하이브리드 애플리케이션(Hybrid Application) 방식에 대해 살펴보려고 합니다.
NestJS 마이크로서비스
공식문서에서 NestJS의 마이크로서비스가 다양한 Transport 레이어의 프로토콜을 활용할 수 있는 애플리케이션이라고 소개하고있습니다. 일반적인 모놀리식 애플리케이션은 클라이언트-서버 간의 통신을 HTTP 기반의 요청-응답 방식으로 수행합니다. 반면, 마이크로서비스는 다양한 TCP 프로토콜(TCP, RabbitMQ, Kafka, gRPC 등)을 지원하여 분산 시스템을 운영할 수 있도록 합니다.
📌 마이크로서비스 구성하기 - 서버 측(Order Service)
1. 마이크로서비스 생성
`@nestjs/microservices` 패키지를 활용하면 NestJS 애플리케이션을 마이크로서비스 형태로 운영할 수 있습니다.
$ npm i --save @nestjs/microservices
`main.ts`의 bootstrap 함수에서 `NestApplication` 인스턴스가 아닌 `NestMicroservice` 인스턴스를 생성합니다.
// order-service
// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
await app.listen();
}
bootstrap();
2. 메시지 패턴 - 응답 핸들러
NestJS 마이크로서비스는 메시지 기반 통신과 이벤트 기반 통신을 모두 지원하지만, 이 글에서는 메시지 기반 통신만 다루겠습니다.
요청-응답 메시지 패턴은 리시버 서비스가 메시지를 수신했음이 보장되어야할 때 유용합니다.
컨트롤러(Controller) 클래스에서 API 핸들러를 정의하듯이 `@nestjs/microservices` 패키지의 `@MessagePattern` 데코레이터를 사용해 메시지 응답 핸들러를 정의할 수 있습니다.
// order-service
// order.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class OrderController {
// ...
@MessagePattern({ cmd: 'order' })
async getUserOrders(data: GetOrderDto): Promise<OrderResponseDto> {
// ...
return orderService.getUserOrders();
}
}
`order()`라는 메시지 핸들러는 `{ cmd: ‘order’ }` 패턴의 메시지가 수신되었을때 이를 처리합니다. 클라이언트측 서비스에서 전송한 데이터를 `data` 파라미터로 받을 수 있고, 메서드의 반환값이 응답값으로 전달됩니다.
📌 마이크로서비스 구성하기 - 클라이언트 측(User Service)
1. 다른 마이크로서비스와의 통신 모듈 설정
클라이언트 측 마이크로서비스는 `ClientProxy` 클래스를 활용해 다른 마이크로서비스에 메시지를 전송할 수 있습니다.
✅ 기본적인 방식 - `ClientsModule.register()`
`@Module` 데코레이터에서 `ClientsModule` 를 활용하여 통신할 다른 마이크로서비스 트랜스포터를 추가합니다.
`name` 은 injection token역할을 하며, 마이크로서비스와의 통신이 필요한 서비스에서 `ClientProxy` 인스턴스를 주입할 때 사용합니다.
💡 마이크로서비스 트랜스포터(Transporter)란?
- 마이크로서비스간 메시지를 주고받는 전송 계층 (Transport Layer)
- NestJS는 `Transport` enum을 제공함
- TCP 트랜스포터의 경우, 기본 host와 port는 `localhost:3000`으로 설정됨
// user-service
// order-service.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{ name: 'ORDER_SERVICE', transport: Transport.TCP },
]),
],
})
export class OrderServiceModule {}
`ClientsModule`은 NestJS의 내장 모듈이므로, `ClientsModule.register()`를 호출하면 NestJS가 자동으로 DI 컨테이너에 `ClientProxy` 인스턴스를 등록해줍니다.
✅ 동적 설정 - `ClientProxyFactory.create()`
`ConfigService` 와같은 다른 서비스에서 트랜스포터 설정값을 동적으로 가져와야 하는 경우 커스텀 프로바이더(Custom Provider)를 등록할 수 있습니다. `useFactory`를 사용해 `ClientProxy` 인스턴스를 동적으로 생성합니다.
// user-service
// order-service.module.ts
import { Module } from '@nestjs/common';
import { ClientProxyFactory } from '@nestjs/microservices';
import { ConfigService } from '..';
@Module({
providers: [
{
provide: 'ORDER_SERVICE',
useFactory: (configService: ConfigService) => {
const orderServiceOptions = configService.get('orderServiceOptions');
return ClientProxyFactory.create({
options: {
host: orderServiceOptions.host,
port: orderServiceOptions.port,
},
});
},
inject: [ConfigService],
}
],
exports: ['ORDER_SERVICE'],
})
export class OrderServiceModule {}
2. 메시지 패턴 - 요청 메시지 전송(핸들러 호출)
모듈에서 `ClientProxy` 인스턴스를 등록했다면, 이제 서비스에서 `ClientProxy`를 주입받아 마이크로서비스에 메시지를 전송할 수 있습니다. `send()` 메서드는 마이크로서비스의 특정 핸들러(`@MessagePattern`)를 호출하는 역할을 합니다.
- 첫 번째 인자로 `@MessagePattern` 데코레이터에 정의한 메시지 패턴(`{ cmd: ‘order’ }`)을 전달하고
- 두 번째 인자로 전송할 데이터(`user`)를 전달하면 됩니다.
// user-service
// order.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Injectable()
export class OrderService {
constructor(
@Inject('ORDER_SERVICE') private readonly client: ClientProxy
) {}
getUserOrders(user: User): Observable<Orders> {
return this.client.send({ cmd: 'order' }, user);
}
}
이제 `order()` 를 호출하면, `ORDER_SERVICE` 마이크로서비스의 `@MessagePattern({ cmd: ‘order’ })` 핸들러가 실행되고 `Observable`을 응답으로 반환하게 됩니다.
NestJS 하이브리드 애플리케이션
이제 각 마이크로서비스 애플리케이션이 외부에서는 HTTP 기반의 API 서버로 동작하면서, 동시에 마이크로서비스 간에는 TCP 기반의 내부 서버로 통신할 수 있도록 설정하는 방식을 알아보겠습니다.
이를 위해서는 NestJS의 하이브리드 애플리케이션 개념을 활용해야 합니다. 하이브리드 애플리케이션이란, 하나의 애플리케이션이 둘 이상의 소스로부터 요청을 처리할 수 있는 구조를 의미합니다.
- 예를 들어, HTTP 서버와 마이크로서비스 리스너(TCP, Redis, Kafka 등)를 결합하거나
- 여러 개의 마이크로서비스 리스너를 하나의 애플리케이션에서 실행할 수도 있습니다.
앞에서 마이크로서비스 애플리케이션을 생성할 때는 `createMicroservice()` 메서드를 사용했습니다. 이 방식은 하나의 독립적인 마이크로서비스 애플리케이션을 생성하는 역할을 합니다. 따라서 여러 개의 서버(HTTP + 마이크로서비스)를 동시에 실행할 수는 없습니다.
📌 HTTP 서버 + TCP 마이크로서비스 리스너
여러 개의 서버를 하나의 NestJS 애플리케이션에서 실행하려면, `connectMicroservice()` 메서드를 사용해야 합니다. 이를 통해 기존 `NestApplication` 인스턴스에 여러 개의 `NestMicroservice` 인스턴스를 추가할 수 있습니다.
// order-service
// main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
// HTTP API 서버 생성
const app = await NestFactory.create(AppModule);
// TCP 기반 마이크로서비스 추가
const microservice = app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
});
await app.startAllMicroservices(); // 마이크로서비스 실행
await app.listen(3001); // HTTP 서버 실행
}
bootstrap();
이제 NestJS 애플리케이션이 HTTP 서버와 TCP 마이크로서비스를 동시에 실행합니다.
📌 Controller 예시
예를 들어, 컨트롤러 클래스는 이런 식으로 API 엔드포인트와 메시지 핸들러를 함께 구성할 수 있습니다.
// order-service
// order.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class OrderController {
@ApiPost('order')
async order(@Body body: OrderDto) {
// ...
}
@MessagePattern({ cmd: 'order' })
async getUserOrders(data: GetOrderDto): Promise<OrderResponseDto> {
// ...
return orderService.getUserOrders();
}
}
📌 `createMicroservice()` vs `connectMicroservice()`
`createMicroservice()`와 `connectMicroservice()` 메서드에 대해 조금 더 자세히 알아보기 위해 NestJS의 일부 코드를 참고했습니다.
둘 다 NestJS에서 마이크로서비스를 생성하고 연결하는 역할을 하는 메서드입니다. 하지만 사용 목적에 조금 차이가 있어 필요에 따라 구분해서 사용해야합니다.
✅ `createMicroservice()`
// nest/packages/core/nest-factory.ts
public async createMicroservice<T extends object>(
moduleCls: IEntryNestModule,
options?: NestMicroserviceOptions & T,
): Promise<INestMicroservice> {
// ...
const container = new NestContainer(applicationConfig, options);
// ...
await this.initialize(
moduleCls,
container,
graphInspector,
applicationConfig,
options,
);
return this.createNestInstance<INestMicroservice>(
new NestMicroservice(
container,
options,
graphInspector,
applicationConfig,
),
);
}
- 새로운 `NestContainer` 생성
- 새로운 DI 컨테이너를 만들고 의존성 관리 준비를 합니다.
- 애플리케이션 초기화 설정 수행
- 마이크로서비스를 설정하고 필요한 의존성을 주입합니다.
- `NestMicroservice` 인스턴스 생성 및 반환
- 새로운 마이크로서비스 인스턴스를 생성하고 반환합니다.
새로운 `NestContainer`를 생성하여 다른 애플리케이션 분리된 독립적인 마이크로서비스 인스턴스를 생성합니다. 따라서 단독 실행되는 마이크로서비스 애플리케이션 만들 때 사용합니다.
✅ `connectMicroservice()`
// nest/packages/core/nest-application.ts
public connectMicroservice<T extends object>(
microserviceOptions: T,
hybridAppOptions: NestHybridApplicationOptions = {},
): INestMicroservice {
// ...
const instance = new NestMicroservice(
this.container,
microserviceOptions,
this.graphInspector,
applicationConfig,
);
instance.registerListeners();
instance.setIsInitialized(true);
instance.setIsInitHookCalled(true);
this.microservices.push(instance);
return instance;
}
- 마이크로서비스 인스턴스 생성
- 현재 애플리케이션(`this.container`)을 기반으로 새로운 마이크로서비스를 생성합니다.
- 마이크로서비스 활성화
- 마이크로서비스의 리스너를 등록하고 초기화 및 초기화 훅 호출 완료 상태로 설정합니다.
- 마이크로서비스 리스트에 추가 후 반환
- 현재 애플리케이션 새로운 마이크로서비스를 추가하고 해당 인스턴스를 반환합니다.
새로운 `NestContainer`를 만들지 않고 기존 애플리케이션 컨텍스트를 공유하는 마이크로서비스 인스턴스를 생성합니다. 그리고 기존 NestJS 애플리케이션에 마이크로서비스를 추가합니다. 따라서 HTTP 서버와 마이크로서비스 서버를 함께 실행하는 하이브리드 애플리케이션 만들 때 사용합니다.
📌 `startAllMicroservices()`
// nest/packages/core/nest-application.ts
public async startAllMicroservices(): Promise<this> {
this.assertNotInPreviewMode('startAllMicroservices');
await Promise.all(this.microservices.map(msvc => msvc.listen()));
return this;
}
NestJS 애플리케이션에 연결된 모든 마이크로서비스를 실행(`listen()`)합니다.
📌 하이브리드 애플리케이션 bootstrap 순서
NestJS 서버를 실행하는데 기본적으로는 HTTP 서버와 마이크로서비스 실행 순서는 큰 영향을 주지 않습니다. 그러나 마이크로서비스를 먼저 실행하고(`startAllMicroservices()`) HTTP 서버를 실행(`listen()`) 하는 순서가 권장됩니다.
`listen()` → `startAllMicroservices()` 의 문제점
- HTTP 서버가 먼저 실행된 후 마이크로서비스가 실행됩니다.
- 클라이언트가 HTTP 요청을 보낼 때, 마이크로서비스가 아직 준비되지 않았을 가능성이 있습니다. 특정 API가 마이크로서비스를 호출해야 하는 경우(예를 들어, API가 메시지큐에 메시지를 보내는 경우), 요청이 실패할 수 있습니다.
- 서버 시작 시 마이크로서비스를 호출해야 하는 경우 문제가 발생할 수 있습니다.
- `onApplicationBootstrap()`이나 `onModuleInit()` 를 활용시, 마이크로서비스를 호출해야한다면 마이크로서비스가 준비되기 전 실행되어 오류가 발생할 수 있다고 합니다.(참고)
마무리
이번 글에서는 NestJS에서 마이크로서비스 아키텍처를 구성하는 방법과 하이브리드 애플리케이션을 활용하여 HTTP API 서버와 마이크로서비스 통신을 위한 내부 서버를 함께 운영하는 방식에 대해 알아보았습니다.
참고 자료
https://docs.nestjs.com/microservices/basics
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
https://docs.nestjs.com/faq/hybrid-application
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
https://github.com/nestjs/docs.nestjs.com/issues/2485
Update FAQ Hybrid application Docs to clarify bootstrap order · Issue #2485 · nestjs/docs.nestjs.com
Is there an existing issue that is already proposing this? I have searched the existing issues Is your feature request related to a problem? Please describe it When utilizing onApplicationBootstrap...
github.com
https://github.com/nestjs/nest
GitHub - nestjs/nest: A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applica
A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀 - nestjs/nest
github.com
'개발 > Node.js, TypeScript' 카테고리의 다른 글
[NestJS] Prisma Transaction (0) | 2025.03.02 |
---|---|
[NestJS] 로컬에서 스크립트 실행하기 (feat. nestjs-command) (0) | 2025.02.16 |
[Node.js] 인앱결제 서버 개발기2 (구글 플레이스토어) (0) | 2023.07.23 |
[Node.js] 인앱결제 서버 개발기1 (앱스토어) (1) | 2023.07.19 |