목차5개 항목
NestJS의 뼈대는 두 가지다. Provider와 의존성 주입(Dependency Injection) 이 객체를 만들어 연결하고, Module이 그 객체들을 기능 단위로 묶어 공개 범위를 정한다.
Provider와 의존성 주입
컨트롤러는 서비스를 직접 new로 만들지 않는다. 생성자에 "이게 필요하다"고 선언만 하면 Nest가 알맞은 인스턴스를 넣어 준다.
| 비유 | NestJS |
|---|---|
| 소비자 | AppController: 서비스를 사용하는 쪽 |
| 제품 | AppService 인스턴스 |
| 공급자(Provider) | @Injectable()로 표시되고 모듈의 providers에 등록된 클래스 |
| 사업자 등록 | @Module({ providers: [AppService] }) |
@Injectable()
export class AppService {
getHello() { return 'Hello World!'; }
}
@Controller()
export class AppController {
// 생성자에 타입만 선언하면 Nest가 AppService 인스턴스를 주입한다
constructor(private readonly appService: AppService) {}
}
@Module({
controllers: [AppController],
providers: [AppService], // ← 여기 등록해야 주입 가능
})
export class AppModule {}providers에서 AppService를 빼면 앱 시작 시 "Nest can't resolve dependencies of the AppController" 에러가 난다. 공급자 등록이 없으니 소비자에게 제품을 줄 수 없는 것이다.
서비스뿐 아니라 리포지토리, 팩토리, 헬퍼도 Provider가 된다. @Injectable()이 붙은 미들웨어·인터셉터, 리포지토리, JWT Strategy 모두 같은 방식으로 주입된다.
| DI를 쓰는 이유 | 내용 |
|---|---|
| 결합도 감소 | 컨트롤러는 서비스의 생성 방법을 모른다. 구현을 바꿔도 사용하는 쪽은 그대로다 |
| 싱글톤 관리 | 기본 스코프에서 Provider는 앱 전체에서 하나의 인스턴스로 공유된다. Express에서 손으로 하던 싱글톤 패턴을 프레임워크가 대신 한다 |
| 테스트 용이성 | 테스트에서 실제 서비스 대신 가짜(mock) Provider를 주입하기 쉽다 |
Module: 기능 단위로 묶기
모듈은 기능 단위(예: cats, users, auth)로 컨트롤러와 프로바이더를 묶는 상자다. 루트 AppModule이 기능 모듈들을 imports해 앱 전체 구조가 트리 형태로 구성된다.
nest g mo cats # cats.module.ts
nest g co cats # cats.controller.ts (모듈에 자동 등록)
nest g s cats # cats.service.ts (모듈에 자동 등록)// cats/cats.module.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService], // 다른 모듈에서 쓰게 하려면 공개
})
export class CatsModule {}
// app.module.ts
@Module({
imports: [CatsModule, UsersModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}| 속성 | 의미 |
|---|---|
imports | 이 모듈이 사용할 다른 모듈 |
controllers | 이 모듈의 컨트롤러 |
providers | 이 모듈 안에서 생성·주입될 프로바이더 |
exports | 이 모듈을 import한 쪽에 공개할 프로바이더 |
캡슐화
모듈은 기본적으로 프로바이더를 캡슐화한다. 현재 모듈에 직접 등록했거나, import한 모듈이 export한 프로바이더만 주입할 수 있다.
CatsModule이exports: [CatsService]를 하지 않으면,AppModule이CatsModule을 import해도AppController에서CatsService를 주입받을 수 없다.- 다른 모듈의 서비스를 쓰고 싶다고 그 서비스를 내 모듈
providers에 다시 등록하면 별도의 인스턴스가 생긴다. 공유하려면 반드시 export/import로 가져온다.
캡슐화(encapsulation)란 객체의 속성과 행위를 하나로 묶고 내부 구현을 감추는 것이다. 모듈 단위로 공개 범위를 통제해 의존 관계를 명확히 한다.
순환 참조와 forwardRef
CatsModule이 AuthModule을 import하고, AuthModule도 CatsModule을 import하면 순환 의존이 생겨 Nest가 모듈을 해석하지 못한다. 양쪽을 forwardRef()로 감싸면 해결된다.
// auth.module.ts
@Module({
imports: [forwardRef(() => CatsModule)],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
// cats.module.ts
@Module({
imports: [forwardRef(() => AuthModule)],
providers: [CatsService, CatsRepository],
exports: [CatsService, CatsRepository],
})
export class CatsModule {}
forwardRef는 응급 처치에 가깝다. 공통으로 필요한 부분(예: DB 접근)을 Repository나 별도 모듈로 분리해 순환 자체를 없애는 설계가 더 바람직하다.
핵심 정리
@Injectable()+ 모듈의providers등록 = 주입 가능한 Provider. 생성자에private readonly x: XService로 선언하면 주입된다.- 기능 단위로 모듈을 만들고
AppModule에서 import한다. - 다른 모듈에서 쓰일 프로바이더는
exports에 넣는다. 다시providers에 등록하면 인스턴스가 따로 생긴다. - 모듈 간 순환 의존은
forwardRef(() => Module)로 풀 수 있지만, 구조 개선이 우선이다.
관련 노트: NestJS 프로젝트 구조와 실행 흐름, Repository 패턴과 레이어 분리, Express 싱글톤 패턴과 서비스 패턴