TypeORM의 forFeature 반복 문제 해결하기 - 서비스의 SRP를 위한 Repository

Younha Lee·2025년 6월 4일

Mash-Up

목록 보기
7/7

서론

매쉬업 15기 kokkok 프로젝트를 하며 엔티티 설계를 하게 되었는데, 노드팀의 다른 프로젝트팀은 어떤식으로 설계했나 궁금해져서 레포를 뒤져보게 되었다.

이 코드 리뷰를 보며 뜨끔했던 게, 나도 원래 이렇게 구현하려고 했다.
다르게 할 수 있는 방법 자체를 생각하지 못했다.
이 리뷰를 처음 봤을 때 한 번에 이해가 잘 되지 않아서 열심히 공부하게 되었고, 생각의 변천사를 가져와서 여기에 남기려고 한다.

본론

우리가 해결하고 싶은 TypeOrmModule.forFeature를 사용했을 때의 문제점은 엔티티를 정의할 때마다 provider에 중복으로 넣어줘야한다는 것이다.

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './entities/post.entity';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';

@Module({
  imports: [
    // Post 엔티티의 Repository를 사용하기 위해 forFeature 호출
    TypeOrmModule.forFeature([Post]),
  ],
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [
    // User 엔티티의 Repository를 사용하기 위해 forFeature 호출
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

새로운 엔티티가 추가될 때마다 해당 모듈의 imports 배열에 TypeOrmModule.forFeature([Comment]) 와 같은 코드를 반드시 추가해야한다.
10개의 엔티티가 있다면 AppModule에 이미 TypeOrmModule.forRoot로 등록해놓고 각각의 모델에서 또 TypeOrmModule.forFeature()에 나열해야하는 것이다.

레포지토리가 뭐라고 특별 대우 받아야하나?

NestJS에서 PostsService 같이 일반적인 서비스들은 어떻게 DI 되나보자.
1. @Injectable() 데코레이터를 붙이고
2. 해당 모듈의 providers 배열에 클래스를 직접 등록하면, NestJS가 알아서 이 클래스의 인스턴스를 만들고 필요한 곳에 주입해준다.

레포지토리도 이렇게 providers 배열에 직접 등록할 수 있다면, TypeOrmModule.forFeature 를 보지 않아도 되는 것 아닐까?"

레포지토리를 NestJS의 표준적인 방식으로 관리해보자.

커스텀 레포지토리

TypeORM이 주는 기본 레포지토리를 상속하여 엔티티에 특화된 레포지토리를 만들고 @Injectable() 데코레이터를 붙이면 여타 다른 서비스 처럼 providers 배열에 등록할 수 있다.

// src/posts/posts.repository.ts
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Post } from './entities/post.entity';

@Injectable()
export class PostRepository extends Repository<Post> {
  constructor(private readonly dataSource: DataSource) {
    super(Post, dataSource.createEntityManager());
  }
}

이렇게 하면 모듈에는 providers로 등록할 수 있다.

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { PostRepository } from './posts.repository';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [
    PostsService,
    PostRepository,
  ],
  exports: [PostsService, PostRepository],
})
export class PostsModule {}

어차피 반복하는 것 아닌가요?

반복이 싫어서 시작한 건데 커스텀 레포지토리를 모든 엔티티마다 등록해야 한다면 그게 TypeOrm.forfeature() 반복이랑 뭐가 다른건가?

TypeOrm.forfeature() 은 특정 엔티티에 대한 TypeORM의 기본 Repository<T> 인스턴스를 NestJS의 DI 컨테이너에 주입 가능하게 만들기 위한 것이다.
Repository<T>는 기본 CRUD만 있는 인스턴스기 때문에, 엔티티에 대한 복잡한 조작이 필요한 경우, 해당 인스턴스를 주입받은 Service 레이어에서 따로 조작해야한다.

그런데 커스텀 레포지토리를 반복하는 행위는 우리가 직접 정의한, 특정 엔티티에 특화된 레포지토리를 NestJS의 일반 Provider로 등록하는 것이다.
그렇게 되면 해당 레포지토리를 주입받은 Service에서는 DB 조작과 같은 일을 레포지토리에 전적으로 위임할 수 있게 되는 것이다.
같은 반복이어도 서비스 레이어의 SRP를 지킬 수 있고,
TypeOrmModule.forFeature와 @InjectRepository를 통한 Repository 주입 없이, 직접적인 Provider로 활용할 수 있다는 것이 다른 것이다.

후기

그런데 여기까지 와보니 예전에 잠깐 공부한 JPA의 Repository 패턴이 생각났다.

물론 TypeORM은 Spring Data JPA처럼 메소드 이름을 기반해서 쿼리를 자동으로 만들어주진 않지만, '데이터 접근 로직을 서비스 계층에서 분리하여 Repository 계층에 캡슐화하고, 이를 의존성 주입을 통해 활용한다' 는 핵심과 아키텍처적 지향점이 같다는 걸 알게 되었다.

profile
할 땐 하고 놀 땐 노는 일일놀놀입니다.

0개의 댓글