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: '누구나 접근 가능' }
}