태그
목차

요청 라이프사이클

생성일: 2024-03-10

수정일: 2024-03-10

https://syntactic-sugar.dev/blog/nested-route/nest-request-lifecycle
https://syntactic-sugar.dev/blog/nested-route/nest-request-lifecycle

NestJS 애플리케이션은 클라이언트에서 서버로 요청이 들어오면, 정해진 순서에 따라 요청을 처리하고 응답을 만들어낸다.

이 과정을 요청 라이프사이클이라고 한다.

NestJS에는 미들웨어, 파이프, 가드, 인터셉터등 여러 구성요소들이 있다. 이들은 요청 처리 과정의 특정 시점에 개입해서 추가적인 작업을 수행한다.

예를 들어 미들웨어는 요청이 본격적으로 처리되기 전에 공통적인 작업을 처리하고, 가드는 인증/인가를 담당한다. 파이프는 데이터 유효성 검사나 변환을 수행하고, 인터셉터는 응답을 내보내기 전에 마지막으로 가공하는 역할을 한다.

그런데 이런 기능들은 애플리케이션 전체에 걸쳐 사용될 수도 있고, 특정 컨트롤러나 라우트에만 적용될 수도 있다. 때문에 코드 실행 순서를 추적하기가 복잡해질 수 있다.

일반적으로는 요청이 들어오면,

  1. 미들웨어를 통과하고,
  2. 가드로 권한 체크를 하고,
  3. 인터셉터와 파이프로 데이터를 가공한 다음,
  4. 컨트롤러에 도착해서 비즈니스 로직을 실행하고,
  5. 다시 인터셉터를 거쳐서,
  6. 최종 응답을 클라이언트에게 전달한다.

이렇게 요청이 처리되는 과정을 잘 이해하고 있으면, NestJS 애플리케이션의 동작을 더 쉽게 예측하고 제어할 수 있다.

미들웨어

미들웨어는 특정 순서로 실행된다.

  1. 전역 미들웨어 (app.use 를 사용하여 등록한다)
  2. 개별 경로에 적용된 미들웨어 (모듈별로 등록한다)

미들웨어는 등록한 순서대로 하나씩 실행된다. 만약 여러 모듈에 미들웨어가 등록되어 있다면, 루트 모듈(가장 상위 모듈)에 있는 미들웨어가 가장 먼저 실행된다. 그리고 나서 나머지 모듈들에 등록된 미들웨어가 실행되는데, 이때 순서는 imports 배열에 추가한 순서와 같다.

쉽게 말해서, 미들웨어는 애플리케이션 전역 => 개별 경로 순으로 실행되고, 같은 레벨에서는 등록된 순서대로 실행된다.

전역 => 개별 경로

가드

가드는 전역 => 컨트롤러 => 라우트 순으로 실행된다.

미들웨어와 마찬가지로 가드는 바인딩된 순서대로 실행된다.

@UseGuards(Guard1, Guard2)
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @UseGuards(Guard3)
  @Get()
  getCats(): Cats[] {
    return this.catsService.getCats();
  }
}

Guard1Guard2 보다 먼저 실행되며, 둘 다 Guard3 보다 먼저 실행된다.

Note

전역 바인딩과 컨트롤러 또는 로컬 바인딩의 차이점은 가드가 바인딩되는 위치에 있다. app.useGlobalGuard() 를 사용하거나 모듈을 통해 컴포넌트를 제공하는 경우 전역으로 바인딩된다. 그렇지 않으면 데코레이터가 컨트롤러 클래스 앞에 오는 경우 컨트롤러에 바인딩되고, 데코레이터가 라우트 선언 앞에 오는 경우 라우트에 바인딩된다.

전역 => 컨트롤러 => 라우트

인터셉터

인터셉터는 가드와 비슷한 방식으로 동작한다. 하지만 한 가지 중요한 차이점이 있다.

인터셉터는 RxJS Observables을 반환한다.

Observables은 비동기 데이터 스트림을 다루는 데 사용되는데, 선입 후출(LIFO) 방식으로 처리된다.

즉, 요청이 들어올 때는 전역 => 컨트롤러 => 라우트 순으로 처리되지만, 응답이 나갈 때는 반대로 라우트 => 컨트롤러 => 전역 순으로 처리된다.

그리고 여기서 발생한 오류는 인터셉터에서 catchError 를 사용해서 잡아낼 수 있다.

전역 => 컨트롤러 => 라우트 => 컨트롤러 => 전역

파이프

파이프는 들어오는 데이터를 검증하고 변환하는 역할을 한다. 파이프도 실행되는 순서는 미들웨어나 가드와 비슷하다. 전역 => 컨트롤러 => 라우트 파이프 순으로 실행된다.

@UsePipes() 데코레이터를 사용해서 파이프를 등록한 경우 등록한 순서대로 파이프가 실행된다.

반면, 라우트 핸들러의 파라미터 레벨에서 여러 개의 파이프가 실행되는 경우 마지막 파라미터에 적용된 파이프부터 첫 번째 파라미터에 적용된 파이프 순으로 실행된다.

@UsePipes(GeneralValidationPipe)
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @UsePipes(RouteSpecificPipe)
  @Patch(':id')
  updateCat(
    @Body() body: UpdateCatDTO,
    @Param() params: UpdateCatParams,
    @Query() query: UpdateCatQuery,
  ) {
    return this.catsService.updateCat(body, params, query);
  }
}

예를 들어 위의 코드에서는 @UsePipes(GeneralValidationPipe) 가 컨트롤러 레벨에 적용되어 있고, @UsePipes(RouteSpecificPipe) 는 라우트 레벨에 적용되어 있다.

그리고 updateCat 메서드에는 @Body(), @Param(), @Query() 데코레이터가 파라미터에 적용되어 있다.

이 경우, query, params, body 에 대해서 먼저 GeneralValidationPipe 가 실행되고, 그 다음에 RouteSpecificPipe 가 실행된다.

만약 특정 파라미터에 파이프가 적용되어 있다면, 그 파이프는 컨트롤러와 라우트 레벨의 파이프가 모두 실행된 후에 실행된다. 이때는 마지막 파라미터부터 첫 번째 파라미터 순으로 실행된다.

전역 => 컨트롤러 => 라우트 => 파라미터

필터

필터는 애플리케이션에서 발생하는 예외를 처리하는 역할을 한다. 다른 컴포넌트들과는 달리, 필터는 전역 레벨에서부터 실행되지 않는다.

필터는 가장 구체적인 레벨, 즉 라우트 레벨에서부터 시작한다. 그리고 컨트롤러 레벨, 마지막으로 전역 레벨 순으로 실행된다.

한 가지 중요한 점은, 예외는 필터 간에 전달될 수 없다는 것이다. 만약 라우트 레벨 필터가 어떤 예외를 잡아냈다면, 컨트롤러 레벨이나 전역 레벨의 필터에서 그 예외를 다시 잡을 수 없다.

필터 간에 정보를 공유하려면, 상속을 사용하는 방법밖에 없다. 한 필터가 다른 필터를 상속받으면, 부모 필터가 가진 정보를 자식 필터에서 사용할 수 있다.

Note

필터는 애플리케이션에서 처리되지 않은 예외가 발생했을 때만 실행된다.

만약 코드 어딘가에서 try / catch 를 사용해서 예외를 잡아냈다면, 그 예외는 필터로 전달되지 않는다. 필터는 오직 잡히지 않은 예외만 처리한다.

애플리케이션에서 처리되지 않은 예외가 발생하면, 요청 처리 과정의 나머지 부분, 예를 들어 인터셉터나 파이프 등은 모두 무시되고 바로 필터로 건너뛴다.

이때 필터는 앞에서 설명한 대로 라우트 레벨 => 컨트롤러 레벨 => 전역 레벨 순으로 실행된다.

이렇게 필터는 예외 처리의 마지막 수단이라고 할 수 있다. 코드의 다른 부분에서 예외를 적절히 처리하지 않았을 때, 필터가 개입해서 예외를 처리하고 적절한 응답을 클라이언트에게 보내준다.

라우트 => 컨트롤러 =>  전역

요약

일반적으로 요청 라이프사이클은 다음과 같은 순으로 진행된다.

  1. 인바운드 요청
  2. 미들웨어
    1. 전역 미들웨어
    2. 모듈 미들웨어
  3. 가드
    1. 전역 가드
    2. 컨트롤러 가드
    3. 라우트 가드
  4. 인터셉터 (컨트롤러 이전)
    1. 전역 인터셉터
    2. 컨트롤러 인터셉터
    3. 라우트 인터셉터
  5. 파이프
    1. 전역 파이프
    2. 컨트롤러 파이프
    3. 라우트 파이프
    4. 라우트 파라미터 파이프
  6. 컨트롤러 (메서드 핸들러)
  7. 서비스 (존재하는 경우)
  8. 인터셉터 (요청후)
    1. 라우트 인터셉터
    2. 컨트롤러 인터셉터
    3. 전역 인터셉터
  9. 예외 필터
    1. 라우트
    2. 컨트롤러
    3. 전역
  10. 서버 응답