02. 의존성 주입을 통한 모듈 분리

작성일:2024.06.27|조회수:0

02. 의존성 주입을 통한 모듈 분리

모듈(Module), 프로바이더(Provider), 그리고 컨트롤러(Controller)는 Nest.js를 구성하는 핵심 요소이다. 모듈은 기능 단위로 코드를 그룹화하는 역할을 하며, 프로바이더는 애플리케이션의 핵심 비즈니스 로직을 담당한다. 컨트롤러는 클라이언트의 요청을 받아 적절한 프로바이더를 호출하고, 그 결과를 응답으로 반환하는 역할을 수행한다.

Nest.js는 이러한 구성 요소들을 의존성 주입(Dependency Injection) 패턴을 통해 유기적으로 연결한다. 이를 통해 복잡한 서비스 계층을 효과적으로 분리할 수 있으며, 각 계층 간의 결합도를 낮출 수 있다. 그 결과 애플리케이션의 모듈화 수준이 높아지고, 코드의 재사용성과 테스트 용이성 역시 자연스럽게 향상된다.

프로바이더

프로바이더는 @Injectable() 데코레이터로 정의되며, 주로 데이터베이스 접근(repository), 외부 API 호출, 복잡한 비즈니스 로직 처리(service)와 같은 책임을 가진다. 핵심 로직을 프로바이더로 분리함으로써 컨트롤러는 요청과 응답 처리에만 집중할 수 있게 된다.

이러한 역할 분리는 컨트롤러의 책임을 최소화하고, 전체 코드베이스의 가독성과 테스트 용이성을 크게 높인다. 프로바이더는 사용 목적과 역할에 따라 Service, Repository, Factory, Helper 등 다양한 형태로 구현될 수 있다.

TS
@Injectable()
export class BoardsService {
  constructor(
    @InjectModel(Board.name)
    private BoardModel: Model<Board>,
  ) {}

  createBoard(createBoardDTO: CreateBoardDTO) {
    return new this.BoardModel(createBoardDTO).save();
  }
}

컨트롤러

컨트롤러는 애플리케이션의 엔드포인트를 정의하고, 클라이언트로부터 전달된 HTTP 요청을 처리하는 역할을 한다. @Controller() 데코레이터로 선언되며, 각 메서드는 @Get(), @Post(), @Put(), @Delete()와 같은 HTTP 메서드 데코레이터를 통해 특정 요청 유형과 매핑된다.

Nest.js에서는 미들웨어, 인터셉터, 파이프 등을 조합하여 요청 처리 과정에 다양한 부가 로직을 유연하게 삽입할 수 있다. 라우팅 경로는 와일드카드를 포함해 정의할 수 있으며, @Req, @Query, @Param, @Body 데코레이터를 사용해 요청 데이터를 명시적으로 추출한다.

응답 처리 시에는 @Res, @HttpCode, 예외 처리 메커니즘을 활용할 수 있으며, @Header를 통해 응답 헤더를 설정하거나 @Redirect를 사용해 300번대 응답을 반환할 수도 있다.

TS
@Controller('boards')
export class BoardsController {
  constructor(private boardService: BoardsService) {}

  @Post()
  createBoard(@Body() createBoardDTO: CreateBoardDTO) {
    return this.boardService.createBoard(createBoardDTO);
  }

  @Get('/:id')
  getBoardById(@Param('id') id: MongoIdDTO) {
    return this.boardService.getBoardById(id);
  }
}

모듈

모듈은 애플리케이션을 구성하는 가장 기본적인 구조 단위로, 관련된 컨트롤러와 프로바이더를 하나의 논리적 묶음으로 관리한다. @Module() 데코레이터를 사용해 정의하며, imports, controllers, providers, exports 속성을 통해 모듈의 구성 요소를 명시한다.

이러한 구조를 통해 애플리케이션의 복잡도를 효과적으로 낮출 수 있으며, 기능 단위로 명확한 경계를 가진 아키텍처를 설계할 수 있다. 결과적으로 코드의 재사용성과 유지보수성이 크게 향상된다.

PY
@Module({
  controllers: [BoardsController],
  providers: [BoardsService],
  imports: [
    MongooseModule.forFeature([
      { name: Board.name, schema: BoardSchema },
    ]),
  ],
})
export class BoardsModule {}

모듈은 imports 속성을 통해 다른 모듈의 기능을 사용할 수 있으며, 외부로 공개하고자 하는 프로바이더나 모듈은 exports 배열에 명시적으로 선언해야 한다. exports에 포함되지 않은 구성 요소는 기본적으로 해당 모듈 내부에서만 사용 가능하다.

Re-exporting

Re-exporting은 가져온 모듈을 다시 외부로 내보내는 방식으로, 모듈 간의 계층 구조를 보다 유연하게 설계할 수 있게 해준다. 이를 활용하면 여러 모듈을 간결하게 조합하면서도 의존성 관리를 명확히 할 수 있다.

아래 코드는 개념을 설명하기 위한 예시이다. BoardsModuleAuthModule을 가져오고, CommentModule은 다시 BoardsModule을 가져온다. 이때 BoardsModuleAuthModule을 re-export 하고 있기 때문에, CommentModuleAuthModule을 직접 import 하지 않더라도 AuthService를 사용할 수 있다.

TS
@Module({
  controllers: [AuthController],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

@Module({
  controllers: [BoardsController],
  providers: [BoardsService],
  imports: [AuthModule],
  exports: [AuthModule],
})
export class BoardsModule {}

@Module({
  providers: [CommentService],
  exports: [CommentService],
  imports: [BoardsModule],
})
export class CommentModule {}

실제 프로젝트에서는 이런 단순한 형태보다는, 개념적인 포함 관계를 표현하는 데 re-exporting이 주로 사용된다. 예를 들어 가장 핵심적인 기능을 담은 CoreModule이 있고, 그보다는 덜 코어하지만 애플리케이션 전반에서 공통으로 사용되는 CommonModule이 있다고 가정해보자.

물론 두 모듈을 모두 글로벌 모듈로 선언하는 것도 하나의 선택지가 될 수 있다. 하지만 CommonModuleCoreModule을 re-export 하도록 구성하면, CommonModule만 글로벌 모듈로 선언해도 동일한 효과를 얻을 수 있다. Nest.js 공식 문서에서도 모든 것을 글로벌 모듈로 만드는 것은 좋은 설계 패턴이 아니라고 언급하며, 글로벌 모듈은 항상 일정 수준의 리스크를 수반한다고 설명한다.

이처럼 re-exporting을 활용하면 글로벌 모듈의 사용을 최소화하면서도, 구조적으로는 여러 모듈을 전역에서 사용하는 것과 유사한 효과를 얻을 수 있다.

더 읽어보기

  • 2025.05.21

    14. Redis를 사용한 세션 관리 및 캐싱

    새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…

  • 2025.01.30

    비즈니스 로직은 어디에 있어야 할까

    Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…

  • 2024.12.07

    13. Jest 테스트 구성

    이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…

  • 2024.11.14

    12. GraphQL로 요청 처리

    GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP…

  • 2026.04.11

    Trie 자료구조

    문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…

  • 2026.03.19

    Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까

    웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…

댓글

댓글을 불러오는 중...