본문으로 건너뛰기
Advertisement

14.3 의존성 주입 — NestJS DI 시스템과 Provider 타입

의존성 주입(DI) 개요

NestJS의 DI 컨테이너는 객체 생성과 의존성 관리를 자동화합니다. @Injectable() 데코레이터로 서비스를 등록하고, 생성자 주입으로 의존성을 해결합니다.

// 의존성 주입 흐름
// Module → DI Container → Controller/Service
// ↑
// Provider 등록 및 주입

Provider 타입

useClass (기본)

// users/users.module.ts
import { Module } from '@nestjs/common'
import { UsersService } from './users.service'

@Module({
providers: [
UsersService, // 축약형 — { provide: UsersService, useClass: UsersService }
],
})
export class UsersModule {}

useValue

// 설정값이나 상수 주입
const DB_CONFIG = {
host: 'localhost',
port: 5432,
database: 'mydb',
}

@Module({
providers: [
{
provide: 'DB_CONFIG',
useValue: DB_CONFIG,
},
],
})
export class DatabaseModule {}

// 사용
@Injectable()
export class DatabaseService {
constructor(@Inject('DB_CONFIG') private config: typeof DB_CONFIG) {}
}

useFactory

// 동적으로 생성되는 Provider
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
return createConnection({
url: configService.get('DATABASE_URL'),
})
},
inject: [ConfigService], // 의존성 주입
},
],
})
export class DatabaseModule {}

인터페이스 기반 DI

// interfaces/user-repository.interface.ts
export interface IUserRepository {
findById(id: string): Promise<User | null>
findAll(): Promise<User[]>
create(data: CreateUserDto): Promise<User>
update(id: string, data: UpdateUserDto): Promise<User>
delete(id: string): Promise<void>
}

// 토큰 정의
export const USER_REPOSITORY = 'USER_REPOSITORY'
// repositories/user.repository.ts
import { Injectable } from '@nestjs/common'
import { IUserRepository } from '../interfaces/user-repository.interface'

@Injectable()
export class UserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
// 실제 DB 쿼리
return null
}
// ... 나머지 메서드
}
// users.module.ts
@Module({
providers: [
{
provide: USER_REPOSITORY,
useClass: UserRepository,
},
UsersService,
],
})
export class UsersModule {}

// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepository: IUserRepository
) {}
}

스코프 (Scope)

import { Injectable, Scope } from '@nestjs/common'

// DEFAULT: 싱글톤 (기본값) — 앱 전체에서 하나의 인스턴스
@Injectable()
export class SingletonService {}

// REQUEST: 요청마다 새 인스턴스 생성
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
// 요청별 상태 저장 가능
}

// TRANSIENT: 주입될 때마다 새 인스턴스
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}

인터셉터와 파이프 타입

인터셉터 (Interceptor)

import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map, tap } from 'rxjs/operators'

// 응답 변환 인터셉터
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<{ data: T }> {
return next.handle().pipe(
map(data => ({ data })) // 응답을 { data: ... } 형식으로 감싸기
)
}
}

// 로깅 인터셉터
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest()
const { method, url } = req
const start = Date.now()

return next.handle().pipe(
tap(() => {
const duration = Date.now() - start
console.log(`${method} ${url} ${duration}ms`)
})
)
}
}

커스텀 파이프

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToInstance } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<unknown> {
async transform(value: unknown, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value
}

const object = plainToInstance(metatype, value)
const errors = await validate(object)

if (errors.length > 0) {
throw new BadRequestException('Validation failed')
}

return object
}

private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}

가드 (Guard)

import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { JwtService } from '@nestjs/jwt'

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

async canActivate(context: ExecutionContext): Promise<boolean> {
// Public 데코레이터 확인
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
])
if (isPublic) return true

const request = context.switchToHttp().getRequest()
const token = request.headers.authorization?.replace('Bearer ', '')

if (!token) {
throw new UnauthorizedException('인증이 필요합니다.')
}

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

고수 팁

커스텀 데코레이터

import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common'

// 현재 사용자 추출 데코레이터
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.user
}
)

// Public 라우트 데코레이터
export const Public = () => SetMetadata('isPublic', true)

// 역할 데코레이터
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// 사용
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@CurrentUser() user: AuthUser) {
return user
}

@Get('public-data')
@Public()
getPublicData() {
return { message: '누구나 접근 가능' }
}
Advertisement