[NestJS] Guard가 정답일까? (Middleware와 Guard의 트레이드오프 회고)
The Problem: '보안 통제와 쿠키 조작'을 위한 최적의 라이프사이클 찾기
호텔 예약·결제 통합 플랫폼의 1인 백엔드로 개발을 진행하며, 관리자(/admin) 페이지 하위의 모든 라우팅에 대해 강력한 보안 통제가 필요했습니다.
요구사항은 Access/Refresh 토큰을 검증하고, 필요시 재발행하여 HTTP Response 쿠키에 세팅 한 뒤, 비즈니스 로직에서 쓸 수 있도록 Request 객체에 유저 정보(req.user)를 주입 하는 것이었습니다.
초기에는 res.cookie() 세팅과 객체 조작이 직관적인 Middleware 를 선택하여 완벽하게 동작하는 코드를 완성했습니다. 하지만 프로젝트 중반, NestJS 공식 문서와 ExecutionContext를 깊이 파보며 "Guard에서도 충분히 req와 res 객체를 추출하여 쿠키를 굽고 유저 정보를 주입할 수 있다" 는 사실을 깨달았습니다.
인증은 Guard에서 처리하라는 NestJS의 권장 패턴(안티 패턴 사용에 대한 불안감)과 이미 작성된 수많은 미들웨어 기반 코드 사이에서, 아키텍처를 뒤엎어야 할지 깊은 딜레마에 빠졌습니다.
The Solution: '라우트 기반 일괄 제어'를 위한 MiddlewareConsumer의 선택
결론적으로, 저는 Guard로 마이그레이션하지 않고 기존의 Middleware 구조를 유지하기로 결정했습니다. 이는 귀찮음이 아니라 '유지보수성'을 위한 명확한 아키텍처적 의사결정이었습니다.
만약 Guard로 전환한다면, 이미 만들어둔 수십 개의 /admin 컨트롤러나 개별 메서드 상단에 일일이 @UseGuards(AdminAuthGuard) 데코레이터를 붙여야 합니다. 혹여나 개발자의 휴먼 에러로 데코레이터를 하나라도 빼먹는다면 치명적인 보안 구멍이 발생합니다.
반면 Middleware는 MiddlewareConsumer를 통해 라우트 기반의 일괄 통제 가 가능했습니다.
export class AdminModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminAuthMiddleware)
.exclude({ path: '*prefix/admin/auth/login', method: RequestMethod.POST }) // 로그인만 예외 처리!
.forRoutes('*prefix/admin'); // /admin 하위 모든 라우트에 일괄 적용
}
}
컨트롤러가 수백 개로 늘어나도, /admin 경로에 속하기만 하면 자동으로 인증 파이프라인을 타게 됩니다. 로그인 API를 제외(exclude)하는 로직도 한곳에서 중앙 통제할 수 있어 시스템의 안정성이 훨씬 높아졌습니다.
The Result & Retrospective
프레임워크가 제공하는 '권장 사항'은 훌륭한 나침반이지만, 그것이 현재 우리 시스템의 생산성과 유지보수성에 항상 100% 정답이 되지는 않습니다.
메서드 단위의 세밀한 권한 제어가 필요하다면 Guard가 압도적으로 유리하겠지만, 특정 경로를 그룹화하여 일괄적이고 누락 없는 보안망을 구축해야 하는 이번 프로젝트에서는 MiddlewareConsumer를 활용한 통제가 훨씬 더 견고한 아키텍처였습니다.
이 경험을 통해 단순히 기술의 사용법을 아는 것을 넘어, 기술 간의 장단점을 파악하고 현재 비즈니스 상황에 맞는 합리적인 트레이드오프를 저울질할 줄 아는 시야 를 얻게 되었습니다.