
1. 들어가며: 텅 빈 클래스의 미스터리
NestJS 프로젝트를 처음 만들고 파일을 살펴보면 이상한 점이 하나 눈에 띈다. 애플리케이션의 핵심 단위인 모듈(Module) 클래스가 대부분 비어 있다는 점이다. UserModule, AppModule 같은 파일을 열어보면 클래스 안에는 로직도 없고 변수도 없다. 이 모습을 보면 이런 의문이 든다.
“아무 코드도 없는 이 클래스가 어떻게 앱의 중심 역할을 하지?”
이 빈 클래스에는 NestJS의 핵심 설계 사상이 담겨 있다.
의존성 관리, 모듈 간 경계, 그리고 유연한 확장을 가능하게 하는 구조다.
2. 첫 번째 발견: 클래스는 이름표, 데코레이터는 설계도
모듈 클래스 내부가 비어 있는 이유는 이 클래스가 직접 기능을 수행하지 않기 때문이다. 모듈 클래스는 NestJS가 애플리케이션 구조를 그릴 때 사용하는 고유 식별자(Token) 역할만 한다. 실제 중요한 정보는 모두 클래스 위에 붙은 @Module 데코레이터에 들어 있다.
@Module({
providers: [UserService],
controllers: [UserController],
})
export class UserModule {}
이 구조는 포스트잇에 비유할 수 있다.
- 클래스: “이 상자의 이름은 UserModule”
- @Module: 상자에 붙은 상세 설명서
NestJS는 reflect-metadata를 이용해 이 데코레이터 정보를 읽고 내부에 저장한다. 이때 사용하는 구조가 ModuleMetadata다.
ModuleMetadata 예시
export interface ModuleMetadata {
providers?: Provider[];
controllers?: Type<any>[];
imports?: Array<Type<any> | DynamicModule>;
exports?: Array<Provider | string | symbol>;
}
중요한 점은 다음이다.
- 정보는 클래스 내부에 저장되지 않는다
- NestJS 내부 메타데이터 저장소에 따로 관리된다
그래서 클래스 본문은 비어 있어도 전혀 문제가 없다.
3. 두 번째 발견: NestJS가 확인하는 4가지 핵심 정보
@Module 데코레이터에는 NestJS가 모듈을 조립할 때 반드시 확인하는 네 가지 속성이 있다.
| 속성 | 의미 | 비유 |
| providers | NestJS가 생성·관리하는 객체 | 실제 일을 하는 일꾼 |
| controllers | HTTP 요청을 처리하는 클래스 | 안내 데스크 |
| imports | 이 모듈이 의존하는 다른 모듈 | 빌려온 도구 |
| exports | 외부에 공개할 provider | 다른 팀으로 보내는 인력 |
핵심 개념: 캡슐화
NestJS 모듈은 기본적으로 닫혀 있다.
@Module({
providers: [UserService],
})
export class UserModule {}
이 상태에서는 UserService를 다른 모듈에서 사용할 수 없다. 외부에서 쓰게 하려면 반드시 exports에 명시해야 한다.
@Module({
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
이 설계 덕분에 얻는 효과는 명확하다.
- 모듈 간 의존성이 통제된다
- 실수로 내부 구현을 노출하지 않는다
- 결합도가 낮아진다
4. 실무에서 가장 많이 겪는 실수: exports 누락
NestJS 초기에 가장 자주 만나는 에러는 이것이다.
Nest can’t resolve dependencies of XXXService
대표적인 원인은 exports 누락이다.
잘못된 예
@Module({
providers: [UserService],
})
export class UserModule {}
@Module({
imports: [UserModule],
})
export class OrderModule {
constructor(private userService: UserService) {}
}
이 경우 UserService를 찾을 수 없다는 에러가 발생한다. UserModule이 해당 서비스를 외부에 공개하지 않았기 때문이다. 이 문제 하나로 모듈 캡슐화 개념이 정리된다..
5. 세 번째 발견: 보이지 않는 손, 의존성 주입의 3단계
클래스 내부가 비어 있어도 애플리케이션이 동작하는 이유는 NestJS가 IoC 컨테이너를 통해 객체를 대신 관리하기 때문이다. 이 과정은 세 단계로 나뉜다.
1단계: 스캔
NestJS가 모든 모듈을 순회하며 @Module 메타데이터를 읽는다.
2단계: 기록
읽은 정보를 IoC 컨테이너에 저장한다.
- 어떤 모듈에
- 어떤 provider가 있고
- 어떤 의존성이 필요한지
3단계: 조립
필요한 객체를 싱글톤으로 생성하고, 의존성이 필요한 클래스 생성자에 자동으로 주입한다.
DI 흐름 도표

이 구조 덕분에 개발자는 new 키워드를 직접 쓸 일이 거의 없다.
6. 네 번째 발견: 정적 모듈과 동적 모듈
NestJS에서는 두 가지 형태의 모듈을 볼 수 있다.
정적 모듈
@Module({
providers: [UserService],
})
export class UserModule {}
- 구성 요소가 코드 작성 시점에 고정된다
- 항상 동일한 형태로 동작한다
동적 모듈
ConfigModule.forRoot({
isGlobal: true,
});
- 실행 시점에 설정값을 받아 구성된다
- 내부적으로 DynamicModule 객체를 반환한다
중요한 점은 이것이다.
NestJS는 정적 모듈과 동적 모듈을 같은 방식으로 처리한다.
차이는 메타데이터를 미리 적어두었는지, 실행 중에 만들어냈는지뿐이다.
마무리: 빈 상자가 만드는 유연함
NestJS 모듈 클래스가 비어 있는 이유는 명확하다.
- 역할을 명확히 나누기 위해서
- 구조 정의와 로직을 분리하기 위해서
모듈은 관계만 정의한다. 실제 동작은 서비스와 컨트롤러가 담당한다. 이 설계는 다음을 가능하게 한다.
- 테스트 시 mock 교체가 쉽다
- 모듈 구성을 유연하게 바꿀 수 있다
- 애플리케이션 규모가 커져도 관리가 쉽다
클래스 안의 빈 공간은 결핍이 아니다. NestJS가 대신 채우는 공간이며, 확장을 위한 여백이다.
'잡(job)기술' 카테고리의 다른 글
| RTSP 클라이언트를 만들기 전에 꼭 해야 할 준비― mediamtx + FFmpeg으로 테스트 환경 먼저 만들기 (3) | 2026.02.21 |
|---|---|
| SVN Merge 실수 줄이는 법: 체리픽부터 mergeinfo까지 실전 전략 5가지 (0) | 2026.02.11 |
| 데이터베이스 성능의 핵심, 복합 인덱스를 이해하는 4가지 포인트 (1) | 2026.02.04 |
| 데이터 무결성을 지키는 마지막 방어선 - TypeORM Entity 설계의 5가지 핵심 포인트 (3) | 2026.02.03 |
| Nginx 설정, 이 5가지만 알면 ‘고수’ 소리 듣는다 (0) | 2026.02.03 |