ExecutionContext란?
NestJS의 ExecutionContext는 Guard, Interceptor, Filter 등 크로스커팅 관심사에서 현재 요청의 모든 메타데이터에 접근하는 핵심 객체다. 어떤 Controller의 어떤 메서드가 호출되는지, HTTP인지 WebSocket인지 gRPC인지, 커스텀 메타데이터(Reflector)가 무엇인지 — 이 모든 것을 ExecutionContext가 제공한다.
이 글에서는 ExecutionContext의 내부 구조, ArgumentsHost와의 관계, Reflector 메타데이터 활용, 프로토콜별 분기 처리, 실전 패턴까지 심화 수준으로 다룬다.
ArgumentsHost: 기반 클래스
ExecutionContext는 ArgumentsHost를 상속한다. ArgumentsHost는 현재 요청의 프로토콜별 인자에 접근하는 저수준 API다.
// ArgumentsHost 인터페이스
interface ArgumentsHost {
getArgs(): any[]; // 원시 인자 배열
getArgByIndex(index: number): any;
getType(): string; // 'http' | 'ws' | 'rpc'
// 프로토콜별 헬퍼
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
switchToRpc(): RpcArgumentsHost;
}
// ExecutionContext 인터페이스 (ArgumentsHost 확장)
interface ExecutionContext extends ArgumentsHost {
getClass(): Type; // Controller 클래스
getHandler(): Function; // 핸들러 메서드
}
프로토콜별 컨텍스트 접근
// HTTP 요청
@Injectable()
export class HttpContextInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
if (context.getType() === 'http') {
const httpCtx = context.switchToHttp();
const request = httpCtx.getRequest<Request>();
const response = httpCtx.getResponse<Response>();
console.log(`${request.method} ${request.url}`);
console.log('Headers:', request.headers);
console.log('Body:', request.body);
console.log('Params:', request.params);
console.log('Query:', request.query);
}
return next.handle();
}
}
// WebSocket
if (context.getType() === 'ws') {
const wsCtx = context.switchToWs();
const client = wsCtx.getClient<Socket>();
const data = wsCtx.getData();
console.log('WS client:', client.id);
console.log('WS data:', data);
}
// gRPC / Microservice
if (context.getType() === 'rpc') {
const rpcCtx = context.switchToRpc();
const data = rpcCtx.getData();
const metadata = rpcCtx.getContext(); // gRPC Metadata
}
getClass()와 getHandler(): Controller 메타데이터
@Injectable()
export class AuditInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
// 어떤 Controller의 어떤 메서드인지 알 수 있다
const controllerClass = context.getClass();
const handlerMethod = context.getHandler();
const controllerName = controllerClass.name; // 'OrderController'
const methodName = handlerMethod.name; // 'createOrder'
console.log(`[Audit] ${controllerName}.${methodName}`);
return next.handle();
}
}
Reflector: 커스텀 메타데이터의 핵심
Reflector는 SetMetadata로 설정한 커스텀 메타데이터를 ExecutionContext에서 읽는 유틸리티다. Guard, Interceptor에서 어노테이션 기반 제어를 구현하는 핵심이다.
// 1. 커스텀 데코레이터 정의
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
export const THROTTLE_KEY = 'throttle';
export const Throttle = (limit: number, ttl: number) =>
SetMetadata(THROTTLE_KEY, { limit, ttl });
// 2. Controller에서 사용
@Controller('orders')
export class OrderController {
@Post()
@Roles('ADMIN', 'MANAGER')
@Throttle(10, 60)
createOrder(@Body() dto: CreateOrderDto) { ... }
@Get('public-catalog')
@Public()
getCatalog() { ... }
}
// 3. Guard에서 Reflector로 읽기
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// getAllAndOverride: 메서드 메타데이터 우선, 없으면 클래스 메타데이터
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true; // 역할 제한 없음
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some(role => user.roles?.includes(role));
}
}
Reflector의 3가지 조회 메서드
| 메서드 | 동작 | 사용 시점 |
|---|---|---|
get(key, target) |
단일 타겟에서 조회 | 메서드 또는 클래스만 확인 |
getAllAndOverride(key, targets[]) |
첫 번째 non-undefined 값 반환 | 메서드 우선, 클래스 폴백 |
getAllAndMerge(key, targets[]) |
모든 타겟 값을 병합(배열) | 클래스 + 메서드 역할 합산 |
// getAllAndOverride 예시
@Roles('USER') // 클래스 레벨
@Controller('orders')
export class OrderController {
@Roles('ADMIN') // 메서드 레벨 — 이것이 우선
@Delete(':id')
deleteOrder() { ... }
}
// getAllAndOverride → ['ADMIN'] (메서드가 덮어씀)
// getAllAndMerge → ['ADMIN', 'USER'] (합산)
실전 패턴: 프로토콜 불문 범용 Guard
@Injectable()
export class UniversalAuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Public 엔드포인트 체크
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) return true;
// 프로토콜별 토큰 추출
let token: string;
switch (context.getType()) {
case 'http': {
const req = context.switchToHttp().getRequest();
token = req.headers.authorization?.replace('Bearer ', '');
break;
}
case 'ws': {
const client = context.switchToWs().getClient();
token = client.handshake?.auth?.token;
break;
}
case 'rpc': {
const metadata = context.switchToRpc().getContext();
token = metadata.get('authorization')?.[0];
break;
}
}
if (!token) return false;
try {
const user = await this.authService.validateToken(token);
// 프로토콜별로 user 정보 주입
this.attachUser(context, user);
return true;
} catch {
return false;
}
}
private attachUser(context: ExecutionContext, user: any) {
switch (context.getType()) {
case 'http':
context.switchToHttp().getRequest().user = user;
break;
case 'ws':
context.switchToWs().getClient().user = user;
break;
}
}
}
실전 패턴: 조건부 캐시 Interceptor
export const CACHE_TTL_KEY = 'cacheTtl';
export const CacheTTL = (seconds: number) =>
SetMetadata(CACHE_TTL_KEY, seconds);
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
constructor(
private reflector: Reflector,
private cacheManager: Cache,
) {}
async intercept(context: ExecutionContext, next: CallHandler) {
const ttl = this.reflector.get<number>(
CACHE_TTL_KEY,
context.getHandler(),
);
// @CacheTTL 없으면 캐시 안 함
if (!ttl) return next.handle();
const request = context.switchToHttp().getRequest();
const cacheKey = `${context.getClass().name}:${context.getHandler().name}:${request.url}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) return of(cached);
return next.handle().pipe(
tap(async (data) => {
await this.cacheManager.set(cacheKey, data, ttl * 1000);
}),
);
}
}
주의점과 안티패턴
| 안티패턴 | 문제점 | 해결책 |
|---|---|---|
| getType() 체크 없이 switchToHttp() | WebSocket/gRPC 요청에서 런타임 에러 | 항상 getType() 분기 처리 |
| getAllAndOverride vs getAllAndMerge 혼동 | 의도와 다른 메타데이터 조회 | 덮어쓰기=Override, 합산=Merge |
| Guard에서 request 직접 수정 | 테스트 어려움, 부수 효과 | 최소한의 user 주입만, 로직은 서비스로 |
| Reflector 없이 Reflect.getMetadata 직접 사용 | NestJS 내부 API에 의존 | 반드시 Reflector 사용 |
마무리
ExecutionContext와 Reflector는 NestJS의 크로스커팅 관심사 메커니즘의 근간이다. Guard, Interceptor, Filter가 모두 ExecutionContext를 통해 요청 정보와 커스텀 메타데이터에 접근한다. 프로토콜 불문 범용 Guard, 어노테이션 기반 캐시/권한 제어 등 고급 패턴의 기반이 된다. NestJS Guard에서의 활용과 Interceptor RxJS 패턴을 함께 이해하면 NestJS의 요청 파이프라인을 완전히 제어할 수 있다.