이 번역을 시작하게 된 이유는 실제로 번역이 된 Nestjs의 MicroServices문서가 전혀 없어서 시작하게 되었습니다.
단순하게 번역을 목적으로하기에 번역이 필요하신 분만 읽어보시는 것을 추천드립니다
이 글에 나온 내용을 간단히 요약하고 진행하도록 하겠습니다.
1. NestJS는 추상화가 잘 되어있어서, 다른 곳에서 사용하는 것과 거의 똑같이 microservice에서 적용할 수 있다.
2. 방식은 크게 2가지가 있다.
3. Request-response패턴
@MessagePattern을 통해서 처리하는 controller의 역할을 함
연결을 무조건 보장하는 안정성을 가지고 있음
4. Event-based 패턴
@EventPattern을 통해서 처리하는 controller의 역할을 함.
Kafka같은 메세지 큐를 이용하는 경우가 많음
Overview
기존 monolithic 아키텍처 구조 뿐만 아니라 Nest는 microservice 아키텍처를 정식으로 지원하고 있습니다.
다른 문서에서 설명되어있는 거의 대부분의 개념인 DI, decorators, exeption filters, pipes, guards, interceptors 같은 것들은 microservice 에서 동일하게 작동합니다.
Nest에서는 Http 기반 플랫폼과, WebSocket 기반에서 동작할 때와 동일하게 추상화를 통해서 동일한 컴포넌트를 사용할 수 있습니다.
이 섹션에서는 Nest가 microservice에서 어떻게 작동하는지에 대한 내용이 나옵니다.
Nest에서 microservice는 transport layer를 통해서 작동합니다. (OSI 7계층중 4계층인 TCP프로토콜을 이용합니다)
Nest는 마이크로서비스 인스턴스 간의 메시지 전송을 담당하는 Transporter라고 하는 기본 전송 계층 구현을 지원합니다.대부분의 Transporter는 request-response모델과, event-based 모델을 지원합니다. Nest는 추상화를 통해서 정식 interface와, 각각의 Transporter 사이에 interface를 연결해줍니다. 이를 통해서 다른 방식으로 전환하는 것을 편하게 도와줍니다.
이를 통해 특정 방식의 기능을 다른 기능으로 쉽게 이동시킬 수 있습니다
Installation
npm i --save @nestjs/microservices
Getting started
microservice를 시작하기 위해서 NestFactory클래스에 있는 createMicroservice() 메소드를 사용합니다.
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();
Hint
Microservice 는 TCP 레이어를 기본으로 사용합니다.
createMicroservice()의 2번째 인자는 options 객체입니다. 이것은 2개의 데이터를 가질 수 있습니다
transport | Transporter를 구체적으로 설정합니다. (예를 들어서 Transport.NATS) |
options | Transporter별로 행동을 지정할 수 있습니다 |
Options객체는 transporter를 기준으로 선택됩니다. TCP Transporter는 아래에 있는 속성이 가능합니다.
host | Connection hostname |
port | Connection port |
retryAttempts | 실패시 재시도 횟수 (default: 0) |
retryDelay | 재시도간 시간 간격(ms) (default: 0) |
Patterns
Microservices는 메세지와, 이벤트를 모두 패턴으로 인식합니다. 패턴은 순수 값인데, 예를 들면 객체, 문자열 같은 것이 가능합니다. 패턴은 네트워크를 통해서 직렬화(serialize)되고, 데이터의 일부분으로 전송됩니다. 이 방법으로, 메세지를 보내는 사람과, 소비자는 어떤 handler를 통해서 처리되는지 알 수 있습니다
Request-response
request-response 방식은 다양한 외부 서비스와 메세지를 교환해야 할 때 효과적입니다. 이 패러다임을 통해서 우리는 실제로 메세지를 수신했음을 확인할 수 있습니다.(직접 ACK프로토콜을 구현하지 않아도 됩니다)
request-response 모델은 언제나 가장 좋은 선택이 되지는 않습니다. streaming transporter가 log-based persistence를 이용하는 경우(Kafka나, NATS streaming) 다양한 방식으로 이미 다양한 문제들을 해결하고, 최적화 했기 때문에, event-based messaging을 사용하면 됩니다.
request-response 메세지 타입을 사용했을 경우에, Nest는 2가지의 논리적인 채널을 사용합니다. 하나는 데이터를 수신하는 것을 목적으로 하고, 하나는 송신을 목적으로 합니다. NATs 같은 방식에서는 dual-channel-group이 기본적으로 적용되어 있습니다. 다른 경우에 Nest는 별도의 채널을 만들어 냅니다. 이것은 서버에 불필요한 overhead일 수 있기에, request-response 메세지 스타일이 필요하지 않다면 event-based method를 사용하는 것을 고려해보실 필요가 있습니다
이런 메세지 handler 를 만들기 위해서는 @nestjs/microservices패키지의 @MessagePattern() 데코레이터를 사용합니다. 이 데코레이터는 @Controller안에 있어야 합니다. provider 안에서 사용될 경우에는 Nest의 런타임에서 무시됩니다.
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class MathController {
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
위의 코드에서, accumulate() 메세지 handler는 { cmd: 'sum' } 와 일치하는 메세지 패턴을 만족하는 모든 메세지를 수신합니다. 이 메세지 handler는 하나의 인자를 받는데, 클라이언트로부터 받는 메세지입니다. 위의 경우에는 데이터는 accumulate될 수 있는 숫자 배열입니다.
Asynchronous responses
메세지 handler들은 동기 또는 비동기적으로 데이터를 처리할 수 있습니다.
@MessagePattern({ cmd: 'sum' })
async accumulate(data: number[]): Promise<number> {
return (data || []).reduce((a, b) => a + b);
}
역시 가능합니다.
메세지 handler는 Observable역시 지원합니다. 결과값은 stream이 complete를 호출할 때까지, 계속 생성됩니다.
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): Observable<number> {
return from([1, 2, 3]);
}
Event-based
Request-response 방식이 서비스들간에 데이터를 주고받을 때 이상적이긴 하지만, 결과값을 확인할 필요 없이 그냥 Event를 publish 하기만 하는 경우에는 적합하지 않습니다. 이때는 하나의 채널만 있으면 됩니다.
시스템의 일부분에서 특정 조건이 발생했음을 다른 서비스에 그냥 전달하기만 하고자 하는 경우에 이 방식이 효과적입니다.
event-handler를 만들기 위해서는 @nestjs/microservices 패키지에 @EventPattern 데코레이터를 사용하면 됩니다.
@EventPattern('user_created')
async handleUserCreated(data: Record<string, unknown>) {
// business logic
}
Hint
많은 event handler를 하나의 이벤트 패턴에 다양한 handler를 등록할 수 있고, 병렬적으로 실행됩니다.
handleUserCreated() event handler는 user_created 이벤트를 수신합니다. 이 event handler는 송신자가 주는 데이터에 대한 하나의 인자만을 받습니다. event 에 payload가 network를 통해 전송됩니다.
Decorators
더 복잡한 경우에는, 들어오는 요청들에 대해서 더 많은 정보를 얻고싶을 수 있습니다. 예를 들면 NATs가 wildcard를 통해서 해당하는 모든 부분을 수신한다고 하는 경우에, 메세지가 가지고 있던 원래 제목을 얻고 싶을 수 있습니다. 마찬가지로, Kafka의 메세지 헤도에도 액세스 할 수 있습니다.
@MessagePattern('time.us.*')
getDate(@Payload() data: number[], @Ctx() context: NatsContext) {
console.log(`Subject: ${context.getSubject()}`); // e.g. "time.us.east"
return new Date().toLocaleTimeString(...);
}
HINT
@Payload(), @Ctx() and NatsContext are imported from @nestjs/microservices.
HINT
You can also pass in a property key to the @Payload() decorator to extract a specific property from the incoming payload object, for example, @Payload('id').
Client
Client는 clientProxy 를 통해서 메세지를 교환하거나, event를 만들어 냅니다. 이 클래스는 몇 개의 메서드가 있는데, send() (request-response 메세징) emit(event-driven 메세징)을 통해서 만들 수 있습니다.
사용 방법은 아래 나온 방법중 한 가지를 선택할 수 있습니다
ClientsModule의 static 메서드 인 register를 활용해 import하는 방법이 있습니다.이 메서드는 하나의 배열을 인자로 받는데, 배열에는 각각 microservice transporter를 의미하는 객체가 들어갑니다. 각각은 name과, 필수는 아닌 transport 속성과(기본은 Transport.TCP입니다), 필수는 아닌 추가 options 속성이 있습니다.
name 속성은 필요한 곳에서 사용할 수 있는 주입 토큰의 역할을 합니다. (ClientProxy) 주입 토큰의 속성 값은 임의의 문자열 또는 JS symbol이 될 수 있습니다.
options 속성은 createMicroservice()메서드에 있는 속성과 동일한 속성이 가능합니다.
@Module({
imports: [
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
]
...
})
한 번 module이 import되면, ClientProxy는 'MATH_SERVICE'를 통해서 다른 곳에서 주입해서 사용할 수 있습니다.
constructor(
@Inject('MATH_SERVICE') private client: ClientProxy,
) {}
HINT
ClientModule과, ClientProxy 클래스는 @nestjs/microservices 패키지에서 가져올 수 있습니다
ConfigService같이, 하드코딩을 하는 것이 좋지 않은 경우에, 다른 모듈을 통해서 가져오는 것이 좋은 선택이 될 수 잇습니다. 이때 custom provider을 만드는 방식과 동일하게 제작할 수 있습니다.
아래 클래스는 transporter options를 받아서 ClientProxy 객체를 반환하는 static 메서드인 create()메서드가 있습니다.
@Module({
providers: [
{
provide: 'MATH_SERVICE',
useFactory: (configService: ConfigService) => {
const mathSvcOptions = configService.getMathSvcOptions();
return ClientProxyFactory.create(mathSvcOptions);
},
inject: [ConfigService],
}
]
...
})
Hint
ClientModule과, ClientProxy 클래스는 @nestjs/microservices 패키지에서 가져올 수 있습니다
@Client() 데코레이터를 통해서 사용하는 경우도 있습니다
@Client({ transport: Transport.TCP })
client: ClientProxy;
HINT
ClientModule과, ClientProxy 클래스는 @nestjs/microservices 패키지에서 가져올 수 있습니다
ClientDecorator를 사용하는 것은 추천하지 않는데, 그 이유는 테스팅 과정이 복잡하고, client 객체를 공유하는 과정이 힘들기 때문입니다.
ClientProxy 가 lazy한 성질을 갖기에, 즉시 만들어 지는 것이 아니라, 첫 microservice 호출이 일어나기 바로 전에 만들어 집니다. 그리고 그 요청이 재사용 되는데요. 만약 시작과 동시에 요청을 만들고 싶다면, ClientProxy 오브젝트를 만들어 내는 connect메서드를 onApplicationBootstrap() 라이프사이클에 추가하는 방식으로 해결할 수 있습니다.
async onApplicationBootstrap() {
await this.client.connect();
}
connection이 생성되지 않는다면 connect메서드는 에러와 함께 거부합니다.
Sending Messages
ClientProxy 가 send메서드를 접근할 수 있도록 만들어 뒀는데요 이것은 Observable과 함께 결과값을 반환함으로써 결과값을 쉽게 subscribe 할 수 있도록 만들어 줍니다.
accumulate(): Observable<number> {
const pattern = { cmd: 'sum' };
const payload = [1, 2, 3];
return this.client.send<number>(pattern, payload);
}
Send 메서드는 2가지 인자를 받는데, 패턴과, payload입니다. 패턴은 @Messagepattern 데코레이터 안에 있는 것과 일치할 필요가 있습니다. payload는 우리가 그 서비스로부터 받고싶은 데이터를 의미합니다. 이 메서드는 cold Observable을 반환하는데, 이는 명시적으로 subscribe를 해둬야 제대로 메세지를 받을 수 있다는 것을 의미합니다.
Publishing events
event를 전송할 때, ClientProxy 객체 가 emit() 메서드를 가지고 있어서, 메세지 브로커에게 메세지를 전달할 수 있습니다.
async publish() {
this.client.emit<number>('user_created', new UserCreatedEvent());
}
emit()메서드는 2가지 인자를 받는데, 패턴과, payload입니다. 패턴은 @EventPattern() 데코레이터를 통해서 받을 수 있고, payload는 보내게 될 내용입니다. 이 메서드는 hot Observable을 만들어서, subscribe 가 없어도 이벤트가 즉시 전송될 것입니다. (send와 차이가 있습니다. send 는 hot Observable입니다.)
Scopes
NestJS에서는 다양한 것들이 global로 공유되는데요 connection pool, singleton service, global state 모두 공유됩니다. 그래서 request/response Multi-Thread Stateless Model 을 따르지 않습니다. singleton 객체를 사용하는 것이 완벽하게 안전하죠
하지만 예외는 발생할 수 있는데요 graphql에서 per-request 캐싱이 일어나는 경우, 요청 추적, 여러 사용자간 공유되는 인스턴스 같은 경우에는 에러가 발생할 수 있습니다.
RequestContext 스코프를 @Injectable({scope}) 를 통해서 처리할 수 있습니다.
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT, RequestContext } from '@nestjs/microservices';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private ctx: RequestContext) {}
}
RequestContext는 2가지 속성이 있습니다.
export interface RequestContext<T = any> {
pattern: string | Record<string, any>;
data: T;
}
data 속성은 메세지 payload 이고, 패턴은 받게되는 메세지를 의미합니다.
Handling Timeout
microservice가 중지되는 상황이 발생할 수 있는데, 계속 기다리는 것이 아닌 일정 시간만을 기다리도록 만들 수 있습니다.
이때 RxJS의 timeout을 사용하는 방식으로 처리합니다.
this.client
.send<TResult, TInput>(pattern, data)
.pipe(timeout(5000));
5초 이 요청은 무조건 실패할 것입니다.
'NestJS' 카테고리의 다른 글
NestJS 로 GraphQL Mutation 만들기 (0) | 2022.09.23 |
---|---|
NestJS 로 GraphQL Query 만들기 (0) | 2022.09.23 |
NestJS로 GraphQL 시작하기 (0) | 2022.09.22 |