NestJS 커스텀 데코레이터란?
NestJS에서 커스텀 데코레이터는 반복 로직을 선언적으로 추상화하는 강력한 도구입니다. TypeScript 데코레이터와 NestJS의 메타데이터 시스템을 결합하면, @CurrentUser(), @Roles('admin'), @Public() 같은 직관적인 API를 만들 수 있습니다. Guard, Interceptor, Pipe와 결합하면 횡단 관심사를 깔끔하게 분리할 수 있습니다.
파라미터 데코레이터: createParamDecorator
가장 많이 사용하는 패턴으로, 요청 객체에서 특정 데이터를 추출합니다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
// 현재 인증된 사용자 추출
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// @CurrentUser() → 전체 user 객체
// @CurrentUser('email') → user.email만
return data ? user?.[data] : user;
},
);
// 클라이언트 IP 추출
export const ClientIp = createParamDecorator(
(data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return (
request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
request.ip
);
},
);
// 사용
@Get('profile')
getProfile(
@CurrentUser() user: User, // 전체 객체
@CurrentUser('email') email: string, // 특정 필드
@ClientIp() ip: string, // 클라이언트 IP
) {
return { user, email, ip };
}
createParamDecorator의 첫 번째 인자 data는 데코레이터에 전달한 값, 두 번째 ctx는 ExecutionContext입니다. HTTP뿐 아니라 WebSocket, GraphQL 컨텍스트도 처리할 수 있습니다.
메타데이터 데코레이터: SetMetadata
Guard나 Interceptor에서 읽을 수 있는 메타데이터를 설정합니다.
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);
// 캐시 TTL 설정
export const CACHE_TTL_KEY = 'cacheTtl';
export const CacheTTL = (seconds: number) => SetMetadata(CACHE_TTL_KEY, seconds);
// 사용
@Controller('users')
export class UserController {
@Public() // 인증 불필요
@Get('health')
health() { return 'ok'; }
@Roles('admin', 'manager') // admin 또는 manager만
@CacheTTL(300) // 5분 캐시
@Get('stats')
getStats() { ... }
}
메타데이터는 Reflector를 통해 Guard/Interceptor에서 읽습니다:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
데코레이터 합성: applyDecorators
여러 데코레이터를 하나로 합성하면 반복을 줄일 수 있습니다.
import { applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
// 인증 필요 엔드포인트 데코레이터
export function Auth(...roles: string[]) {
return applyDecorators(
Roles(...roles),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: '인증 필요' }),
);
}
// API 응답 캐싱 데코레이터
export function Cached(ttlSeconds: number = 60) {
return applyDecorators(
CacheTTL(ttlSeconds),
UseInterceptors(CacheInterceptor),
);
}
// Paginated 응답 데코레이터
export function Paginated(dto: Type) {
return applyDecorators(
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(PaginatedResponseDto) },
{ properties: { data: { type: 'array', items: { $ref: getSchemaPath(dto) } } } },
],
},
}),
);
}
// 사용 — 데코레이터 1개로 5개 역할
@Controller('admin')
export class AdminController {
@Auth('admin') // Guard + Swagger + Role 한 번에
@Cached(300) // 캐시 + Interceptor 한 번에
@Paginated(UserDto) // Swagger 페이지네이션 응답
@Get('users')
findAll(@Query() query: PaginateQuery) { ... }
}
applyDecorators는 데코레이터 배열을 하나의 데코레이터로 합성합니다. 팀 전체에서 일관된 패턴을 강제할 수 있어, 코드 리뷰 부담을 크게 줄여줍니다.
Pipe 연동 파라미터 데코레이터
커스텀 파라미터 데코레이터에 Pipe를 연동하면 추출과 검증을 동시에 처리합니다.
// UUID 파라미터 + 검증
export const UUIDParam = (paramName: string = 'id') =>
Param(paramName, new ParseUUIDPipe({ version: '4' }));
// 페이지네이션 쿼리 추출 + 변환
export const Pagination = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const page = Math.max(1, parseInt(request.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(request.query.limit) || 20));
const sort = request.query.sort || 'createdAt';
const order = request.query.order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
return { page, limit, sort, order, skip: (page - 1) * limit };
},
);
// 파일 업로드 + 검증
export const UploadImage = () =>
applyDecorators(
UseInterceptors(FileInterceptor('file')),
UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({ fileType: /(jpg|jpeg|png|webp)$/ })
.addMaxSizeValidator({ maxSize: 5 * 1024 * 1024 })
.build(),
),
);
// 사용
@Get(':id/orders')
findOrders(
@UUIDParam() id: string,
@Pagination() pagination: PaginationDto,
) { ... }
@Post('avatar')
uploadAvatar(@UploadImage() file: Express.Multer.File) { ... }
GraphQL·WebSocket 호환 데코레이터
HTTP 외에 다른 전송 계층도 지원하는 범용 데코레이터 패턴입니다.
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const type = ctx.getType<string>();
let user: User;
if (type === 'http') {
user = ctx.switchToHttp().getRequest().user;
} else if (type === 'ws') {
user = ctx.switchToWs().getClient().user;
} else if (type === 'graphql') {
// GqlExecutionContext로 변환
const gqlCtx = GqlExecutionContext.create(ctx);
user = gqlCtx.getContext().req.user;
}
return data ? user?.[data] : user;
},
);
// HTTP, WebSocket, GraphQL 모두에서 동일하게 동작
@Query(() => UserType)
me(@CurrentUser() user: User) { return user; }
@SubscribeMessage('chat')
handleChat(@CurrentUser() user: User, @MessageBody() msg: string) { ... }
ctx.getType()으로 현재 실행 컨텍스트를 판별하면 하나의 데코레이터로 모든 전송 계층을 지원할 수 있습니다. NestJS ExecutionContext의 핵심 활용 패턴입니다.
클래스 데코레이터 패턴
컨트롤러 전체에 공통 설정을 적용하는 클래스 데코레이터입니다.
// API 버전 + 접두사 + Swagger 태그
export function ApiController(prefix: string, version: string = '1') {
return applyDecorators(
Controller(`api/v${version}/${prefix}`),
ApiTags(prefix),
UseGuards(ThrottlerGuard),
UseInterceptors(LoggingInterceptor),
);
}
// Crud 컨트롤러 기본 설정
export function CrudController(entity: string) {
return applyDecorators(
ApiController(entity),
Auth('admin'),
UseInterceptors(ClassSerializerInterceptor),
);
}
// 사용
@CrudController('users')
export class UserController {
// 자동으로: /api/v1/users, Swagger 태그, 인증, 직렬화 적용
@Get()
findAll() { ... }
}
이 패턴은 마이크로서비스에서 수십 개의 CRUD 컨트롤러를 생성할 때 보일러플레이트를 극적으로 줄여줍니다. NestJS Middleware와 함께 레이어별 관심사를 분리하면 유지보수성이 크게 향상됩니다.
테스트 전략
describe('CurrentUser decorator', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
controllers: [TestController],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should extract user from request', () => {
return request(app.getHttpServer())
.get('/test/me')
.set('Authorization', 'Bearer valid-token')
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
});
});
it('should extract specific field', () => {
return request(app.getHttpServer())
.get('/test/email') // @CurrentUser('email')
.set('Authorization', 'Bearer valid-token')
.expect(200)
.expect((res) => {
expect(res.body).toBe('test@example.com');
});
});
});
정리
NestJS 커스텀 데코레이터는 3가지 핵심 패턴으로 나뉩니다: createParamDecorator로 파라미터 추출, SetMetadata로 Guard/Interceptor 메타데이터 설정, applyDecorators로 여러 데코레이터 합성. 이 3가지를 조합하면 @Auth('admin'), @Cached(300), @CrudController('users') 같은 선언적 API를 만들어 코드 중복을 제거하고 팀 전체의 일관성을 유지할 수 있습니다.