본 포스팅에서는, 최근 Nest JS로 작성한 단일 HTTP API 서버를
monorepo mode + nestjs/microservices 기반으로 수정하며 겪은, 예외 처리 문제와 그 해결 과정을 담고자 합니다.
전체 공개하기 어려운 소스 코드의 경우 일부 로직을 생략하였음을 밝힙니다! (__)
[ 3줄 요약 ]
1. NestJS의 microservice는 RpcException
을 사용한다.
2. 기존 HTTPException
을 그대로 사용하면, 다른 microservice에 예외가 온전한 형태로 전달되지 않는다.
3. microservice에서 발생시키는 HTTPException
및 custom exception을 RpcException
으로 감싸 throw하는 방식으로 해결하였습니다 :)
Nest JS 공식문서 - microservices/exception-filters
Typescript Documentation : method-decorators
toss tech - NestJS 환경에 맞는 Custom Decorator 만들기
최근 진행한 프로젝트에서, 기존 단일 HTTP API 서버였던 구조를 아래와 같이 api-gateway
와 여러 microservice로 분리하는 작업을 진행하였습니다.
api-gateway
는 3000번 포트를 listen하는 HTTP API 서버이고,
이외 microservice는 nestjs/microservices의 기본 transport인 TCP을 사용하는 방식으로 구현하였습니다!
[ api-gateway # main.ts ]
async function bootstrap() {
const app = await NestFactory.create(ApiGatewayModule);
...
...
await app.listen(3000);
}
[ microservice 1,2,3 # main.ts ]
async function bootstrap() {
const app = await NestFactory.createMicroservice(MicroserviceAModule, {
transport: Transport.TCP,
options: {
host,
port: +port,
},
});
...
...
await app.listen();
}
microservice에서 api-gateway
로 예외를 throw하는 경우 문제가 발생하였습니다.
[ api-gateway - microservice #1 호출 ]
public async getAResponse() {
try {
const responseObservable = this.msOneClient
.send<MSAResponse>(MSG_PATTERN_A, req)
.pipe(timeout(this.CLIENT_ONE_TIMEOUT));
const response = await firstValueFrom(responseObservable);
return response;
} catch (e: any) {
console.log(e);
throw e;
}
}
[ microservice #1에서 BadRequestException
throw 시 예외 ]
{ status: 'error', message: 'Internal server error' }
microservice에서 어떤 예외를 throw하여도, api-gateway
에서는 위와 같은 형태로 받아져 어떤 예외 상황인지 식별할 수 없는 상황이었습니다..!
원인은 Nest JS 공식 문서의 microservice Exception Filters 항목에서 찾을 수 있었습니다.
The only difference between the HTTP exception filter layer and the corresponding microservices layer is that instead of throwing HttpException, you should use RpcException.
→ microservice에서 HTTPException
대신 RpcException
을 사용해야 함!
저는 메서드에서 발생한 Exception을 RpcException
으로 감싸주는 간단한 데코레이터를 달아주는 방식으로 수정하였습니다!
cf. Typescript 문서 : method-decorators
function toRpcException() {
return function (target:any, propertyKey:string, descriptor:PropertyDescriptor) {
descriptor.value = async function (...args: any) {
try {
// decorator가 사용될 원래 메서드를 실행
return await originMethod.apply(this, args);
} catch (err: any) { // 예외가 발생하면
// RpcException으로 감싸서 throw!
throw new RpcException(err);
}
};
}
P.S. 본문과 큰 관련이 없어 생략하였지만, 혹시 데코레이터의 적용으로 다른 데코레이터의 적용이 오버라이드되는 등의 문제가 발생할 경우에 대비해,
기존에 등록된 metadata를 변수에 할당해 두었다가, 다시 등록해 주는 로직이 필요할 수 있습니다!
관련하여서는, 토스 기술 블로그에서 NestJS 환경에서의 Custom Decorator를 다룬 이 글의 "메타데이터를 유지하는 데코레이터" 부분을 참고하시면 좋을 것 같습니다! :)
[ microservice #1 - 호출되는 메서드 ]
// 데코레이터 적용!
@toRpcException()
public async getAResponse() {
// 예외가 발생하는 상황
throw new BadRequestException();
}
[ api-gateway - microservice #1 호출 결과 ]
{
response: { message: 'Bad Request', statusCode: 400 },
status: 400,
options: {},
message: 'Bad Request',
name: 'BadRequestException'
}
기존엔 예외가 식별되지 않았지만, 이제 의도한 BadRequestException
예외 객체를 확인할 수 있었습니다!
P.S. 해당 객체는 HTTPException의 instance 형태는 아닌 듯하여,
instanceof
메서드를 사용하려면 별도로 HTTPException으로 감싸는 로직도 필요할 듯합니다!