# NestJS 면접: Guards, Interceptors, 모듈형 아키텍처 > Guards, Interceptors, 모듈형 아키텍처에 관한 NestJS 기술 면접의 빈출 질문을 구체적인 TypeScript 코드 예제와 기술적 설명과 함께 다룹니다. - Published: 2026-03-30 - Updated: 2026-05-04 - Author: Anthony Fillion-Maillet - Tags: nestjs, guards, interceptors, architecture, interview - Reading time: 9 min --- NestJS 기술 면접은 일관되게 프레임워크의 세 가지 기둥에 초점을 맞춥니다. 바로 Guards, Interceptors, 그리고 모듈형 아키텍처입니다. 이 메커니즘들은 잘 구조화된 모든 NestJS 애플리케이션의 척추를 이루며, 채용 담당자는 이를 통해 프레임워크에 대한 실제 숙련도를 평가합니다. > **면접관이 평가하는 것** > > Guard는 접근을 통제하고(누가 들어올 수 있는지), Interceptor는 데이터를 변환하며(무엇이 들어오고 나가는지), 모듈형 아키텍처는 모든 것을 정리합니다. 세 가지 모두를 능숙하게 다루면 기본 CRUD 연산을 넘어선 깊은 NestJS 이해를 보여줍니다. ## NestJS의 Guards 작동 방식과 흔한 면접 시나리오 Guards는 `CanActivate` 인터페이스를 구현하며, 요청이 핸들러에 도달할지를 결정합니다. 미들웨어와 달리 Guards는 NestJS 실행 컨텍스트(`ExecutionContext`)에 접근하여 핸들러 메타데이터에 기반한 인가 결정을 가능하게 합니다. 전형적인 질문: "사용자 역할을 검증하는 Guard를 구현해 주세요." ```typescript // roles.guard.ts 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('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()` 데코레이터는 다음과 같은 모습입니다. ```typescript // roles.decorator.ts 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를 만들어 주세요." ```typescript // logging.interceptor.ts 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 { 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`입니다. ```typescript // transform.interceptor.ts 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 { data: T; timestamp: string; path: string; } @Injectable() export class TransformInterceptor implements NestInterceptor> { intercept(context: ExecutionContext, next: CallHandler): Observable> { const request = context.switchToHttp().getRequest(); return next.handle().pipe( map((data) => ({ data, timestamp: new Date().toISOString(), path: request.url, })), ); } } ``` 이 표준화된 응답 패턴은 면접에서 좋은 점수를 받습니다. API 설계와 응답의 일관성에 대해 사고할 수 있는 능력을 보여주기 때문입니다. > **Guard vs Interceptor vs Middleware** > > Middleware: 원시 HTTP 처리(Express와 유사). Guard: 접근에 대한 불리언 결정. Interceptor: RxJS를 통한 요청/응답 스트림의 변환. 면접에서 이 세 개념을 혼동하는 것은 즉각적인 레드 플래그입니다. ## NestJS 모듈형 아키텍처: 확장 가능한 애플리케이션 구축 모듈형 아키텍처는 면접에서 주니어와 시니어 개발자를 가릅니다. NestJS는 모듈 기반 조직을 강제하지만, 진짜 질문은 아키텍처적 결정에 있습니다. 언제 모듈을 만들고 모듈 간 의존성을 어떻게 관리할 것인가입니다. 고급 질문: "적절히 분리된 모듈로 NestJS 이커머스 애플리케이션을 어떻게 구조화하시겠습니까?" ```typescript // orders/orders.module.ts 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`를 사용하는 것입니다. ```typescript // Resolving circular dependencies @Module({ imports: [forwardRef(() => PaymentsModule)], }) export class OrdersModule {} ``` 가장 강한 면접 답변은 `forwardRef`가 임시방편이지 해결책이 아니라고 설명합니다. 올바른 수정은 공유 모듈(`SharedOrderPaymentModule`)을 추출하도록 리팩터링하거나, 통신을 분리하기 위해 Event Emitter를 사용하는 것입니다. ## Dynamic Modules와 고급 설정 Dynamic Modules는 프로젝트 간에 재사용 가능한, 설정 가능한 모듈을 생성합니다. 이 패턴은 시니어 레벨 면접에서 등장합니다. 질문: "동적 옵션을 가진 설정 가능한 캐시 모듈을 만들어 주세요." ```typescript // cache/cache.module.ts 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()`를 통해 옵션을 주입합니다. ```typescript // cache/cache.service.ts import { Injectable, Inject } from '@nestjs/common'; import { CacheModuleOptions } from './cache.module'; @Injectable() export class CacheService { private store = new Map(); 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(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()`로 표시하는 것은 편리해 보이지만, 남용하면 보이지 않는 결합이 생깁니다. 면접에서 `@Global()`은 횡단 관심사 서비스(config, cache, logger)에 한정해야 한다고 언급하는 것은 아키텍처적 성숙도를 보여줍니다. ## 사용자 정의 데코레이터와 Guards 합성 사용자 정의 데코레이터는 여러 Guards와 메타데이터를 단일 어노테이션으로 결합합니다. 이 주제는 미드 시니어 직책에서 등장합니다. ```typescript // auth.decorator.ts 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), ); } ``` 컨트롤러에서의 사용 예시입니다. ```typescript // orders/orders.controller.ts 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 모듈](/technologies/node-nestjs/interview-questions/nestjs-modules-di)과 [Interceptors](/technologies/node-nestjs/interview-questions/middleware-interceptors)를 참고하시기 바랍니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/node-nestjs/nestjs-guards-interceptors-modular-architecture