NestJS 코드를 보면 @Controller, @Get, @Injectable 같은 @ 문법이 자주 등장합니다. 이것이 데코레이터(Decorator)입니다. 클래스나 메서드 선언 위에 붙여서 기능을 추가하거나 메타데이터를 등록하는 용도로 쓰입니다.
데코레이터는 실험적 기능으로, tsconfig.json에서 명시적으로 활성화해야 합니다.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true // 메타데이터 활용 시 필요
}
}
experimentalDecorators: TypeScript 5.0 이전의 레거시 데코레이터 API를 사용하는 옵션. NestJS, TypeORM 등 주요 프레임워크는 이 방식을 씁니다.
클래스 선언 앞에 붙습니다. 클래스 생성자를 인수로 받습니다.
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
데코레이터 팩토리를 쓰면 인수를 받을 수 있습니다.
function Log(prefix: string) {
return function (constructor: Function) {
console.log(`${prefix}: ${constructor.name} 클래스가 정의됨`);
};
}
@Log("INFO")
class Service {
// "INFO: Service 클래스가 정의됨" 출력
}
데코레이터 팩토리(Decorator Factory): 데코레이터를 반환하는 함수. @Decorator 형태로 인수를 전달할 수 있습니다.
메서드 선언 앞에 붙습니다. 메서드의 PropertyDescriptor를 조작할 수 있습니다.
function Measure(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = Date.now();
const result = await original.apply(this, args);
console.log(`${key} 실행 시간: ${Date.now() - start}ms`);
return result;
};
return descriptor;
}
class DataService {
@Measure
async fetchData() {
// 자동으로 실행 시간 측정
}
}
프로퍼티 선언 앞에 붙습니다.
function ReadOnly(target: any, key: string) {
Object.defineProperty(target, key, {
writable: false,
});
}
class Config {
@ReadOnly
version = "1.0.0";
}
메서드의 매개변수 앞에 붙어 메타데이터를 등록합니다. 주로 의존성 주입에서 활용됩니다.
function Body(target: any, methodName: string, paramIndex: number) {
// 메타데이터에 paramIndex 위치가 request body임을 기록
Reflect.defineMetadata("body", paramIndex, target, methodName);
}
class UserController {
createUser(@Body data: CreateUserDto) {
// data는 요청 본문에서 추출됨
}
}
NestJS는 데코레이터를 통해 라우팅, 의존성 주입, 유효성 검사 등을 선언적으로 처리합니다.
import { Controller, Get, Post, Body, Param } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
@Injectable()
class UserService {
findAll() {
return [];
}
}
@Controller("users")
class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(":id")
findOne(@Param("id") id: string) {
return { id };
}
@Post()
create(@Body() createUserDto: { name: string }) {
return createUserDto;
}
}
@Controller, @Get, @Post, @Body, @Param은 모두 데코레이터입니다. 각 데코레이터가 메타데이터를 등록하고, NestJS 프레임워크가 그 정보를 읽어 라우팅과 의존성 주입을 처리합니다.
여러 데코레이터가 붙으면 아래에서 위로 실행됩니다.
@First
@Second
@Third
class Example {}
// 실행 순서: Third → Second → First
데코레이터 팩토리의 평가(함수 호출)는 위에서 아래로, 실제 데코레이터 적용은 아래에서 위로 처리됩니다.
데코레이터는 코드를 선언적이고 읽기 쉽게 만들지만, 런타임 동작이 코드에서 보이지 않아 디버깅이 어려울 수 있습니다. 프레임워크가 제공하는 데코레이터를 사용하는 것과 직접 작성하는 것은 다른 영역입니다. NestJS처럼 데코레이터 기반 프레임워크를 사용한다면 동작 원리를 이해하는 것이 문제 해결에 도움이 됩니다.