[Nest JS] @nestjs/microservices 사용 시 microservice 간 예외 처리

toto9602·2024년 4월 28일
1

NestJS 공부하기

목록 보기
4/4

본 포스팅에서는, 최근 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

@ToRpcException 데코레이터 작성

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를 다룬 이 글의 "메타데이터를 유지하는 데코레이터" 부분을 참고하시면 좋을 것 같습니다! :)

예외를 RpcException으로 감싸기

[ 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으로 감싸는 로직도 필요할 듯합니다!

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글