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의 진정한 힘은 메타데이터 기반 동적 검증에 있다. SetMetadata와 Reflector를 조합한다.
// 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의 파이프라인 위치를 이해하고, 단일 책임 원칙에 따라 인증과 인가를 분리하는 것이다.