개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

NestJS Dependency Injection — 의존성을 자동으로 주입하는 방식

2025년 2월 27일
NestJSDIDependency InjectionIoCSingleton

NestJS를 처음 접하면 @Injectable, @Inject 같은 데코레이터가 낯설게 느껴진다. 하지만 이 데코레이터들이 바로 NestJS의 핵심인 Dependency Injection 시스템을 구성하는 요소다. DI를 이해하면 왜 NestJS 코드가 그런 구조를 갖는지 자연스럽게 납득하게 된다.

기존 방식 vs DI

전통적인 방식에서는 클래스 내부에서 직접 new 키워드로 의존 객체를 생성한다.

// 기존 방식 — 강한 결합
class OrderService {
  private userService = new UserService(); // 직접 생성
}

이 방식은 OrderService가 UserService의 생성 방식까지 알고 있어야 한다. UserService의 생성자 시그니처가 바뀌면 OrderService도 함께 수정해야 한다.

Dependency Injection은 이 책임을 외부로 넘긴다. 클래스는 "나는 이 타입이 필요하다"고 선언만 하고, 실제 인스턴스 생성은 IoC Container가 담당한다.

// DI 방식 — 느슨한 결합
@Injectable()
class OrderService {
  constructor(private readonly userService: UserService) {} // 주입받음
}

IoC(Inversion of Control): 제어의 역전. 객체 생성과 생명주기 관리의 주체를 개발자에서 프레임워크(Container)로 넘기는 설계 원칙.

IoC Container의 동작 흐름

  앱 부트스트랩
       │
       ▼
  NestJS IoC Container
       │
       ├── @Injectable() 클래스 스캔
       ├── 의존성 그래프 생성
       └── 인스턴스 생성 & 주입
             │
             ▼
      Module A          Module B
    ┌──────────┐       ┌──────────┐
    │Controller│       │Controller│
    │    │     │       │    │     │
    │  Service │       │  Service │
    └──────────┘       └──────────┘

NestJS는 앱이 시작될 때 모든 @Injectable 클래스를 스캔하고, TypeScript의 메타데이터를 통해 생성자 파라미터 타입을 읽어 의존성 그래프를 구성한다. 이후 필요한 순서대로 인스턴스를 생성해 주입한다.

Constructor Injection

NestJS에서 권장하는 주입 방식은 Constructor Injection이다.

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.userRepository.find();
  }
}

생성자 파라미터로 의존성을 선언하면, Container가 해당 타입의 인스턴스를 찾아 자동으로 넘겨준다. readonly를 사용하면 주입 이후 재할당을 방지할 수 있다.

Singleton 스코프

스코프동작사용 시점
DEFAULT (Singleton)앱 전체에서 인스턴스 1개 공유대부분의 Service
REQUESTHTTP 요청마다 새 인스턴스 생성요청별 상태가 필요할 때
TRANSIENT주입될 때마다 새 인스턴스 생성격리된 상태가 필요할 때

기본 스코프인 Singleton은 메모리 효율이 높고 초기화 비용이 한 번뿐이다. 다만 인스턴스가 공유되므로 내부에 가변 상태를 두면 안 된다.

DI의 실질적 장점

결합도 감소: OrderService는 UserService가 어떻게 만들어지는지 모른다. 구현체가 바뀌어도 OrderService 코드는 그대로다.

테스트 용이: 테스트 시 실제 구현체 대신 Mock을 주입할 수 있다.

const module = await Test.createTestingModule({
  providers: [
    OrderService,
    { provide: UserService, useValue: mockUserService }, // Mock 주입
  ],
}).compile();

Container가 의존성 생성을 관리하기 때문에, 테스트 환경에서 원하는 구현체로 교체하는 것이 간단하다. 이것이 NestJS에서 단위 테스트가 자연스럽게 구조화되는 이유다.

← 이전 글NestJS Provider — DI 컨테이너가 관리하는 객체
다음 글 →NestJS Decorator — 클래스와 메서드에 메타데이터를 추가하는 문법