본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev49

가드·인터셉터·파이프·필터 중 무엇이 먼저 도는지 매번 틀린 문제

  • #Engineering Note
  • #NestJS

문제 발생

인증 가드에서 던진 예외를 컨트롤러 단위 예외 필터가 잡아 주기를 기대했는데, 잡히지 않고 전역 필터로 갔습니다. 반대로 요청 본문을 로깅하려고 만든 인터셉터에서는 아직 변환되지 않은 원본 값이 찍혔습니다.

어느 것이 먼저 도는지 감으로 짐작하다가 매번 틀렸습니다.

원인 분석

Nest는 요청 처리 순서를 문서로 못박아 두었습니다. 순서는 이렇습니다.

  1. 요청 도착
  2. 미들웨어 — 전역 바인딩 → 모듈 바인딩
  3. 가드 — 전역 → 컨트롤러 → 라우트
  4. 인터셉터(컨트롤러 이전) — 전역 → 컨트롤러 → 라우트
  5. 파이프 — 전역 → 컨트롤러 → 라우트 → 파라미터
  6. 컨트롤러 핸들러
  7. 서비스
  8. 인터셉터(응답) — 라우트 → 컨트롤러 → 전역
  9. 예외 필터 — 라우트 → 컨트롤러 → 전역
  10. 응답

두 가지가 위 증상을 설명합니다.

파이프는 인터셉터보다 늦습니다. 그래서 인터셉터에서 본 값은 아직 ValidationPipe의 변환을 거치지 않은 원본입니다. 변환된 DTO를 보고 싶다면 인터셉터가 아니라 다른 지점이어야 합니다.

예외 필터만 방향이 반대입니다. 문서가 명시합니다 — 필터는 가능한 가장 낮은 수준에서 해석됩니다. 라우트에 바인딩된 필터에서 시작해 컨트롤러 수준, 마지막으로 전역 필터로 진행합니다. 그리고 필터는 전역이 먼저 해석되지 않는 유일한 컴포넌트라고 덧붙입니다. 가드에서 던진 예외가 컨트롤러 필터로 가지 않은 것은 순서가 아니라, 그 예외가 해당 라우트/컨트롤러 필터가 처리하는 타입이 아니었기 때문에 상위로 올라간 것입니다.

해결 방안

  1. 각 컴포넌트를 목적에 맞는 자리에 둡니다.
하고 싶은 일자리
요청/응답 객체 자체를 다뤄야 함 (raw body, 쿠키 파서)미들웨어
이 요청을 처리할지 말지 결정가드
입력을 변환·검증파이프
응답을 감싸거나 시간 측정, 캐시인터셉터
오류를 응답 형태로 바꿈예외 필터
  1. 변환된 값을 보려면 파이프 이후를 봅니다. 로깅이 목적이라면 인터셉터의 응답 쪽(tap)에서 컨트롤러가 받은 값을 함께 남기는 편이 정확합니다.

  2. 인터셉터는 "먼저 들어가고 나중에 나옵니다". 요청 방향은 전역→라우트, 응답 방향은 라우트→전역입니다. 시간을 재는 인터셉터를 전역에 두면 안쪽 인터셉터의 시간까지 포함합니다.

@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const startedAt = Date.now();
    return next.handle().pipe(tap(() => console.log(Date.now() - startedAt)));
  }
}
  1. 오류를 인터셉터에서 먼저 잡을 수도 있습니다. 문서가 명시합니다 — 파이프·컨트롤러·서비스가 던진 오류는 인터셉터의 catchError 연산자에서 읽을 수 있습니다. 필터보다 앞이라는 뜻입니다.
return next.handle().pipe(
  catchError((error) => {
    this.logger.error(error);
    return throwError(() => error);   // 필터가 계속 처리하게 둔다
  }),
);
  1. 전역 필터는 최후의 그물로 둡니다. 라우트·컨트롤러 필터가 먼저 보므로, 전역에는 "여기까지 온 것은 예상 못 한 오류"라는 처리를 둡니다. 스택 트레이스를 응답에 담지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.