
미들웨어에 대해 알아보기 전에, 한 가지 짚고 넘어가야 할 개념이 있다. Nest.js 서버로 들어온 요청은 단순히 컨트롤러로 바로 전달되지 않는다. 앞서 살펴본 가드나 파이프뿐만 아니라, 미들웨어와 인터셉터 등을 거쳐 단계적으로 처리된 뒤 컨트롤러에 도달한다. 그리고 컨트롤러에서 처리된 결과는 다시 인터셉터와 예외 필터를 거쳐 클라이언트로 전달된다.
이처럼 요청이 서버에 들어와 응답으로 나가기까지 거치는 일련의 흐름을 요청 생명주기(Request Lifecycle) 라고 부른다. Nest.js에서 HTTP 요청은 다음과 같은 순서로 처리된다. 이때 컨트롤러 이전 단계에서 동작하는 컴포넌트들은 전역 → 컨트롤러 → 라우터 순으로 실행되고, 컨트롤러 이후 단계에서 동작하는 컴포넌트들은 그 반대 순서로 실행된다. 이 실행 순서의 특성은 미들웨어의 역할과 위치를 이해하는 데 매우 중요하다.

Middleware
토큰이 만료되었을 때, 클라이언트가 직접 refresh 엔드포인트를 호출하도록 만들 수도 있다. 하지만 나는 이 과정을 클라이언트에 맡기지 않고, Nest.js가 요청을 처리하는 과정에서 알아서 토큰을 재발급하도록 구성하기로 했다.
이미 가드를 사용해 유저 인증을 구현한 상태였기 때문에, 토큰 재발급 로직은 가드보다 앞 단계에서 실행되어야 했다. 요청 생명주기를 기준으로 보면, 이 조건을 만족하는 컴포넌트는 미들웨어뿐이다. 그래서 토큰 재발급 로직을 미들웨어로 구현하는 것이 가장 자연스러운 선택이었다.
Nest.js에서 미들웨어는 보통 NestMiddleware 인터페이스를 구현하는 Injectable한 프로바이더 클래스로 작성된다. 다만 전역 미들웨어로 적용하는 경우에 한해 함수형 미들웨어를 사용할 수도 있다.
이 차이에 대한 설명은 글의 후반부에서 다시 다루고, 특별한 언급이 없는 한 이 글에서 말하는 “미들웨어”는 NestMiddleware를 구현한 클래스형 미들웨어를 의미한다.
토큰 재발급 로직에서 AuthService를 사용해야 했기 때문에, 미들웨어 모듈에 AuthModule을 import했다.
import { Module } from '@nestjs/common';
import { RefreshMiddleware } from './refresh.middleware';
import { AuthModule } from 'src/auth/auth.module';
@Module({
providers: [RefreshMiddleware],
imports: [AuthModule],
})
export class MiddlewareModule {}NestMiddleware를 구현하는 클래스라는 점을 제외하면, 작성 방식은 Express.js의 미들웨어와 크게 다르지 않다. 덕분에 코드 자체는 비교적 빠르게 작성할 수 있었다.
다만 Nest.js든 Express.js든 미들웨어를 작성할 때 절대 잊지 말아야 할 부분이 하나 있다. 바로 next()를 호출해 다음 단계로 제어권을 넘겨주어야 한다는 점이다. 이걸 깜빡하면 요청은 그대로 멈춰버린다.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { AuthService } from '../auth/auth.service';
@Injectable()
export class RefreshMiddleware implements NestMiddleware {
constructor(private authService: AuthService) {}
use(req: Request, res: Response, next: NextFunction) {
// 토큰 재발급 로직 처리
next();
}
}미들웨어 적용하기
이렇게 만든 미들웨어를 실제로 적용하려면, 해당 모듈이 NestModule 인터페이스를 구현해야 한다. 그리고 configure 메서드 안에서 MiddlewareConsumer를 사용해 미들웨어를 설정한다.
@Module({
imports: [...],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RefreshMiddleware)
.forRoutes('*');
}
}apply에는 적용할 미들웨어를, forRoutes에는 미들웨어를 적용할 경로를 지정한다. 위처럼 '*'를 지정하면 전역 미들웨어로 동작한다.
함수형 미들웨어
함수형 미들웨어의 가장 큰 장점은 app.use()를 사용해 아주 적은 코드로 전역 미들웨어를 적용할 수 있다는 점이다. 하지만 이 장점은 직접 미들웨어를 구현할 때보다는, 이미 만들어진 외부 미들웨어를 사용할 때 훨씬 크게 체감된다.
대표적인 예가 Express 커뮤니티에서 오랫동안 검증된 미들웨어 라이브러리들이다. 예를 들어 cookie-parser 같은 라이브러리는 Nest.js에서도 그대로 사용할 수 있다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
await app.listen(3000);
}
bootstrap();반면, 애플리케이션 내부 로직과 밀접하게 연관된 미들웨어를 작성해야 한다면 클래스형 미들웨어가 압도적으로 유리하다. 모듈–프로바이더 구조를 그대로 활용할 수 있기 때문에, 다른 서비스나 레포지토리를 주입받아 중복 없이 로직을 구성할 수 있다.
결과적으로 전역 설정이나 외부 라이브러리에는 함수형 미들웨어가 적합하고, 비즈니스 로직이 개입되는 경우에는 클래스형 미들웨어가 훨씬 좋은 선택이라고 생각한다.
더 읽어보기
2025.05.21
14. Redis를 사용한 세션 관리 및 캐싱
새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…
2025.01.30
비즈니스 로직은 어디에 있어야 할까
Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…
2024.12.07
13. Jest 테스트 구성
이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…
2024.11.14
12. GraphQL로 요청 처리
GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...