본문으로 건너뛰기

안동민 개발노트

본문 시작

커스텀 데코레이터 만들기

요청 객체에서 인증 사용자를 꺼내는 매개변수 데코레이터를 만들고 가드와 결합해 반복 코드를 줄입니다.

NestJS를 사용하다 보면 @Controller(), @Get(), @Body(), @Param() 같은 다양한 데코레이터를 접하게 됩니다.

이 데코레이터는 클래스, 메서드, 매개변수에 메타데이터를 추가하거나 특정 기능을 부여하는 문법 요소입니다.

NestJS는 내장 데코레이터 외에도 개발자가 직접 커스텀 데코레이터(Custom Decorators)를 만들 수 있도록 지원합니다.

커스텀 데코레이터는 코드의 중복을 줄이고, 가독성을 높이며, 관심사를 더욱 명확하게 분리하여 애플리케이션의 유지보수성을 향상시키는 데 크게 기여합니다.

이번 절에서는 커스텀 데코레이터의 개념을 이해하고, 실제로 만들어보고, 이를 활용하는 방법을 배워보겠습니다.

커스텀 데코레이터는 요청 문맥에서 필요한 값만 꺼낸다

컨트롤러가 req 전체를 알 필요가 없을 때 createParamDecorator로 필요한 값만 추출해 매개변수로 전달한다.

  1. Request

    인증 가드나 미들웨어가 req에 문맥을 넣는다.

  2. Factory

    createParamDecorator 콜백이 ExecutionContext를 받는다.

  3. Extract

    req.user, header, session 중 필요한 값만 고른다.

  4. Parameter

    컨트롤러 매개변수에 깔끔하게 주입된다.

getMe(@Req() req) { return req.user }
getMe(@User() user: AuthUser)

커스텀 데코레이터란?

커스텀 데코레이터는 NestJS의 createParamDecorator 헬퍼 함수로 만듭니다.

이 함수는 요청 컨텍스트(ExecutionContext)에서 데이터를 추출하거나 변환해 라우트 핸들러 매개변수로 주입합니다.

파이프가 주로 매개변수 유효성 검사와 타입 변환에 초점을 맞춘다면, 커스텀 데코레이터는 요청에서 특정 데이터를 추출하는 데 더 유용합니다.

요청 객체에서 컨트롤러 매개변수까지

커스텀 데코레이터는 ExecutionContext에서 HTTP 요청을 꺼내고, 그 안의 특정 데이터를 컨트롤러 인자로 전달한다.

  1. Context

    Nest가 현재 요청 실행 문맥을 전달한다.

  2. switchToHttp

    HTTP 요청 객체를 꺼낼 수 있는 모드로 전환한다.

  3. getRequest

    Express Request에서 user, headers, params를 읽는다.

  4. Select

    data 인자를 기준으로 user의 일부 필드만 선택할 수 있다.

  5. Controller

    @User("id")처럼 필요한 값이 매개변수로 들어온다.

구분역할예시
Decorator어디서 값을 가져올지 정한다.@User(), @HeaderValue()
Pipe가져온 값이 올바른지 검증하거나 변환한다.ParseIntPipe, ValidationPipe
주요 사용 사례
  • 인증된 사용자 정보 추출: 요청 객체에 저장된 사용자(req.user) 정보를 직접 가져오기.
  • 특정 헤더 값 가져오기: Authorization 헤더나 커스텀 헤더 값을 간편하게 추출하기.
  • 세션 또는 쿠키 데이터 접근: 세션이나 쿠키에서 필요한 데이터를 가져오기.
  • 복잡한 요청 객체에서 특정 부분만 추출: req.query, req.params, req.body 등에서 원하는 데이터만 뽑아내기.

첫 번째 커스텀 데코레이터: @User() 만들기

가장 흔하게 사용되는 커스텀 데코레이터 중 하나는 현재 인증된 사용자 정보를 가져오는 데코레이터입니다.

예를 들어, JWT 인증을 통해 요청 객체에 사용자 정보가 req.user 형태로 저장되어 있다고 가정해 봅시다.

매번 req.user를 직접 가져오는 대신, @User() 데코레이터를 사용하여 간편하게 사용자 정보를 가져올 수 있도록 만들어보겠습니다.

단계 1: 데코레이터 파일 생성

src/common/decorators 디렉토리를 생성하고 user.decorator.ts 파일을 만듭니다.

src/common/decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    // ExecutionContext는 Http, RPC, Websockets 컨텍스트를 추상화합니다.
    // 여기서는 HTTP 요청을 다루므로 Http 컨텍스트로 전환합니다.
    const request = ctx.switchToHttp().getRequest();

    // data 인자는 데코레이터에 전달된 인자입니다 (예: @User('firstName')).
    // 만약 data가 있다면 해당 속성만 반환하고, 없다면 전체 user 객체를 반환합니다.
    return data ? request.user?.[data] : request.user;
  },
);
코드 설명
  • createParamDecorator((data, ctx) => { ... }): NestJS에서 매개변수 데코레이터를 생성할 때 사용하는 헬퍼 함수입니다. 인자로 콜백 함수를 받습니다.
  • data: unknown: 이 데코레이터를 사용할 때 전달하는 인자입니다. 예를 들어 @User('id')라고 사용하면 data'id'가 됩니다.
  • ctx: ExecutionContext: 현재 실행 컨텍스트에 대한 정보를 제공하는 객체입니다. HTTP 요청뿐만 아니라 WebSockets, gRPC 등 다양한 컨텍스트에서 작동할 수 있도록 추상화되어 있습니다.
  • ctx.switchToHttp().getRequest(): ExecutionContext를 HTTP 컨텍스트로 전환하고, Request 객체를 가져옵니다. Express.js의 req 객체와 동일하다고 생각하시면 됩니다.
  • request.user: 이 예시에서는 인증 과정에서 req.user에 사용자 정보가 담긴다고 가정합니다. 실제 애플리케이션에서는 가드나 미들웨어에서 이 정보를 request 객체에 추가하는 로직이 필요합니다.
  • data ? request.user?.[data] : request.user;: data 인자가 있으면 (예: id) request.user.id를 반환하고, 없으면 (@User()) request.user 전체 객체를 반환합니다. ?. (Optional Chaining)을 사용해 request.user가 없을 때 undefined를 반환하도록 안전하게 처리합니다.

커스텀 데코레이터 사용하기

이제 @User() 데코레이터를 컨트롤러에서 사용해 사용자 정보를 간편하게 가져와 보겠습니다.

이 데코레이터가 동작하려면 요청이 컨트롤러에 도달하기 전에 request.user에 실제 사용자 정보가 채워져 있어야 합니다.

여기서는 간단한 미들웨어로 req.user를 임시 설정해 보겠습니다.

실제 환경에서는 인증 가드(Guard)에서 JWT 토큰 등을 검증해 사용자 정보를 설정하게 됩니다.

단계 1: 사용자 정보를 설정할 미들웨어 또는 가드 준비 (예시)
src/auth/auth.middleware.ts (예시)
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 실제 인증 로직 (DB 조회, JWT 검증 등)
    // 여기서는 임시로 가짜 사용자 정보를 req.user에 할당합니다.
    (req as any).user = { userId: 1, username: 'nest_user', roles: ['admin'] };
    console.log('User mocked by AuthMiddleware:', (req as any).user);
    next();
  }
}
src/app.module.ts (미들웨어 적용)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthMiddleware } from './auth/auth.middleware'; // 미들웨어 임포트

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes('*'); // 모든 경로에 AuthMiddleware 적용
  }
}
단계 2: 컨트롤러에서 @User() 데코레이터 사용
src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from './common/decorators/user.decorator'; // 커스텀 데코레이터 임포트

// 사용자 정보의 타입을 정의해두면 타입 안정성을 높일 수 있습니다.
interface CurrentUser {
  userId: number;
  username: string;
  roles: string[];
}

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('profile')
  // @User() 데코레이터를 사용하여 요청 객체에서 user 정보를 가져옵니다.
  getProfile(@User() user: CurrentUser) {
    console.log('Current User:', user);
    return `Hello, ${user.username}! Your ID is ${user.userId} and roles are ${user.roles.join(', ')}.`;
  }

  @Get('username')
  // @User('username') 데코레이터를 사용하여 user 객체에서 특정 속성만 가져옵니다.
  getUsername(@User('username') username: string) {
    console.log('Current Username:', username);
    return `Your username is: ${username}`;
  }
}
실행 및 확인

npm run start:dev 명령어로 애플리케이션을 실행합니다.

웹 브라우저나 Postman 등으로 http://localhost:3000/profile 또는 http://localhost:3000/username으로 GET 요청을 보냅니다.

콘솔에 Current User: { userId: 1, username: 'nest_user', roles: [ 'admin' ] }와 같은 메시지가 출력되고, 응답으로 사용자 정보가 나타나는 것을 확인할 수 있습니다.


고급 활용: 데코레이터와 가드의 결합

데코레이터와 가드 권한 검사

데코레이터는 메타데이터를 선언하고, 가드는 실행 시점에 그 메타데이터와 사용자 정보를 비교한다. 선언과 판단을 분리하는 구조다.

  1. @Roles

    핸들러나 클래스에 필요한 역할 메타데이터를 붙인다.

  2. AuthGuard

    토큰을 확인하고 req.user를 준비한다.

  3. RolesGuard

    Reflector로 requiredRoles를 읽는다.

  4. Compare

    user.roles와 requiredRoles를 비교한다.

  5. Handler

    통과한 요청만 컨트롤러 메서드로 들어간다.

  6. 인증 먼저

    역할 비교 전에 사용자가 누구인지 확정되어야 한다.

  7. 메타데이터는 선언

    @Roles 자체는 접근을 막지 않는다.

  8. 가드는 실행 판단

    true면 통과, false나 예외면 핸들러가 실행되지 않는다.

@User() 데코레이터는 가드와 함께 사용될 때 더욱 강력해집니다.

인증 가드가 요청을 통과시키면서 req.user에 사용자 정보를 설정하면, 컨트롤러에서는 @User() 데코레이터를 통해 이 정보를 깔끔하게 가져다 쓸 수 있기 때문입니다.

예를 들어, 역할 기반 접근 제어(RBAC)를 구현할 때, @Roles('admin')과 같은 커스텀 데코레이터를 만들고, 이 데코레이터가 설정한 메타데이터를 가드에서 읽어 사용자의 역할을 검증하는 방식으로 활용할 수 있습니다.

예시: @Roles() 데코레이터와 RolesGuard (개념만 소개)
src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
src/auth/guards/roles.guard.ts (간략화된 예시)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; // Reflector를 사용하여 메타데이터를 읽습니다.

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true; // 역할 제한이 없으면 접근 허용
    }
    const { user } = context.switchToHttp().getRequest();
    // 실제 로직: user의 roles가 requiredRoles에 포함되는지 확인
    return user.roles.some((role: string) => requiredRoles.includes(role));
  }
}
src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guards/auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';

@Controller('admin')
@UseGuards(AuthGuard, RolesGuard) // AuthGuard 먼저, RolesGuard 나중에 실행
export class AdminController {
  @Get('dashboard')
  @Roles('admin') // 이 메서드는 'admin' 역할만 접근 가능
  getAdminDashboard() {
    return 'Welcome to the admin dashboard!';
  }
}

@Roles()RolesGuard의 관계를 메타데이터 흐름으로 보면, 데코레이터와 가드의 책임 경계가 더 명확해집니다.

@Roles와 RolesGuard 역할

@Roles는 required roles를 메타데이터로 저장한다. RolesGuard는 요청마다 그 값을 읽어 현재 사용자 역할과 맞는지 판정한다.

  1. SetMetadata

    @Roles("admin")이 roles 키에 값을 저장한다.

  2. Reflector

    RolesGuard가 클래스와 핸들러의 값을 함께 읽는다.

  3. Request user

    AuthGuard가 만든 user.roles를 가져온다.

  4. Decision

    필요 역할이 없으면 통과, 있으면 포함 여부를 확인한다.

상황판정결과
requiredRoles 없음공개 또는 별도 정책통과
사용자 역할에 admin 포함요구 역할 충족핸들러 실행
역할 불일치요구 역할 미충족403 응답

가드와 함께 사용할 때는 데코레이터가 값을 추출하고, 가드가 그 값의 신뢰성을 보장한다는 책임 분리를 유지해야 합니다.

커스텀 데코레이터는 반복 추출을 감추고 의도를 드러낸다

컨트롤러가 요청 객체를 뒤지는 대신 @User, @Roles 같은 선언을 쓰면, 핸들러의 목적과 필요한 문맥이 바로 보인다.

  1. 반복되는 추출

    여러 핸들러가 같은 요청 필드를 꺼낸다.

  2. 의미 있는 문맥

    현재 사용자, 테넌트, 언어처럼 업무 이름이 있다.

  3. 검증 결합

    데코레이터로 값 추출 후 Pipe로 검증하면 경계가 선명하다.

const user = req.user; const role = req.headers.role;
update(@User() user, @Roles("admin") role)

이처럼 커스텀 데코레이터는 코드의 재사용성을 높이고, 특정 로직을 선언적으로 표현할 수 있게 하여 NestJS 애플리케이션의 구조를 더욱 깔끔하고 효율적으로 만들어줍니다.


이번 절에서는 NestJS 커스텀 데코레이터를 만들고 적용하는 방법을 살펴봤습니다.

요청 객체에서 반복해서 꺼내는 값이나 공통 검증 로직은 데코레이터로 분리하면 컨트롤러 코드를 더 읽기 쉽게 유지할 수 있습니다.

다음 장에서는 NestJS 애플리케이션의 핵심 기능인 데이터베이스 연동에 대해 자세히 알아보겠습니다.

커스텀 데코레이터로 요청 문맥 추출하기

데코레이터를 설계할 때는 어떤 요청 문맥을 숨길지, 어떤 이름으로 컨트롤러에 드러낼지, 검증은 어디에서 할지 함께 정한다.

  1. Name

    @CurrentUser, @TenantId처럼 업무 의미가 있는 이름을 고른다.

  2. Source

    req.user, headers, params, session 중 출처를 한정한다.

  3. Shape

    전체 객체 또는 특정 필드 중 무엇을 넘길지 정한다.

  4. Pipe

    필요하면 ParseIntPipe나 ValidationPipe와 같이 쓴다.

  5. Fallback

    값이 없을 때 undefined, 기본값, 예외 중 하나를 정한다.

데코레이터출처컨트롤러에서 보이는 의미
@CurrentUser()req.user인증된 사용자 전체
@CurrentUser("id")req.user.id사용자 식별자만 사용
@TenantId()header 또는 token claim멀티테넌트 경계
@Locale()Accept-Language응답 언어 선택