Custom Decorator 만들기 — createParamDecorator의 내부 (6편)
5편에서 요청이 Guard → Interceptor → Pipe를 거쳐 컨트롤러에 도달하는 과정을 봤습니다. NestJS를 쓰다 보면 @CurrentUser(), @Roles('admin') 같은 커스텀 데코레이터를 만들어 쓰게 됩니다. 이번에는 createParamDecorator와 SetMetadata가 내부에서 어떻게 동작하는지 추적해보겠습니다.
NestJS에서 데코레이터를 만드는 두 가지 방법
NestJS에서 커스텀 데코레이터를 만드는 방법은 크게 두 가지입니다.
| 방법 | 용도 | 대표 예시 |
|---|---|---|
| createParamDecorator | 파라미터에서 값을 추출 | @CurrentUser(), @ClientIp() |
| SetMetadata | 메서드/클래스에 메타데이터를 부착 | @Roles('admin'), @Public() |
두 방식은 목적이 다릅니다. createParamDecorator는 "요청에서 특정 값을 꺼내서 파라미터로 넘기는 것"이고, SetMetadata는 "이 메서드에 조건/설정을 붙여두고 Guard나 Interceptor에서 읽는 것"입니다. 하나씩 추적해보겠습니다.
createParamDecorator 소스코드 추적
먼저, 가장 많이 쓰이는 커스텀 데코레이터 패턴입니다.
// 실전에서 가장 흔한 사용법
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// 컨트롤러에서 사용
@Get('me')
getMe(@CurrentUser() user: User) {
return user; // Guard에서 부착한 request.user가 여기로 옵니다
}
createParamDecorator에 팩토리 함수를 넘기면 파라미터 데코레이터가 만들어집니다. 이 팩토리 함수는 요청 시점에 실행되어 값을 추출합니다. 그런데 내부에서는 어떤 일이 일어나는 걸까요?
소스코드: create-route-param-metadata.decorator.ts
packages/common/decorators/http/create-route-param-metadata.decorator.ts
// packages/common/decorators/http/create-route-param-metadata.decorator.ts
import { uid } from 'uid';
import { ROUTE_ARGS_METADATA } from '../../constants';
import { assignCustomParameterMetadata } from '../../utils/assign-custom-metadata.util';
export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
factory: CustomParamFactory<FactoryData, FactoryOutput>,
enhancers: ParamDecoratorEnhancer[] = [],
): (
...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
) => ParameterDecorator {
const paramtype = uid(21); // 고유 ID 생성
return (
data?,
...pipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
): ParameterDecorator =>
(target, key, index) => {
const args =
Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key!) || {};
const isPipe = (pipe: any) =>
pipe &&
((isFunction(pipe) &&
pipe.prototype &&
isFunction(pipe.prototype.transform)) ||
isFunction(pipe.transform));
const hasParamData = isNil(data) || !isPipe(data);
const paramData = hasParamData ? (data as any) : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
Reflect.defineMetadata(
ROUTE_ARGS_METADATA,
assignCustomParameterMetadata(
args,
paramtype,
index,
factory,
paramData,
...(paramPipes as PipeTransform[]),
),
target.constructor,
key!,
);
enhancers.forEach(fn => fn(target, key, index));
};
}
코드가 길지만 핵심은 세 가지입니다.
첫째, uid(21)로 고유한 paramtype을 생성합니다. 이 ID가 이 커스텀 데코레이터를 식별하는 키가 됩니다. @Body(), @Param(), @Query() 같은 내장 데코레이터는 enum 값을 paramtype으로 쓰지만, 커스텀 데코레이터는 랜덤 ID를 씁니다.
둘째, Reflect.defineMetadata(ROUTE_ARGS_METADATA, ...)로 메타데이터를 저장합니다. 2편에서 본 Reflect.metadata와 같은 메커니즘입니다. 데코레이터가 실행되는 시점(클래스 로딩 시)에 메타데이터를 저장하고, 요청이 올 때 NestJS가 이 메타데이터를 읽어서 팩토리 함수를 실행합니다.
셋째, factory 함수를 메타데이터에 함께 저장합니다. assignCustomParameterMetadata를 보면 이것이 명확해집니다.
assignCustomParameterMetadata
// packages/common/utils/assign-custom-metadata.util.ts
export function assignCustomParameterMetadata(
args: Record<number, RouteParamMetadata>,
paramtype: number | string,
index: number,
factory: CustomParamFactory,
data?: ParamData,
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
return {
...args,
[`${paramtype}${CUSTOM_ROUTE_ARGS_METADATA}:${index}`]: {
index,
factory,
data,
pipes,
},
};
}
📎 packages/common/utils/assign-custom-metadata.util.ts — 메타데이터의 키는 `${paramtype}${CUSTOM_ROUTE_ARGS_METADATA}:${index}` 형태입니다. 예를 들어 "abc123__custom__:0" 같은 문자열이 됩니다. 값으로는 { index, factory, data, pipes }를 저장합니다.
이것을 다이어그램으로 그려보면 이렇습니다.
클래스 로딩 시 (데코레이터 실행)
─────────────────────────────
@CurrentUser() user: User
│
▼
createParamDecorator가 반환한 ParameterDecorator 실행
│
▼
Reflect.defineMetadata(ROUTE_ARGS_METADATA, {
"abc123__custom__:0": {
index: 0,
factory: (data, ctx) => ctx.switchToHttp().getRequest().user,
data: undefined,
pipes: []
}
}, Controller, 'getMe')
요청 시 (RouterExecutionContext)
─────────────────────────────
1. ROUTE_ARGS_METADATA에서 메타데이터 읽기
2. factory 함수에 ExecutionContext 전달하여 실행
3. 반환값을 Pipe에 통과시킨 후 컨트롤러 메서드 파라미터로 전달
핵심은 "선언"과 "실행"의 분리입니다. 데코레이터는 클래스가 로드될 때 메타데이터를 저장할 뿐이고, 실제 값 추출은 요청이 올 때 일어납니다. 5편에서 본 PipesConsumer가 이 값을 처리하는 것입니다.
ExecutionContext 깊이 파기
커스텀 데코레이터의 팩토리 함수는 두 번째 인자로 ExecutionContext를 받습니다. 이것이 무엇인지 소스를 보겠습니다.
ExecutionContextHost 소스코드
packages/core/helpers/execution-context-host.ts
// packages/core/helpers/execution-context-host.ts
export class ExecutionContextHost implements ExecutionContext {
private contextType = 'http';
constructor(
private readonly args: any[],
private readonly constructorRef: Type<any> | null = null,
private readonly handler: Function | null = null,
) {}
setType<TContext extends string = ContextType>(type: TContext) {
type && (this.contextType = type);
}
getType<TContext extends string = ContextType>(): TContext {
return this.contextType as TContext;
}
getClass<T = any>(): Type<T> {
return this.constructorRef!;
}
getHandler(): Function {
return this.handler!;
}
getArgs<T extends Array<any> = any[]>(): T {
return this.args as T;
}
getArgByIndex<T = any>(index: number): T {
return this.args[index] as T;
}
switchToHttp(): HttpArgumentsHost {
return Object.assign(this, {
getRequest: () => this.getArgByIndex(0),
getResponse: () => this.getArgByIndex(1),
getNext: () => this.getArgByIndex(2),
});
}
switchToRpc(): RpcArgumentsHost {
return Object.assign(this, {
getData: () => this.getArgByIndex(0),
getContext: () => this.getArgByIndex(1),
});
}
switchToWs(): WsArgumentsHost {
return Object.assign(this, {
getClient: () => this.getArgByIndex(0),
getData: () => this.getArgByIndex(1),
getPattern: () => this.getArgByIndex(this.getArgs().length - 1),
});
}
}
5편에서 Guard와 Interceptor가 ExecutionContext를 받는다고 했습니다. 그 실체가 이 ExecutionContextHost입니다. 생성자를 보면 세 가지를 받습니다.
| 파라미터 | 역할 | HTTP에서의 값 |
|---|---|---|
args |
원본 인자 배열 | [request, response, next] |
constructorRef |
컨트롤러 클래스 | BlogPostsController |
handler |
핸들러 메서드 | BlogPostsController.findOne |
switchToHttp()의 정체
switchToHttp()는 Object.assign(this, ...)으로 자기 자신에 getRequest(), getResponse(), getNext() 메서드를 덧붙입니다. getRequest()는 결국 this.args[0]을 반환하는 것뿐입니다.
이 구조 덕분에 하나의 ExecutionContext가 HTTP, WebSocket, Microservice 세 가지 전송 계층을 모두 지원할 수 있습니다. switchToHttp()를 호출하면 HTTP 관점의 메서드가 추가되고, switchToWs()를 호출하면 WebSocket 관점의 메서드가 추가됩니다.
getClass()와 getHandler() — 왜 중요한가
// Guard에서 "어떤 컨트롤러의 어떤 메서드인지" 알 수 있습니다
const controller = context.getClass(); // BlogPostsController
const handler = context.getHandler(); // findOne 메서드
// 이 정보로 메타데이터를 읽을 수 있습니다
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
handler, // 메서드 레벨 메타데이터 우선
controller, // 없으면 컨트롤러 레벨 확인
]);
이것이 Guard에서 @Roles() 데코레이터의 값을 읽을 수 있는 이유입니다. getHandler()가 메서드 참조를 반환하고, Reflector가 그 메서드에 붙은 메타데이터를 읽습니다. 5편에서 "Middleware는 ExecutionContext를 받지 않는다"고 했던 이유가 여기에 있습니다.
ArgumentsHost와 ExecutionContext의 차이
| 인터페이스 | 사용 위치 | getClass/getHandler | 설명 |
|---|---|---|---|
| ArgumentsHost | ExceptionFilter | ❌ | 원본 인자(req, res)만 접근 가능 |
| ExecutionContext | Guard, Interceptor | ⭕ | ArgumentsHost를 확장 — 컨트롤러/메서드 정보까지 |
ExecutionContext는 ArgumentsHost를 상속합니다. ArgumentsHost는 switchToHttp()로 req/res에 접근할 수 있지만, getClass()/getHandler()는 없습니다. ExceptionFilter가 ArgumentsHost를 받는 이유는, 에러가 어디서든 발생할 수 있어서 컨트롤러 정보가 항상 존재한다고 보장할 수 없기 때문입니다.
SetMetadata 소스코드 추적
이제 두 번째 방법, SetMetadata를 보겠습니다.
실전 사용법
// @Roles() 데코레이터 정의
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// 컨트롤러에서 사용
@Roles('admin')
@UseGuards(RolesGuard)
@Delete(':slug')
remove(@Param('slug') slug: string) {
return this.service.remove(slug);
}
소스코드: set-metadata.decorator.ts
packages/common/decorators/core/set-metadata.decorator.ts
// packages/common/decorators/core/set-metadata.decorator.ts
export function SetMetadata<K = string, V = any>(
metadataKey: K,
metadataValue: V,
): CustomDecorator<K> {
const decoratorFactory = (
target: object,
key?: any,
descriptor?: any,
) => {
if (descriptor) {
// 메서드 데코레이터로 사용된 경우
Reflect.defineMetadata(metadataKey as any, metadataValue, descriptor.value);
return descriptor;
}
// 클래스 데코레이터로 사용된 경우
Reflect.defineMetadata(metadataKey as any, metadataValue, target);
return target;
};
decoratorFactory.KEY = metadataKey;
return decoratorFactory as CustomDecorator<K>;
}
createParamDecorator보다 훨씬 단순합니다. Reflect.defineMetadata(key, value, target) — 이것이 전부입니다.
@Roles('admin')을 메서드에 붙이면, 그 메서드의 메타데이터에 'roles' → ['admin']이 저장됩니다. 클래스에 붙이면 클래스에 저장됩니다.
하지만 SetMetadata만으로는 아무 일도 일어나지 않습니다. 메타데이터를 저장만 할 뿐, 그것을 읽어서 행동하는 것은 Guard나 Interceptor의 몫입니다.
Reflector — 메타데이터를 읽는 서비스
SetMetadata로 저장한 메타데이터를 읽을 때 사용하는 것이 Reflector입니다.
packages/core/services/reflector.service.ts
// packages/core/services/reflector.service.ts (핵심 메서드들)
@Injectable()
export class Reflector {
// 단일 타겟에서 메타데이터 읽기
public get<TResult = any, TKey = any>(
metadataKeyOrDecorator: TKey,
target: Type<any> | Function,
): TResult {
const metadataKey =
(metadataKeyOrDecorator as ReflectableDecorator<unknown>).KEY ??
metadataKeyOrDecorator;
return Reflect.getMetadata(metadataKey, target);
}
// 여러 타겟에서 읽어서 첫 번째 유효한 값 반환
public getAllAndOverride<TResult = any, TKey = any>(
metadataKeyOrDecorator: TKey,
targets: (Type<any> | Function)[],
): TResult | undefined {
for (const target of targets) {
const result = this.get(metadataKeyOrDecorator, target);
if (result !== undefined) {
return result; // 첫 번째 유효한 값 반환
}
}
return undefined;
}
// 여러 타겟에서 읽어서 합치기
public getAllAndMerge<TResult extends any[] | object = any[], TKey = any>(
metadataKeyOrDecorator: TKey,
targets: (Type<any> | Function)[],
): TResult {
// 배열이면 concat, 객체면 spread로 합침
}
}
세 가지 메서드가 있지만, 실전에서 가장 많이 쓰이는 것은 getAllAndOverride()입니다.
getAllAndOverride의 의미
// RolesGuard에서의 사용
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), // 1순위: 메서드 레벨
context.getClass(), // 2순위: 컨트롤러 레벨
]);
getAllAndOverride는 배열의 앞에서부터 찾아서 첫 번째로 발견된 값을 반환합니다. 메서드 레벨에 @Roles('admin')이 있으면 그것을 쓰고, 없으면 컨트롤러 레벨의 @Roles()를 확인합니다. "Override" 패턴입니다.
반면 getAllAndMerge는 모든 레벨의 값을 합칩니다. 컨트롤러에 @Roles('user'), 메서드에 @Roles('admin')이 있으면 ['user', 'admin']이 됩니다.
실전: RolesGuard 전체 구현
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {} // DI로 주입
canActivate(context: ExecutionContext): boolean {
// 1. 메타데이터 읽기 — SetMetadata로 저장한 roles
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// 2. roles 메타데이터가 없으면 → 제한 없음, 통과
if (!requiredRoles) {
return true;
}
// 3. Guard에서 부착한 request.user 확인
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
데이터 흐름을 다이어그램으로 그리면 이렇습니다.
빌드 타임 (클래스 로딩)
──────────────────────
@Roles('admin') → SetMetadata('roles', ['admin'])
│
▼
Reflect.defineMetadata('roles', ['admin'], handler)
요청 시 (Guard 실행)
──────────────────────
RolesGuard.canActivate(context)
│
▼
this.reflector.getAllAndOverride('roles', [handler, class])
│
▼
Reflect.getMetadata('roles', handler) → ['admin']
│
▼
user.roles.includes('admin') → true/false
SetMetadata는 "선언"이고, Reflector는 "읽기"이고, Guard는 "판단"입니다. 세 가지가 하나의 흐름을 이룹니다.
데코레이터 합성 — applyDecorators()
실무에서는 여러 데코레이터를 조합해서 하나로 만드는 경우가 많습니다.
// 매번 이렇게 세 개를 붙이는 대신...
@UseGuards(AuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth()
@Delete(':slug')
remove() { ... }
// 하나로 합칠 수 있습니다
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
);
}
// 사용
@Auth('admin')
@Delete(':slug')
remove() { ... }
applyDecorators()는 여러 데코레이터를 받아서 하나의 데코레이터로 합쳐줍니다. 내부적으로는 받은 데코레이터들을 순서대로 실행할 뿐입니다. 복잡한 마법은 없지만, 코드 중복을 크게 줄여줍니다.
주의할 점이 있습니다. applyDecorators()는 클래스 데코레이터와 메서드 데코레이터만 합칠 수 있습니다. 파라미터 데코레이터(createParamDecorator로 만든 것)는 합칠 수 없습니다. 파라미터 데코레이터는 (target, key, index)를 받고, 메서드 데코레이터는 (target, key, descriptor)를 받기 때문입니다.
파라미터 데코레이터 vs 메타데이터 데코레이터 비교
| 구분 | createParamDecorator | SetMetadata |
|---|---|---|
| 용도 | 요청에서 값 추출/변환 | 조건/설정 메타데이터 저장 |
| 데코레이터 종류 | 파라미터 데코레이터 | 메서드/클래스 데코레이터 |
| 저장하는 것 | ROUTE_ARGS_METADATA + factory 함수 | 임의의 키에 임의의 값 |
| 실행 시점 | 요청 시 — Pipe 단계에서 factory 실행 | 저장은 빌드 타임, 읽기는 요청 시 |
| 읽는 쪽 | NestJS 내부 (RouterExecutionContext) | 개발자가 Guard/Interceptor에서 Reflector로 |
| Pipe 적용 | ⭕ 자동 적용 (글로벌 + 인라인 Pipe) | ❌ 해당 없음 |
| 예시 | @CurrentUser(), @ClientIp() | @Roles(), @Public(), @Throttle() |
선택 기준은 단순합니다. "요청에서 값을 꺼내야 하면" createParamDecorator, "조건을 붙여야 하면" SetMetadata입니다.
@Body(), @Param(), @Query()도 같은 원리
NestJS의 내장 파라미터 데코레이터도 원리는 동일합니다. 다만 createParamDecorator를 쓰는 대신, 직접 ROUTE_ARGS_METADATA에 enum 값을 저장합니다.
그리고 요청 시에는 RouteParamsFactory가 enum에 따라 값을 추출합니다.
// packages/core/router/route-params-factory.ts
export class RouteParamsFactory implements IRouteParamsFactory {
public exchangeKeyForValue(
key: RouteParamtypes | string,
data: string,
{ req, res, next },
) {
switch (key) {
case RouteParamtypes.REQUEST:
return req;
case RouteParamtypes.RESPONSE:
return res;
case RouteParamtypes.BODY:
return data && req.body ? req.body[data] : req.body;
case RouteParamtypes.PARAM:
return data ? req.params[data] : req.params;
case RouteParamtypes.QUERY:
return data ? req.query[data] : req.query;
case RouteParamtypes.HEADERS:
return data ? req.headers[data.toLowerCase()] : req.headers;
case RouteParamtypes.SESSION:
return req.session;
case RouteParamtypes.IP:
return req.ip;
case RouteParamtypes.FILE:
return req[data || 'file'];
case RouteParamtypes.FILES:
return req.files;
default:
return null; // 커스텀 데코레이터는 여기가 아닌 factory로 처리
}
}
}
switch 문으로 분기하는 단순한 구조입니다. @Body('name')을 쓰면 data = 'name'이 되어 req.body['name']을 반환합니다. @Body()를 data 없이 쓰면 req.body 전체를 반환합니다.
커스텀 데코레이터(createParamDecorator)의 경우 default: return null에 해당합니다. 커스텀 데코레이터는 RouteParamsFactory가 아니라 메타데이터에 저장된 factory 함수가 값을 추출합니다.
내장 데코레이터: @Body('name') → RouteParamsFactory → req.body.name
커스텀 데코레이터: @CurrentUser() → factory(data, ctx) → ctx.switchToHttp().getRequest().user
data 파라미터 활용하기
createParamDecorator의 첫 번째 인자 data를 활용하면 더 유연한 데코레이터를 만들 수 있습니다.
// data를 활용하는 커스텀 데코레이터
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
// 사용: user 객체 전체
@Get('me')
getMe(@CurrentUser() user: User) { ... }
// 사용: user의 특정 필드만
@Get('my-email')
getEmail(@CurrentUser('email') email: string) { ... }
@CurrentUser('email')에서 'email'이 data 파라미터로 전달됩니다. @Body('name')이 req.body.name을 반환하는 것과 같은 패턴입니다.
커스텀 데코레이터에 Pipe 적용하기
createParamDecorator로 만든 데코레이터에도 Pipe를 적용할 수 있습니다.
// Pipe를 인라인으로 적용
@Get(':id')
findOne(@CurrentUser(new ValidationPipe()) user: User) { ... }
// createParamDecorator 소스를 다시 보면...
const hasParamData = isNil(data) || !isPipe(data);
const paramData = hasParamData ? (data as any) : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
소스코드에서 isPipe()로 첫 번째 인자가 데이터인지 Pipe인지 구분합니다. Pipe 객체(transform 메서드가 있는 객체)이면 paramPipes에 포함되고, 아니면 paramData로 취급됩니다. 이것이 @CurrentUser('email')과 @CurrentUser(new ValidationPipe())를 동시에 지원할 수 있는 이유입니다.
정리 — 데코레이터는 "선언"이고, 실행은 요청 시점에 일어납니다
NestJS의 커스텀 데코레이터를 추적하면서 확인한 핵심 패턴을 정리하겠습니다.
┌──────────────────────────────────────────────────────┐
│ 빌드 타임 (선언) │
├──────────────────────────────────────────────────────┤
│ │
│ createParamDecorator │
│ → Reflect.defineMetadata(ROUTE_ARGS_METADATA, { │
│ index, factory, data, pipes │
│ }) │
│ │
│ SetMetadata('roles', ['admin']) │
│ → Reflect.defineMetadata('roles', ['admin'], │
│ handler) │
│ │
└──────────────────────────┬───────────────────────────┘
│
요청이 들어오면
│
▼
┌──────────────────────────────────────────────────────┐
│ 요청 시점 (실행) │
├──────────────────────────────────────────────────────┤
│ │
│ Guard │
│ → Reflector.get('roles', handler) │
│ → ['admin'] 읽어서 권한 확인 │
│ │
│ RouterExecutionContext │
│ → ROUTE_ARGS_METADATA 읽기 │
│ → factory(data, ctx) 실행하여 값 추출 │
│ → Pipe를 통과시켜 변환/검증 │
│ → 컨트롤러 메서드 파라미터로 전달 │
│ │
└──────────────────────────────────────────────────────┘
TypeScript 데코레이터는 클래스가 로드될 때 한 번 실행됩니다. 그 시점에 Reflect.defineMetadata로 메타데이터를 저장합니다. 실제 값 추출이나 권한 확인은 요청이 올 때 일어납니다. 2편에서 본 @Injectable()의 Reflect.metadata와 같은 패턴이 여기서도 반복됩니다.
결국 NestJS의 데코레이터 시스템은 "메타데이터 저장 → 나중에 읽어서 실행"이라는 일관된 패턴 위에 서 있습니다. @Injectable()이 DI를 위한 메타데이터를, @Controller()가 라우팅을 위한 메타데이터를, @Roles()가 권한을 위한 메타데이터를, @CurrentUser()가 값 추출을 위한 메타데이터를 저장하는 것입니다. 실행 방식은 다르지만, 저장 메커니즘은 모두 Reflect.defineMetadata입니다.
다음 7편(마지막)에서는 NestJS가 Express와 Fastify를 어떻게 플랫폼 추상화하는지 추적하겠습니다. 지금까지 applicationRef.reply(), switchToHttp().getRequest() 같은 코드를 봤는데, 이것들이 Express일 때와 Fastify일 때 어떻게 다르게 동작하는지 — AbstractHttpAdapter와 ExpressAdapter/FastifyAdapter의 내부를 보겠습니다.
관련 글
@Injectable()의 실체 — Reflect.metadata와 데코레이터가 하는 일 (2편)
@Injectable() 하나 붙이면 DI가 된다는 건 알지만, 이 데코레이터가 정확히 무슨 일을 하는 걸까요? NestJS 소스코드와 TypeScript 컴파일러 출력을 직접 추적하며, 데코레이터가 '실행'이 아니라 '등록'이라는 사실을 확인합니다.
요청이 컨트롤러에 도달하기까지 — Guard, Interceptor, Pipe, Filter (5편)
NestJS에서 HTTP 요청 하나가 컨트롤러 메서드에 도달하기까지 거치는 6단계를 소스코드로 추적합니다. Middleware, Guard, Interceptor, Pipe, ExceptionFilter — 각 레이어가 왜 존재하고 어떤 순서로 실행되는지 RouterExecutionContext 소스를 통해 확인합니다.
DI 컨테이너는 어떻게 의존성을 해결하는가 — NestJS 소스코드 추적 (3편)
@Injectable()이 메타데이터를 저장하는 것과 실제로 인스턴스를 생성해서 주입하는 것은 다른 문제입니다. NestContainer, InstanceLoader, Injector — 세 클래스가 협력하여 의존성을 해결하는 과정을 NestJS 소스코드에서 직접 추적합니다.