routing-controllers로 리팩터링하기: 2. Interceptor
목차
시작한 이유 #
1편에서는 Passport 인증 로직을 routing-controllers의 class middleware로 옮겼습니다. 두 번째로 정리할 대상은 response logging이었습니다.
기존 프로젝트는 morgan middleware로 method, URL, status, response time을 기록했습니다. 이 방식은 HTTP 요청의 흐름을 확인하기에는 충분했지만 controller가 반환한 값을 함께 살펴보려면 다른 접근이 필요했습니다.
Express middleware에서 response body를 기록하려면 res.send()나 res.json()을 감싸는 방식까지 고려해야 합니다. 반면 routing-controllers의 interceptor는 action이 반환한 content를 직접 받을 수 있습니다. 그래서 요청 메타데이터는 middleware에 남기고, 반환값 logging은 interceptor로 분리했습니다.
middleware와 interceptor의 위치 #
두 기능은 실행되는 위치가 다릅니다.
- middleware는 Express의
request,response,next를 받아 요청 흐름 전후에 동작합니다. - interceptor는 controller action이 실행된 뒤
Action과 반환된content를 받습니다.
인증처럼 action 실행 전에 확인해야 하는 로직은 middleware가 자연스럽습니다. controller의 결과를 관찰하거나 변환하는 로직은 interceptor가 더 직접적이었습니다.
기존 response logging #
기존 handler.middleware.ts는 status에 따라 morgan handler를 나눴습니다.
| |
app.ts의 initMiddleware()에서 두 handler를 직접 등록하고 있었습니다.
| |
이후 morgan logging은 LoggingHandler라는 class middleware로 옮겼습니다. 다만 이 middleware의 역할은 method, URL, status, response time 같은 요청 메타데이터를 기록하는 데 두었습니다.
ResponseInterceptor 만들기 #
controller의 반환값은 별도의 ResponseInterceptor에서 기록했습니다.
| |
action에서는 현재 request와 response를 가져오고, content에서는 action의 반환값을 받습니다. 정상 response는 현재 status와 반환값을 기록한 뒤 원래 content를 그대로 돌려줍니다.
당시 구현에는 content instanceof HttpError 분기도 넣었습니다. 하지만 controller에서 throw된 모든 오류가 이 분기를 거친다는 뜻은 아닙니다. exception의 최종 response 변환은 별도로 등록한 error middleware가 담당합니다.
전역 interceptor로 등록하기 #
특정 controller마다 decorator를 반복하지 않고, useExpressServer() options에 interceptor 경로를 등록했습니다.
| |
이 설정으로 모든 controller action의 반환값을 같은 위치에서 확인할 수 있게 했습니다. 각 controller는 logging을 위해 별도의 함수를 호출할 필요가 없습니다.
error middleware와 역할 나누기 #
이 변경에서도 HttpErrorHandler는 제거하지 않았습니다. interceptor와 error middleware의 책임이 다르기 때문입니다.
LoggingHandler: method, URL, status, response time 기록ResponseInterceptor: controller가 반환한 content 기록HttpErrorHandler: throw되거나next(error)로 전달된 오류를 HTTP response로 변환
모두 response 주변에서 동작하지만 같은 일을 하지는 않습니다. 실행 시점과 입력값에 맞춰 역할을 나누니 controller 밖의 공통 처리를 찾기가 쉬워졌습니다.
지금 다시 보면 #
당시 코드는 JSON.stringify(content)로 response body 전체를 기록했습니다. 개발 중에는 빠르게 내용을 확인할 수 있지만, 운영 환경에서는 민감한 값이나 큰 payload까지 log에 남을 수 있습니다.
지금 다시 만든다면 method, URL, status, duration, request ID처럼 검색에 필요한 필드를 구조화하고, response body는 기본적으로 제외하겠습니다. 꼭 필요하다면 허용한 필드만 남기거나 민감한 값을 마스킹한 뒤 debug 환경에서 제한적으로 기록하는 편이 안전합니다.
정리 #
middleware와 interceptor 중 하나만 선택할 필요는 없었습니다. 요청 메타데이터와 오류 흐름은 middleware에 두고, controller의 반환값은 interceptor에서 다뤘습니다.
routing-controllers가 action의 전후 지점을 나눠 제공했기 때문에 각 공통 로직을 실행 시점에 맞는 위치로 옮길 수 있었습니다.