NestJS Guard 접근 제어

NestJS Guard란?

Guard는 NestJS의 요청 사전 검증 레이어다. CanActivate 인터페이스를 구현하며, 컨트롤러 메서드 실행 전에 요청을 허용할지 거부할지 결정한다. 인증(Authentication)과 인가(Authorization)를 분리하고, 역할 기반 접근 제어(RBAC), 정책 기반 제어(ABAC)까지 확장할 수 있다.

1. 기본 Guard 구현

Guard는 CanActivate를 구현하고 true/false 또는 Promise<boolean>을 반환한다.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);

    if (!token) {
      throw new UnauthorizedException('토큰이 없습니다');
    }

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException('유효하지 않은 토큰입니다');
    }
  }

  private extractToken(request: Request): string | null {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : null;
  }
}

ExecutionContext는 HTTP, WebSocket, gRPC 등 모든 전송 계층에 접근할 수 있는 추상화다. switchToHttp(), switchToWs(), switchToRpc()로 전환한다.

2. 커스텀 데코레이터와 Reflector

Guard의 진정한 힘은 메타데이터 기반 동적 검증에 있다. SetMetadataReflector를 조합한다.

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// public.decorator.ts — Guard 건너뛰기
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // Public 엔드포인트 체크
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    // 필요 역할 조회
    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));
  }
}

getAllAndOverride는 핸들러 → 클래스 순으로 메타데이터를 조회하고 첫 번째 결과를 반환한다. getAllAndMerge는 둘을 합친다.

Reflector 메서드 동작 사용 시나리오
get 단일 타겟에서 조회 핸들러만 체크
getAllAndOverride 첫 번째 non-undefined 반환 핸들러 우선, 클래스 폴백
getAllAndMerge 모든 타겟 결과 합침 역할 누적 (클래스 + 핸들러)

3. Guard 적용 범위

Guard는 메서드, 컨트롤러, 전역 3단계로 적용할 수 있다.

// 1. 메서드 레벨
@UseGuards(RolesGuard)
@Roles('admin')
@Get('admin/dashboard')
getDashboard() { ... }

// 2. 컨트롤러 레벨
@UseGuards(AuthGuard, RolesGuard)
@Controller('users')
export class UsersController { ... }

// 3. 전역 레벨 (app.module.ts)
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

전역 Guard로 등록하면 모든 엔드포인트에 적용된다. @Public() 데코레이터로 로그인/회원가입 같은 공개 API를 면제한다. NestJS의 DI 스코프에 대해서는 DI Scope·Provider 심화 글도 참고하자.

4. 정책 기반 Guard (ABAC)

RBAC을 넘어 리소스 소유권, 구독 상태 등 속성 기반 접근 제어를 구현할 수 있다.

// policy.interface.ts
export interface Policy {
  canAccess(user: User, context: ExecutionContext): boolean | Promise<boolean>;
}

// owner-only.policy.ts
@Injectable()
export class OwnerOnlyPolicy implements Policy {
  constructor(private readonly postService: PostService) {}

  async canAccess(user: User, context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const postId = request.params.id;
    const post = await this.postService.findById(postId);
    return post.authorId === user.id;
  }
}

// policies.decorator.ts
export const POLICIES_KEY = 'policies';
export const CheckPolicies = (...policies: Type<Policy>[]) =>
  SetMetadata(POLICIES_KEY, policies);

// policies.guard.ts
@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly moduleRef: ModuleRef,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policies = this.reflector.getAllAndOverride<Type<Policy>[]>(
      POLICIES_KEY, [context.getHandler(), context.getClass()],
    ) ?? [];

    const { user } = context.switchToHttp().getRequest();

    for (const PolicyClass of policies) {
      const policy = await this.moduleRef.resolve(PolicyClass);
      const allowed = await policy.canAccess(user, context);
      if (!allowed) throw new ForbiddenException('접근 권한이 없습니다');
    }
    return true;
  }
}

// 사용
@CheckPolicies(OwnerOnlyPolicy)
@Put(':id')
updatePost(@Param('id') id: string, @Body() dto: UpdatePostDto) { ... }

ModuleRef.resolve()로 정책 클래스를 동적 인스턴스화하므로 각 정책이 자체 의존성을 주입받을 수 있다.

5. Guard 실행 순서와 파이프라인

NestJS 요청 파이프라인에서 Guard의 위치를 정확히 이해해야 한다.

Middleware → Guard → Interceptor(before) → Pipe → Handler → Interceptor(after) → Filter

Guard는 Middleware 이후, Pipe 이전에 실행된다. 즉 요청 파싱(Pipe) 전에 인증/인가를 먼저 처리한다. 여러 Guard가 등록되면 배열 순서대로 실행되며, 하나라도 false를 반환하면 이후 Guard는 실행되지 않는다.

// 실행 순서: AuthGuard → RolesGuard → PoliciesGuard
@UseGuards(AuthGuard, RolesGuard, PoliciesGuard)
@Roles('editor')
@CheckPolicies(OwnerOnlyPolicy)
@Put(':id')
updatePost() { ... }

Interceptor 패턴과 조합하면 더 강력한 요청 제어가 가능하다. NestJS Interceptor 6가지 패턴 글도 함께 참고하자.

6. WebSocket·GraphQL Guard

// WebSocket Guard
@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client = context.switchToWs().getClient();
    const token = client.handshake?.auth?.token;
    // 토큰 검증 로직
    return !!this.jwtService.verify(token);
  }
}

// GraphQL Guard
@Injectable()
export class GqlAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const ctx = GqlExecutionContext.create(context);
    const { req } = ctx.getContext();
    // HTTP 요청과 동일하게 처리
    return this.validateRequest(req);
  }
}

ExecutionContext의 추상화 덕분에 전송 계층만 전환하면 동일한 Guard 로직을 재사용할 수 있다.

마무리

NestJS Guard는 단순한 인증 체크를 넘어 RBAC, ABAC, 정책 기반 접근 제어까지 확장할 수 있는 강력한 메커니즘이다. Reflector와 커스텀 데코레이터를 조합하면 선언적이고 유지보수하기 쉬운 인가 시스템을 구축할 수 있다. 핵심은 Guard의 파이프라인 위치를 이해하고, 단일 책임 원칙에 따라 인증과 인가를 분리하는 것이다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux