캡스톤디자인(2)/공부

NestJS에 MSA 구축하기

ludvbg (이지현) 2023. 3. 21. 23:10

이번 학기 캡스톤디자인에서 우리 팀 프로젝트에 MSA를 구축해야 한다.

우리 프로젝트의 백엔드 프레임워크는 NestJS이므로 NestJS에 MSA를 구축하는 방법에 대해서 조사하고 정리해보았다.

 

1. MSA

기존 우리 프로젝트처럼 서비스들이 하나로 통합되어 있는 Monolithic Architecture와 달리 애플리케이션을 구성하는 서비스들이 서로 독립한 상태로 존재하는 것을 마이크로서비스(MicroService)라고 하며, 이 마이크로서비스들을 종합적으로 사용하는 인프라 구조를 마이크로서비스 아키텍처(MicroService Architecture, MSA)라고 한다.

 

Monolithic Application을 MicroService로 분할 (https://aws.amazon.com/ko/microservices/)

 

- Monolithic Architecture

: MSA 이전의 아키텍처를 지칭하는 의미로 생겨난 단어

모든 모듈은 하나의 서비스 내부에 종속되어 있으며 서비스 자체에 집중할 수 있는 구조로 되어있다.

각기 다른 역할을 하는 모듈들이 모여서 하나의 프로젝트를 이루는 것

이는 개발, 빌드, 배포가 용이할 수 있지만 프로젝트의 규모가 커질수록 수정, 새로운 기능의 추가가 어렵다.

 

장점: 구조가 단순하고, 개발환경과 방법이 통일되어 있다. 그리고 배포가 간편하고 End to End(종단 간) 테스트가 쉽다.

단점: 프로젝트의 규모가 커질수록 복잡도가 심각하게 증가하고 코드 전체를 이해하기 힘들다. CI/CD가 어렵다. 모든 모듈이 하나의 프로세스에서 동작하기 때문에 모듈 하나가 수정되어 서버를 내렸다 올릴 경우 다른 모듈도 그 동안 작동할 수 없다.

 

- MicroService Architecture

: 작은 서비스 여러개가 모여서 하나의 시스템을 제공하는 아키텍처

각 서비스는 작고 독립적이며 느슨하게 결합되어 있다.

그렇기 때문에 서비스들을 독립적으로 배포할 수 있으며, 전체 프로그램을 빌드한 뒤에 재배치하지 않아도 기존 서비스들을 업데이트 할 수 있다.

CI(지속적 통합) / CD(지속적 배포)가 필수이다.

 

장점: 서비스가 분리되어 있어서 서비스의 생성과 삭제가 자유롭고, 하나의 서비스가 다운되어도 전체 서비스가 마비되는 상황이 발생하지 않는다. 분리된 서비스들이 같은 언어로 개발될 필요가 없다.

단점: 모든 서비스를 독립적으로 분할해야 하기 때문에 모듈 간의 인터페이스를 적절히 고려해야 한다. 그리고 서비스 간 통신방법이 필요하고 복잡하다. 서비스끼리의 테스트가 어려우며, 통합적인 유지관리가 어려워질 수 있다.

 

What are microservices? (https://microservices.io/)

 

1-1. MSA를 쓰는 이유

MSA가 복잡하고 어려워보이지만 쓰는 이유는 서비스의 재사용성과 비용 면에서 클라우드 환경과 잘 맞기 때문이다.

Monolithic 구조에서는 사용량이 적은 모듈을 삭제한다고 하더라도 전체 시스템 스펙은 변하지 않기 때문에 사용량은 같을 것이므로 사용량 단위로 과금을 해야하는 클라우드 환경에서는 비효율적이다.

MicroService 구조에서는 서비스 단위로 기능을 분리해서 구축할 수 있기 때문에, 사용하지 않거나 사용량이 적은 기능을 축소해서 효율화시킬 수 있다.

 

 

2. NestJS에 MSA 구축하기

- 제목은 NestJS에 MSA 구축하기이지만 NestJS에서 Microservice끼리 통신하는 방법에 가까운 것 같다.

Nest에서 MicroService는 기본적으로 HTTP와 다른 전송 계층을 사용하는 애플리케이션이다.

서로 다른 MicroService 인스턴스 간의 메세지 전송을 담당하는 'transporters'라는 여러 기본 제공 전송 계층 구현을 지원한다. 대부분의 transporter는 기본적으로 request-response 및 event-based 메세지 스타일을 모두 지원한다.

Nest는 위의 두 스타일에 대한 표준 인터페이스 뒤의 각 transporter의 구현 세부사항을 추상화한다. (이게무슨말) 이를 통해 애플리케이션 코드에 영항을 주지 않고 한 전송 계층에서 다른 전송 계층으로 쉽게 전환할 수 있다.

 

2-1. 설치

 

$ npm i --save @nestjs/microservices

 

이제 @nestjs/microservices를 import하여 NestJS에서 MicroService를 사용할 수 있다.

 

2-2. 적용

MicroService를 인스턴스화 하기 위해, createMicroservice() 메소드를 사용한다.

createMicroservice()는 NestFactory 클래스에 있음

 

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();

createMicroservice()의 두 번째 인자에는 options 객체가 들어가는데, options 객체에는 transport, options 두 멤버가 있다. transport는 transporter를 구체화하는데, Transport.NATS / Transport.TCP 와 같이 사용한다.

options는 선택한 transporter에 따라 다른데, TCP의 경우 host, port, retryAttempts, retryDelay의 옵션들이 있다.

 

2-3. 패턴

마이크로서비스는 패턴으로 메세지와 이벤트를 모두 인식한다. 패턴은 literal 객체나 문자열이다.

메세지를 주고 받을 때 같은 패턴(이름)을 사용한 보낸 메세지와 받는 메세지를 매칭하는 방식

 

2-4. 메세지 전달 방식

1) request-response (요청-응답)

- 다양한 외부 서비스 간에 메세지를 교환해야 할 때 유용

- 메세지 ACK 프로토콜을 수동으로 구현할 필요 없이 서비스가 실제로 메세지를 수신했는지 확인할 수 있음

- Nest에서는 두 개의 논리 채널을 사용하여 요청-응답 메세지 유형을 활성화하는데, 하나는 데이터 전송을 담당하고 하나는 들어오는 응답을 기다림

이때 Nest는 별도의 채널을 수동으로 생성하므로 이에 따른 오버헤드가 발생할 수 있음.

 

요청-응답 패러다임을 기반으로 메시지 핸들러를 생성하려면 @MessagePattern() 데코레이터를 사용한다.

이 데코레이터는 Controller 클래스 내에서만 사용해야 함 (Provider에서 사용하면 Nest 런타임에서 무시됨)

아래 코드처럼 사용하면 된다.

 

math.controller.ts >

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()는 메세지 핸들러 -> 메세지를 수신 대기 ({ cmd: 'sum' }인 메세지를)

 

 

2) Event-based (이벤트 기반)

- 요청-응답 방법은 응답을 기다리지 않고 이벤트를 게시하려는 경우에는 적합하지 않음

그리고 이 경우는 두 채널을 유지하기 위해 요청-응답에 필요한 오버헤드를 원하지 않음

- 시스템의 어떤 부분에서 특정 조건이 발생했다는 것을 단순히 다른 서비스에 알리고 싶을 때 이벤트 기반 메세지 스타일의 이상적인 사용 예시임.

 

이벤트 기반 패러다임을 기반으로 이벤트 핸들러를 생성하기 위해서 @EventPattern() 데코레이터를 사용한다.

 

@EventPattern('user_created')
async handleUserCreated(data: Record<string, unknown>) {
	// business logic
}

handlerUserCreated() 이벤트 핸들러는 'user_created' 이벤트를 수신 대기한다. 이벤트 핸들러는 클라이언트에서 전달된 'data' 단일 인수를 사용한다.

 

2-5. 클라이언트

클라이언트 Nest 응용 프로그램은 ClientProxy 클래스를 사용하여 메세지를 교환하거나 이벤트를 Nest Microservice에 게시할 수 있다.

이 클래스는 remote Microservice와 통신할 수 있도록 하는 send() 및 emit()과 같은 여러 메소드를 정의한다.

    send(): 요청-응답 메시징에 사용

    emit(): 이벤트 기반 메시징에 사용

 

한 가지 방법은 정적 register() 메소드를 가진 ClientsModule을 import하는 것이다.

@Module({
	imports: [
    	ClientsModule.register([
        	{ name: 'MATH_SERVICE', transport: Transport.TCP },
        ]),
    ]
    ...
})

register() 메소드의 각 객체에는 이름 속성, transport 속성 (optional) (default:  Transport.TCP), transporter별 option 속성 (optional)이 있다.

 

위의 코드대로 모듈을 import 하면, 'MATH_SERVICE' transporter 옵션을 통해 ClientProxy 인스턴스를 @Inject() 데코레이터를 사용하여 inject할 수 있다.

constructor(
	@Inject('MATH_SERVICE') private client: ClientProxy,
){}

 

클라이언트 응용 프로그램에서 transporter 구성을 다른 서비스(ConfigService 등)에서 가져와야할 경우, ClientProxyFactory 클래스를 사용해서 커스텀 provider를 등록할 수 있다. 이 클래스에는 정적 create() 메소드를 사용한다.

@Module({
	providers: [
    	provide: 'MATH_SERVICE',
        useFactory: (configService: ConfigService) => {
        	const mathSvcOptions = configService.getMathSvcOptions();
            return ClientProxyFactory.create(mathSvcOptions);
        },
        inject: [ConfigService],
    ]
    ...
})

 

2-6. 메세지 전송

1) ClientProxy의 send() 메소드를 사용한다. (send()는 요청-응답 방법)

accumulate(): Observable<number> {
	const pattern = { cmd: 'sum' };
    const payload = [1, 2, 3];
    return this.client.send<number>(pattern, payload);
}

pattern은 @MessagePattern() 데코레이터 안에 정의된 패턴과 일치해야 한다.

payload는 우리가 remote 마이크로서비스로 전송할 메세지이다.

 

2) ClientProxy의 emit() 메소드를 사용한다. (emit()은 이벤트 기반 방법)

async publish() {
	this.client.emit<number>('user_created', new UserCreatedEvent());
}

여기서도 pattern은 @EventPattern() 데코레이터 안에 정의된 패턴과 일치해야 하며,

payload는 우리가 remote 마이크로서비스로 전송할 이벤트 메세지이다.

 

 

 

NestJS에서 Microservice를 사용하기 위해 더 많은 내용이 있지만 일단 기본적인 방법 정도만 이해했다고 생각한다. 지금 이해한 것으로 실제로 구현하기는 어렵겠지만 지금은 어떤 흐름인지 대충 이해한 것만으로 괜찮다고 생각한다.

 

 

+ 참고 / 더 많은 내용

https://docs.nestjs.com/microservices/basics