NestJS 면접: Guards, Interceptors, 모듈형 아키텍처
Guards, Interceptors, 모듈형 아키텍처에 관한 NestJS 기술 면접의 빈출 질문을 구체적인 TypeScript 코드 예제와 기술적 설명과 함께 다룹니다.

NestJS 기술 면접은 일관되게 프레임워크의 세 가지 기둥에 초점을 맞춥니다. 바로 Guards, Interceptors, 그리고 모듈형 아키텍처입니다. 이 메커니즘들은 잘 구조화된 모든 NestJS 애플리케이션의 척추를 이루며, 채용 담당자는 이를 통해 프레임워크에 대한 실제 숙련도를 평가합니다.
Guard는 접근을 통제하고(누가 들어올 수 있는지), Interceptor는 데이터를 변환하며(무엇이 들어오고 나가는지), 모듈형 아키텍처는 모든 것을 정리합니다. 세 가지 모두를 능숙하게 다루면 기본 CRUD 연산을 넘어선 깊은 NestJS 이해를 보여줍니다.
NestJS의 Guards 작동 방식과 흔한 면접 시나리오
Guards는 CanActivate 인터페이스를 구현하며, 요청이 핸들러에 도달할지를 결정합니다. 미들웨어와 달리 Guards는 NestJS 실행 컨텍스트(ExecutionContext)에 접근하여 핸들러 메타데이터에 기반한 인가 결정을 가능하게 합니다.
전형적인 질문: "사용자 역할을 검증하는 Guard를 구현해 주세요."
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Retrieve roles defined via the @Roles() decorator
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// No roles required means the route is public
if (!requiredRoles) return true;
// Extract user from the HTTP request
const { user } = context.switchToHttp().getRequest();
// Check if the user holds at least one required role
return requiredRoles.some((role) => user.roles?.includes(role));
}
}Reflector는 사용자 정의 데코레이터로 부착된 메타데이터를 읽어옵니다. 짝을 이루는 @Roles() 데코레이터는 다음과 같은 모습입니다.
import { SetMetadata } from '@nestjs/common';
// Creates a decorator that attaches roles as metadata
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);면접에서 강한 답변은 코드를 넘어섭니다. 실행 순서가 중요합니다. Middleware → Guards → Interceptors(이전) → Pipes → Handler → Interceptors(이후) → Exception Filters. 이 순서는 거의 모든 NestJS 면접에서 등장합니다.
NestJS Interceptors: 요청과 응답의 변환
Interceptors는 NestInterceptor를 구현하고 RxJS를 사용하여 핸들러 전후의 데이터 스트림을 조작합니다. 그 힘은 실행 파이프라인을 나타내는 CallHandler에 대한 접근에서 나옵니다.
자주 나오는 질문: "실행 시간을 측정하는 로깅 Interceptor를 만들어 주세요."
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
// next.handle() invokes the route handler
return next.handle().pipe(
tap(() => {
// Runs AFTER the handler responds
const duration = Date.now() - now;
this.logger.log(`${method} ${url} — ${duration}ms`);
}),
);
}
}RxJS의 tap 연산자는 스트림을 수정하지 않고 관찰합니다. 응답을 변환하려면 알맞은 연산자는 map입니다.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// Generic interface for a standardized API response
interface ApiResponse<T> {
data: T;
timestamp: string;
path: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
const request = context.switchToHttp().getRequest();
return next.handle().pipe(
map((data) => ({
data,
timestamp: new Date().toISOString(),
path: request.url,
})),
);
}
}이 표준화된 응답 패턴은 면접에서 좋은 점수를 받습니다. API 설계와 응답의 일관성에 대해 사고할 수 있는 능력을 보여주기 때문입니다.
Middleware: 원시 HTTP 처리(Express와 유사). Guard: 접근에 대한 불리언 결정. Interceptor: RxJS를 통한 요청/응답 스트림의 변환. 면접에서 이 세 개념을 혼동하는 것은 즉각적인 레드 플래그입니다.
NestJS 모듈형 아키텍처: 확장 가능한 애플리케이션 구축
모듈형 아키텍처는 면접에서 주니어와 시니어 개발자를 가릅니다. NestJS는 모듈 기반 조직을 강제하지만, 진짜 질문은 아키텍처적 결정에 있습니다. 언제 모듈을 만들고 모듈 간 의존성을 어떻게 관리할 것인가입니다.
고급 질문: "적절히 분리된 모듈로 NestJS 이커머스 애플리케이션을 어떻게 구조화하시겠습니까?"
import { Module } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
import { PaymentsModule } from '../payments/payments.module';
import { ProductsModule } from '../products/products.module';
@Module({
// Import modules that OrdersModule depends on
imports: [PaymentsModule, ProductsModule],
controllers: [OrdersController],
providers: [OrdersService],
// Expose OrdersService to modules that import OrdersModule
exports: [OrdersService],
})
export class OrdersModule {}전형적인 면접 함정은 순환 의존성입니다. OrdersModule이 PaymentsModule을 임포트하고 PaymentsModule이 OrdersModule을 임포트하면, NestJS는 시작 시 실패합니다. 비상 출구는 forwardRef를 사용하는 것입니다.
// Resolving circular dependencies
@Module({
imports: [forwardRef(() => PaymentsModule)],
})
export class OrdersModule {}가장 강한 면접 답변은 forwardRef가 임시방편이지 해결책이 아니라고 설명합니다. 올바른 수정은 공유 모듈(SharedOrderPaymentModule)을 추출하도록 리팩터링하거나, 통신을 분리하기 위해 Event Emitter를 사용하는 것입니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Dynamic Modules와 고급 설정
Dynamic Modules는 프로젝트 간에 재사용 가능한, 설정 가능한 모듈을 생성합니다. 이 패턴은 시니어 레벨 면접에서 등장합니다.
질문: "동적 옵션을 가진 설정 가능한 캐시 모듈을 만들어 주세요."
import { Module, DynamicModule, Global } from '@nestjs/common';
import { CacheService } from './cache.service';
// Module configuration interface
export interface CacheModuleOptions {
ttl: number; // Time-to-live in seconds
maxItems: number; // Maximum number of entries
prefix: string; // Key prefix
}
@Global() // Available everywhere without explicit import
@Module({})
export class CacheModule {
static forRoot(options: CacheModuleOptions): DynamicModule {
return {
module: CacheModule,
providers: [
{
// Inject options via a custom token
provide: 'CACHE_OPTIONS',
useValue: options,
},
CacheService,
],
exports: [CacheService],
};
}
}해당 서비스는 @Inject()를 통해 옵션을 주입합니다.
import { Injectable, Inject } from '@nestjs/common';
import { CacheModuleOptions } from './cache.module';
@Injectable()
export class CacheService {
private store = new Map<string, { value: any; expiry: number }>();
constructor(@Inject('CACHE_OPTIONS') private options: CacheModuleOptions) {}
set(key: string, value: any): void {
const prefixedKey = `${this.options.prefix}:${key}`;
this.store.set(prefixedKey, {
value,
expiry: Date.now() + this.options.ttl * 1000,
});
}
get<T>(key: string): T | null {
const prefixedKey = `${this.options.prefix}:${key}`;
const entry = this.store.get(prefixedKey);
// Check expiration before returning
if (!entry || entry.expiry < Date.now()) return null;
return entry.value as T;
}
}forRoot / forRootAsync 패턴은 NestJS의 정석입니다. forRootAsync는 주입 가능한 ConfigService에서 설정을 로드하며, 이는 프로덕션에서 권장되는 관행입니다.
모듈을 @Global()로 표시하는 것은 편리해 보이지만, 남용하면 보이지 않는 결합이 생깁니다. 면접에서 @Global()은 횡단 관심사 서비스(config, cache, logger)에 한정해야 한다고 언급하는 것은 아키텍처적 성숙도를 보여줍니다.
사용자 정의 데코레이터와 Guards 합성
사용자 정의 데코레이터는 여러 Guards와 메타데이터를 단일 어노테이션으로 결합합니다. 이 주제는 미드 시니어 직책에서 등장합니다.
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
// Combines JWT authentication + role verification
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(JwtAuthGuard, RolesGuard),
);
}컨트롤러에서의 사용 예시입니다.
import { Controller, Get, Post, Body } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { OrdersService } from './orders.service';
@Controller('orders')
export class OrdersController {
constructor(private ordersService: OrdersService) {}
@Get()
@Auth('admin', 'manager') // Single decorator replaces UseGuards + Roles
findAll() {
return this.ordersService.findAll();
}
@Post()
@Auth('admin')
create(@Body() dto: CreateOrderDto) {
return this.ordersService.create(dto);
}
}applyDecorators 패턴은 컨트롤러 코드를 단순화하고 인가 로직을 중앙 집중화합니다. 면접에서 이 접근법을 선제적으로 제안하는 것은 강한 신호가 됩니다.
결론
- Guards는
CanActivate와Reflector를 사용하여 핸들러 메타데이터에 기반한 접근 결정을 내립니다 - Interceptors는 RxJS(
tap,map)를 활용하여 핸들러 전후의 스트림을 변환합니다 - 실행 순서 Middleware → Guards → Interceptors → Pipes → Handler는 거의 보편적인 질문입니다
- 순환 의존성은
forwardRef가 아니라 아키텍처 리팩터링으로 해결해야 합니다 - Dynamic Modules(
forRoot/forRootAsync)는 재사용 가능한 컴포넌트를 구축하는 능력을 보여줍니다 applyDecorators는 여러 Guards를 읽기 쉬운 단일 데코레이터로 합성합니다- 이 질문들을 실제 코드로 연습하려면 NestJS 모듈과 Interceptors를 참고하시기 바랍니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

NestJS + Prisma: Node.js를 위한 모던 백엔드 스택
NestJS와 Prisma로 모던한 백엔드 API를 구축하기 위한 완전한 가이드입니다. 설정, 모델, 서비스, 트랜잭션 및 모범 사례를 설명합니다.

Node.js 백엔드 면접 질문: 완벽 가이드 2026
Node.js 백엔드 면접에서 가장 자주 나오는 25가지 질문. Event loop, async/await, streams, 클러스터링, 성능을 상세한 답변과 함께 설명합니다.

NestJS: 완전한 REST API 구축 가이드
NestJS로 전문적인 REST API를 구축하는 완벽 가이드입니다. 컨트롤러, 서비스, 모듈 구성, class-validator를 활용한 유효성 검사, 에러 핸들링을 실전 코드로 설명합니다.