| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- typescript
- 알고리즘
- react
- mongoose
- nest.js
- Grafana
- js
- WIL
- 생각일기
- javascript
- 코테
- 자바스크립트
- CS
- 리눅스
- 주간회고
- array
- mongo
- next.js
- 기록
- 회고
- MongoDB
- 생각로그
- til
- 피드백
- 생각정리
- 네트워크
- Git
- mysql
- 트러블슈팅
- Java
- Today
- Total
코딩일상
[nest.js] Middleware vs Guards vs Interceptors 본문

해당 포스팅 작성이유:
Middleware를 공부 하면서 헷갈리는 개념이 존재하였다
어떻게 대략적으로 보게되면 Middleware vs Guards vs Interceptors 같은거 아닌가 싶기도하고 서로의 영역을 침범하는것 같기도 언제 어느쪽에 쓰는게 맞는것인지 이론상으로 헷갈리기도 하였다.
이 의문점과 나의 헷갈림을 제대로 구분해서 사용하기 위해서 기록을 남긴다.
1. 핵심 철학의 차이
⭐️⭐️⭐️1.1 설계 원칙 (Design Philosophy)
Middleware의 철학: "HTTP 레벨에서 작동"
- Express/Fastify의 request/response 객체를 직접 다룸
- NestJS의 추상화 레이어 "이전" 단계
Guards의 철학: "접근 제어 (Access Control)"
- "이 요청이 핸들러에 도달할 수 있는가?"를 결정
- ExecutionContext를 통한 메타데이터 기반 결정
Interceptors의 철학: "AOP (Aspect-Oriented Programming)"
- 핸들러 실행 전/후 로직 삽입
- 응답 변환, 부가 기능 추가
⭐️⭐️실행 순서로 이해
클라이언트 요청
↓
[1] Middleware Layer (HTTP 계층)
- Express/Fastify 영역
- req, res 객체 직접 접근
- NestJS 컨텍스트 진입 전
↓
[2] Guards Layer (접근 제어 계층)
- ExecutionContext 사용
- 메타데이터 기반 판단
- true/false 반환으로 통과 여부 결정
↓
[3] Interceptors (before) - AOP 계층
- Observable 스트림 제어
- 요청 데이터 변환
↓
[4] Pipes (검증/변환 계층)
↓
[5] Route Handler (비즈니스 로직)
↓
[6] Interceptors (after) - AOP 계층
- 응답 데이터 변환
- 부가 로직 실행
↓
클라이언트 응답
2. 왜 이렇게 나눠야 할까?
시나리오: 충전소 데이터 조회 API
// POST /api/charging-stations/search
// Body: { location: "Seoul", radius: 5000 }
// Header: Authorization: Bearer eyJhbGc...
이 요청이 처리되는 과정을 각 레이어별로 분석해봅시다.
2.1 Middleware 단계 - "HTTP 프로토콜 레벨 처리"
// ✅ Middleware가 적합한 이유
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 1. HTTP 요청의 원시 정보 접근
const requestId = uuidv4();
const startTime = Date.now();
// 2. Request 객체에 직접 데이터 추가 (모든 후속 레이어에서 사용 가능)
req['requestId'] = requestId;
req['startTime'] = startTime;
// 3. Response 이벤트 리스너 등록 (HTTP 응답 완료 시점 감지)
res.on('finish', () => {
const duration = Date.now() - startTime;
console.log(`[${requestId}] ${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`);
});
next();
}
}
왜 Middleware에서?
- res.on('finish'): Response 이벤트 리스너는 Express의 기능
- HTTP 헤더 조작: res.setHeader() 같은 저수준 작업
- 요청 전처리: Body 파싱, 압축 해제 등 HTTP 프로토콜 레벨 작업
- NestJS 컨텍스트 무관: 어떤 컨트롤러/메서드인지 알 필요 없음
// ❌ Guards에서 로깅하면 안 되는 이유
@Injectable()
export class LoggingGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// 문제 1: res.on('finish') 사용 불가 (Response 객체 접근은 가능하지만 부자연스러움)
// 문제 2: Guards는 "접근 허용 여부"를 결정하는 곳, 로깅은 책임 위반
// 문제 3: Guards는 특정 핸들러에 바인딩, 전역 HTTP 로깅에 부적합
console.log('Request received'); // 이건 Guards의 역할이 아님
return true;
}
}
2.2 Guards 단계 - "인증/인가 결정"
// ✅ Guards가 적합한 이유
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector, // 메타데이터 접근
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. 메타데이터로 공개 API 확인
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true; // 공개 API는 통과
}
// 2. 토큰 검증
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Token required');
}
try {
const payload = await this.jwtService.verifyAsync(token);
request['user'] = payload;
return true; // 인증 성공 → 핸들러 실행 허용
} catch {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
왜 Guards에서?
// Guards의 핵심 기능: 메타데이터 기반 결정
@Controller('charging-stations')
export class ChargingStationsController {
@Public() // 메타데이터: 이 엔드포인트는 공개
@Get('public-list')
getPublicStations() {
return this.service.getPublicStations();
}
@Roles('admin') // 메타데이터: 관리자만 접근 가능
@Post()
createStation() {
return this.service.createStation();
}
}
// Guards는 이 메타데이터를 읽어서 결정
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true; // 역할 지정 안 됨 = 모두 접근 가능
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
❌ Middleware에서 인증하면 안 되는 이유
// Middleware는 메타데이터에 접근할 수 없음!
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 문제: 이 엔드포인트가 @Public()인지 알 수 없음
// 문제: 이 엔드포인트가 @Roles('admin')인지 알 수 없음
// 모든 요청을 동일하게 처리할 수밖에 없음
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'Unauthorized' });
}
next();
}
}
2.3 Interceptors 단계 - "횡단 관심사 (Cross-Cutting Concerns)"
// ✅ Interceptors가 적합한 이유
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
const request = context.switchToHttp().getRequest();
const requestId = request['requestId'];
return next.handle().pipe(
map(data => ({
success: true,
requestId,
timestamp: new Date().toISOString(),
data, // 핸들러에서 반환한 실제 데이터
})),
catchError(error => {
// 에러도 일관된 형식으로 변환
return throwError(() => ({
success: false,
requestId,
timestamp: new Date().toISOString(),
error: error.message,
}));
}),
);
}
}
// 핸들러 반환값
// Before: { stations: [...] }
// After: {
// success: true,
// requestId: "uuid",
// timestamp: "2024-12-13T...",
// data: { stations: [...] }
// }
왜 Interceptors에서?
// 1. Observable 스트림 제어 가능
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const cacheKey = `${request.method}:${request.url}`;
// 캐시 확인
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return of(cached); // 핸들러 실행하지 않고 캐시 반환
}
// 핸들러 실행 후 결과 캐싱
return next.handle().pipe(
tap(data => {
this.cacheManager.set(cacheKey, data, 60000); // 1분 캐싱
}),
);
}
}
❌ Guards에서 응답 변환하면 안 되는 이유
// Guards는 boolean만 반환 가능
@Injectable()
export class TransformGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// 문제: Guards는 핸들러의 반환값에 접근할 수 없음
// 문제: true/false만 반환 가능, 데이터 변환 불가
// 문제: "접근 허용 여부"가 아닌 다른 일을 하면 책임 위반
return true;
}
}
3. 질문 & 답변
Q1: "왜 인증을 Guards에서 하나요? Middleware에서 해도 되지 않나요?"
A:
// 실제 요구사항을 생각해보세요
// 케이스 1: 공개 API
@Public()
@Get('health')
healthCheck() {
return { status: 'ok' };
}
// 케이스 2: 인증 필요 API
@Get('profile')
getProfile(@User() user) {
return user;
}
// 케이스 3: 관리자만 가능
@Roles('admin')
@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.service.delete(id);
}
Middleware로 구현하면:
// ❌ 문제점
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 문제 1: 어떤 엔드포인트가 @Public()인지 모름
// 해결방법? URL 하드코딩?
if (req.url === '/health') {
return next(); // 하드코딩... 유지보수 지옥
}
// 문제 2: 역할 검증 불가
// @Roles 메타데이터에 접근할 수 없음
const token = req.headers.authorization;
if (!token) {
return res.status(401).send('Unauthorized');
}
next();
}
}
Guards로 구현하면:
// ✅ 올바른 방법
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 메타데이터로 동적 판단
const isPublic = this.reflector.get('isPublic', context.getHandler());
if (isPublic) return true;
const requiredRoles = this.reflector.get('roles', context.getHandler());
// 역할 검증 로직...
return true;
}
}
핵심 답변:
"Middleware는 HTTP 레벨에서 작동하므로 라우트 메타데이터에 접근할 수 없습니다. 따라서 @Public(), @Roles() 같은 데코레이터 기반 접근 제어가 불가능합니다. Guards는 ExecutionContext와 Reflector를 통해 메타데이터에 접근할 수 있어, 유연하고 선언적인 접근 제어가 가능합니다."
Q2: "로깅은 왜 Middleware에서 하나요? Interceptor에서 해도 되지 않나요?"
A:
// HTTP 응답 완료 시점 감지가 필요한 경우
// ✅ Middleware (올바름)
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
// Response의 finish 이벤트: 실제 클라이언트에게 응답이 전송된 시점
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next();
}
}
// ❌ Interceptor (문제 있음)
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
// 문제: 핸들러 완료 시점이지, HTTP 응답 완료 시점이 아님
// 큰 파일 전송 시 실제 전송 시간이 누락됨
console.log(`Duration: ${duration}ms`);
}),
);
}
}
실제 차이점:
// 컨트롤러에서 큰 파일 스트리밍
@Get('large-file')
getLargeFile(@Res() res: Response) {
const stream = fs.createReadStream('large-file.zip');
stream.pipe(res);
// 핸들러는 여기서 즉시 완료
// 하지만 실제 파일 전송은 계속 진행 중
}
// Middleware: 실제 전송 완료 시간 측정 (예: 5000ms)
// Interceptor: 핸들러 완료 시간만 측정 (예: 10ms)
핵심 답변:
"Middleware는 Response 객체의 이벤트 리스너를 등록할 수 있어 실제 HTTP 응답 완료 시점을 감지할 수 있습니다. Interceptor는 핸들러 완료 시점만 알 수 있어, 스트리밍이나 대용량 데이터 전송 시 정확한 측정이 불가능합니다. 또한 Middleware는 NestJS 컨텍스트 진입 전에 실행되므로, NestJS 자체의 에러도 포착할 수 있습니다."
Q3: "응답 변환은 왜 Interceptor에서 하나요? Middleware에서 못 하나요?"
A:
// ❌ Middleware에서 응답 변환 시도
@Injectable()
export class TransformMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 문제: 핸들러의 반환값에 접근할 방법이 없음!
// res.json()을 오버라이드? 너무 복잡하고 위험함
const originalJson = res.json.bind(res);
res.json = function(data) {
return originalJson({
success: true,
data,
timestamp: new Date(),
});
};
next();
}
}
// ✅ Interceptor에서 응답 변환
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date(),
})),
);
}
}
RxJS Observable의 힘:
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // 5초 타임아웃
catchError(err => {
if (err.name === 'TimeoutError') {
throw new RequestTimeoutException();
}
throw err;
}),
);
}
}
// Middleware에서는 이런 제어가 불가능
핵심 답변:
"Interceptor는 RxJS Observable 스트림으로 핸들러 실행을 감싸므로, map, tap, catchError, timeout 등 강력한 스트림 연산자를 사용할 수 있습니다. Middleware는 핸들러 반환값에 접근하려면 Response 객체를 직접 조작해야 하는데, 이는 NestJS의 추상화를 깨뜨리고 복잡도를 높입니다."
Q4: "ExecutionContext는 뭐고, 왜 Guards와 Interceptors만 사용하나요?"
A:
// ExecutionContext의 구조
interface ExecutionContext {
getClass(): Type<any>; // 컨트롤러 클래스
getHandler(): Function; // 핸들러 메서드
getArgs(): any[]; // 핸들러 인자들
getType(): string; // 'http' | 'ws' | 'rpc'
switchToHttp(): HttpArgumentsHost; // HTTP 컨텍스트로 전환
switchToWs(): WsArgumentsHost; // WebSocket 컨텍스트로 전환
switchToRpc(): RpcArgumentsHost; // Microservice 컨텍스트로 전환
}
실제 활용:
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const className = context.getClass().name;
const handlerName = context.getHandler().name;
console.log(`Executing: ${className}.${handlerName}()`);
// Output: "Executing: ChargingStationsController.search()"
// HTTP 요청 정보 접근
const request = context.switchToHttp().getRequest();
console.log(`User: ${request.user?.id}`);
return next.handle();
}
}
Middleware는 왜 못 쓰나요?
// Middleware 실행 시점
클라이언트 → Middleware → [NestJS 라우팅] → Guards → Interceptors → Handler
// Middleware 시점에는:
// - 어떤 컨트롤러가 처리할지 모름
// - 어떤 핸들러가 실행될지 모름
// - 메타데이터 접근 불가
// → ExecutionContext가 아직 생성 안 됨!
// Guards/Interceptors 시점에는:
// - 라우팅 완료
// - ExecutionContext 생성됨
// - 모든 메타데이터 접근 가능
핵심 답변:
"ExecutionContext는 현재 실행 중인 핸들러에 대한 모든 정보를 담고 있는 객체입니다. Middleware는 라우팅 전에 실행되므로 아직 어떤 핸들러가 실행될지 모르는 상태입니다. 따라서 ExecutionContext가 존재하지 않습니다. Guards와 Interceptors는 라우팅 후에 실행되므로 ExecutionContext에 접근할 수 있고, 이를 통해 메타데이터 기반 결정이 가능합니다."
질문: "Middleware, Guards, Interceptors의 차이점은?"
답변 구조:
1. 실행 순서와 위치
2. 접근 가능한 객체
3. 주요 용도
4. 실제 예시
모범 답변:
"세 가지는 실행 시점과 책임이 다릅니다.
Middleware는 가장 먼저 실행되며 HTTP 프로토콜 레벨에서 작동합니다. Express의 req, res 객체에 직접 접근하여 로깅, CORS, 압축 등 HTTP 관련 작업을 처리합니다. 하지만 라우팅 전에 실행되므로 어떤 핸들러가 실행될지 모르고, 메타데이터에 접근할 수 없습니다.
Guards는 라우팅 후 실행되며 접근 제어를 담당합니다. ExecutionContext와 Reflector를 통해 메타데이터에 접근할 수 있어, @Public(), @Roles() 같은 데코레이터 기반 인증/인가가 가능합니다. true/false를 반환하여 핸들러 실행 여부를 결정합니다.
Interceptors는 핸들러 전후로 실행되며 AOP 패턴을 구현합니다. RxJS Observable로 핸들러 실행을 감싸므로 응답 변환, 캐싱, 타임아웃 등 부가 기능을 추가할 수 있습니다. 핸들러의 반환값에 접근하여 데이터 변환이 가능합니다.
실무에서는 PMGrow의 IoT 시스템에서 Middleware로 40,000건의 일일 데이터 요청을 로깅하고, Guards로 차량 소유권을 검증하며, Interceptors로 API 응답을 표준화했습니다."
'개발 공부 > nest.js' 카테고리의 다른 글
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[exception-filters] (1) | 2025.12.16 |
|---|---|
| [nest.js] NestJS 제공 라이브러리 및 데코레이터 완벽 정리 (0) | 2025.12.14 |
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Middleware] (0) | 2025.12.14 |
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Modules] (0) | 2025.12.11 |
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[providers]Constructor-based Injection vs Property-based Injection (0) | 2025.12.11 |
