가드·인터셉터·파이프·필터 중 무엇이 먼저 도는지 매번 틀린 문제
문제 발생
인증 가드에서 던진 예외를 컨트롤러 단위 예외 필터가 잡아 주기를 기대했는데, 잡히지 않고 전역 필터로 갔습니다. 반대로 요청 본문을 로깅하려고 만든 인터셉터에서는 아직 변환되지 않은 원본 값이 찍혔습니다.
어느 것이 먼저 도는지 감으로 짐작하다가 매번 틀렸습니다.
원인 분석
Nest는 요청 처리 순서를 문서로 못박아 두었습니다. 순서는 이렇습니다.
- 요청 도착
- 미들웨어 — 전역 바인딩 → 모듈 바인딩
- 가드 — 전역 → 컨트롤러 → 라우트
- 인터셉터(컨트롤러 이전) — 전역 → 컨트롤러 → 라우트
- 파이프 — 전역 → 컨트롤러 → 라우트 → 파라미터
- 컨트롤러 핸들러
- 서비스
- 인터셉터(응답) — 라우트 → 컨트롤러 → 전역
- 예외 필터 — 라우트 → 컨트롤러 → 전역
- 응답
두 가지가 위 증상을 설명합니다.
파이프는 인터셉터보다 늦습니다. 그래서 인터셉터에서 본 값은 아직 ValidationPipe의 변환을 거치지 않은 원본입니다. 변환된 DTO를 보고 싶다면 인터셉터가 아니라 다른 지점이어야 합니다.
예외 필터만 방향이 반대입니다. 문서가 명시합니다 — 필터는 가능한 가장 낮은 수준에서 해석됩니다. 라우트에 바인딩된 필터에서 시작해 컨트롤러 수준, 마지막으로 전역 필터로 진행합니다. 그리고 필터는 전역이 먼저 해석되지 않는 유일한 컴포넌트라고 덧붙입니다. 가드에서 던진 예외가 컨트롤러 필터로 가지 않은 것은 순서가 아니라, 그 예외가 해당 라우트/컨트롤러 필터가 처리하는 타입이 아니었기 때문에 상위로 올라간 것입니다.
해결 방안
- 각 컴포넌트를 목적에 맞는 자리에 둡니다.
| 하고 싶은 일 | 자리 |
|---|---|
| 요청/응답 객체 자체를 다뤄야 함 (raw body, 쿠키 파서) | 미들웨어 |
| 이 요청을 처리할지 말지 결정 | 가드 |
| 입력을 변환·검증 | 파이프 |
| 응답을 감싸거나 시간 측정, 캐시 | 인터셉터 |
| 오류를 응답 형태로 바꿈 | 예외 필터 |
-
변환된 값을 보려면 파이프 이후를 봅니다. 로깅이 목적이라면 인터셉터의 응답 쪽(
tap)에서 컨트롤러가 받은 값을 함께 남기는 편이 정확합니다. -
인터셉터는 "먼저 들어가고 나중에 나옵니다". 요청 방향은 전역→라우트, 응답 방향은 라우트→전역입니다. 시간을 재는 인터셉터를 전역에 두면 안쪽 인터셉터의 시간까지 포함합니다.
@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)));
}
}- 오류를 인터셉터에서 먼저 잡을 수도 있습니다. 문서가 명시합니다 — 파이프·컨트롤러·서비스가 던진 오류는 인터셉터의
catchError연산자에서 읽을 수 있습니다. 필터보다 앞이라는 뜻입니다.
return next.handle().pipe(
catchError((error) => {
this.logger.error(error);
return throwError(() => error); // 필터가 계속 처리하게 둔다
}),
);- 전역 필터는 최후의 그물로 둡니다. 라우트·컨트롤러 필터가 먼저 보므로, 전역에는 "여기까지 온 것은 예상 못 한 오류"라는 처리를 둡니다. 스택 트레이스를 응답에 담지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.