모듈, 컨트롤러, 서비스의 기본 개념
NestJS는 모듈, 컨트롤러, 서비스라는 세 가지 핵심 요소를 통해 애플리케이션을 구조화합니다.
이들의 적절한 활용은 확장 가능하고 유지보수가 용이한 애플리케이션 개발의 기반이 됩니다.
모듈 (Modules)
모듈은 NestJS 애플리케이션의 기본 구성 단위입니다.
관련된 기능을 그룹화하고 구조화하는 데 사용됩니다.
@Module 데코레이터
모듈은 @Module
데코레이터를 사용하여 정의되며 다음과 같은 속성을 가집니다.
imports
: 이 모듈에서 사용할 다른 모듈들을 지정controllers
: 이 모듈에 속한 컨트롤러들을 정의providers
: 이 모듈에서 사용할 서비스나 다른 프로바이더들을 정의exports
: 이 모듈에서 다른 모듈로 내보낼 프로바이더들을 지정
예시
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
컨트롤러 (Controllers)
컨트롤러는 들어오는 요청을 처리하고 클라이언트에 응답을 반환하는 역할을 합니다.
기본 구조와 라우팅
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll(): Promise<User[]> {
return this.userService.findAll();
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.userService.create(createUserDto);
}
}
이 예시에서 @Controller('users')
데코레이터는 '/users' 경로에 대한 라우팅을 정의합니다.
@Get()
및 @Post()
데코레이터는 각각 GET 및 POST 요청을 처리하는 메소드를 지정합니다.
서비스 (Services)
서비스는 비즈니스 로직을 포함하며 컨트롤러와 데이터 계층 사이의 중간자 역할을 합니다.
서비스 구현 예시
@Injectable()
export class UserService {
constructor(@InjectRepository(User) private userRepository: Repository<User>) {}
async findAll(): Promise<User[]> {
return this.userRepository.find();
}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
}
여기서 @Injectable()
데코레이터는 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리됨을 나타냅니다.
의존성 주입 (Dependency Injection)
NestJS는 의존성 주입을 통해 컴포넌트 간의 결합도를 낮추고 테스트 용이성을 높입니다.
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
// ...
}
이 방식으로 UserService
가 UserController
에 자동으로 주입됩니다.
모듈 간 의존성 관리
모듈 간 의존성은 imports
배열을 통해 관리됩니다.
순환 종속성 문제를 피하기 위해 다음과 같은 전략을 사용할 수 있습니다.
- 공통 모듈 생성
forwardRef()
함수 사용- 이벤트 기반 통신 활용
글로벌 모듈과 동적 모듈
글로벌 모듈
전체 애플리케이션에서 공유되어야 하는 모듈은 글로벌 모듈로 선언할 수 있습니다.
@Global()
@Module({
providers: [CommonService],
exports: [CommonService],
})
export class CommonModule {}
동적 모듈
동적 모듈은 실행 시점에 동적으로 모듈을 구성할 수 있게 해줍니다.
@Module({})
export class ConfigModule {
static forRoot(options: ConfigOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
Best Practices와 주의사항
- 단일 책임 원칙 준수 : 각 모듈, 컨트롤러, 서비스는 명확히 정의된 하나의 책임을 가져야 합니다.
- 모듈의 적절한 분리 : 기능 단위로 모듈을 분리하여 코드의 재사용성과 유지보수성을 높입니다.
- 의존성 주입 활용 : 직접적인 인스턴스 생성 대신 의존성 주입을 사용하여 결합도를 낮춥니다.
- 비즈니스 로직은 서비스에 : 컨트롤러는 요청 처리와 응답 반환에 집중하고, 복잡한 비즈니스 로직은 서비스에 구현합니다.
- 적절한 예외 처리 : 각 계층에서 발생할 수 있는 예외를 적절히 처리하고 전파합니다.
- 테스트 용이성 고려 : 각 컴포넌트를 설계할 때 단위 테스트의 용이성을 고려합니다.
- 순환 종속성 주의 : 모듈 간 순환 종속성을 피하고, 필요한 경우 적절한 해결 전략을 사용합니다.
- 글로벌 모듈 남용 주의 : 글로벌 모듈은 필요한 경우에만 제한적으로 사용합니다.
- 동적 모듈의 적절한 활용 : 구성의 유연성이 필요한 경우에 동적 모듈을 활용합니다.
- 명확한 네이밍 규칙 : 모듈, 컨트롤러, 서비스의 이름은 그 역할을 명확히 나타내야 합니다.
NestJS의 모듈, 컨트롤러, 서비스는 애플리케이션의 구조와 로직을 체계적으로 조직하는 데 핵심적인 역할을 합니다.
이들을 효과적으로 활용함으로써 확장 가능하고 유지보수가 용이한 애플리케이션을 개발할 수 있습니다.
모듈은 관련 기능을 그룹화하고, 컨트롤러는 HTTP 요청을 처리하며, 서비스는 비즈니스 로직을 캡슐화합니다.
의존성 주입 시스템은 이들 컴포넌트 간의 결합도를 낮추고 테스트 용이성을 높입니다.