본문 바로가기
아티클

사라진 데코레이터, @EntityRepository (by. TypeORM 0.3.X)

by imsoncod 2023. 7. 18.

개요

TypeORM 0.3.X 버전부터 @EntityRepository 데코레이터가 삭제되었다 (따로 커스텀을 하면 사용이 가능하다고는 한다) 해당 데코레이터가 어떤 역할을 했고, 왜 사라지게 되었는지 알아보자.

@EntityRepository 란?

지금까지 Custom Repository 패턴을 사용할 때, 아래와 같이 @EntityRepository 데코레이터를 사용했었다.

@EntityRepository(User)
class UserRepository extends Repository<User> {}

@EntityRepository 데코레이터는 매개변수로 들어온 target(User 클래스)를 TypeORM 모듈 내 존재하는 MetadataArgsStorage 클래스의 배열형 변수인 entityRepositories 에 저장하는 역할을 한다.

// TypeORM, EntityRepository.ts
export function EntityRepository(
  entity?: Function | EntitySchema<any>,
): ClassDecorator {
  return function (target: Function) {
    getMetadataArgsStorage().entityRepositories.push({
      target: target,
      entity: entity,
    } as EntityRepositoryMetadataArgs)
  }
}

getMetadataArgsStorage() 함수 로직을 보면 아래와 같이 구현되어 있다. 전역 스코프에서 MetadataArgsStorage 클래스 객체를 찾아 있으면 반환하고 없으면 생성해서 반환한다. 싱글톤 패턴이 적용되어 있음을 알 수 있다.

// TypeORM, global.ts
export function getMetadataArgsStorage(): MetadataArgsStorage {
  const globalScope = PlatformTools.getGlobalVariable()
  if (!globalScope.typeormMetadataArgsStorage)
    globalScope.typeormMetadataArgsStorage = new MetadataArgsStorage()

  return globalScope.typeormMetadataArgsStorage
}

MetadataArgsStorage 클래스 내부를 보면 아래와 같이 구현되어 있다. 엔티티 / 트랜잭션 / 컬럼 / 릴레이션 / 제약조건 등과 같은 전체적인 메타데이터를 관리하고 있음을 확인할 수 있다 (처음 Entity 클래스가 저장되는 배열형 변수 entityRepositories 가 여기에 선언되어 있다)

// TypeORM, MetadataArgsStorage.ts
export class MetadataArgsStorage {
  // Properties (일부 생략)
  readonly tables: TableMetadataArgs[] = []
  readonly trees: TreeMetadataArgs[] = []
  readonly entityRepositories: EntityRepositoryMetadataArgs[] = []
  readonly transactionEntityManagers: TransactionEntityMetadataArgs[] = []
  readonly transactionRepositories: TransactionRepositoryMetadataArgs[] = []
  readonly namingStrategies: NamingStrategyMetadataArgs[] = []
  readonly entitySubscribers: EntitySubscriberMetadataArgs[] = []
  readonly uniques: UniqueMetadataArgs[] = []
  readonly checks: CheckMetadataArgs[] = []
  readonly exclusions: ExclusionMetadataArgs[] = []
  readonly columns: ColumnMetadataArgs[] = []
  readonly generations: GeneratedMetadataArgs[] = []
  readonly relations: RelationMetadataArgs[] = []
  readonly joinColumns: JoinColumnMetadataArgs[] = []
  readonly joinTables: JoinTableMetadataArgs[] = []
  readonly entityListeners: EntityListenerMetadataArgs[] = []

  // Public Methods
  // ...
}

entityRepositories 변수에 저장된 Entity 클래스들은 getCustomRepository() 와 같이 특정 Entity의 Repository 를 받아오는 함수 내에서 활용된다. 로직을 보면 entityRepositories 변수에서 매개변수의 타입 T와 일치하는 요소를 찾아 반환하는 것을 확인할 수 있다 (즉, entityRepositories는 Entity 클래스 정보가 담겨있는 Storage 역할을 하는 것이다)

// TypeORM, EntityManager.ts
getCustomRepository<T>(customRepository: ObjectType<T>): T {
  const entityRepositoryMetadataArgs =
    getMetadataArgsStorage().entityRepositories.find((repository) => {
      return (
        repository.target ===
          (typeof customRepository === "function"
            ? customRepository
            : (customRepository as any).constructor)
          )
    })

  // Logic
  // ...
}

Active Record vs Data Mapper

두 패턴 모두 어플리케이션 서버의 코드 레벨에서 데이터베이스와 통신하는 방법에 대해 정의하고 있다. TypeORM 에서 각 패턴의 구현 방식과 특징을 살펴보자.

Active Record 패턴

Active Record 패턴모델 내에서 데이터베이스에 직접 접근하는 방식이다. 말이 어려우니 바로 코드 레벨로 들어가보자.

// User Entity
@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  email: string

  @Column()
  password: string

  @Column()
  name: string

  static async findOneByEmail(email: string): Promise<User> {
    return this.findOne({ email })
  }
}

// User Service
const user = await User.findOneByEmail(email)

TypeORM 에서 Active Record 패턴은 위처럼 구현할 수 있으며, 특징은 아래와 같다.

  • Entity 클래스를 통해 데이터베이스에 직접 접근한다.
  • BaseEntity 클래스를 상속 받아 TypeORM 에서 지원하는 표준 쿼리메소드를 사용할 수 있다.
  • Custom 쿼리메소드를 Entity 클래스 내에 static 하게 정의한다.

추가적인 Layer 가 생기지 않아 단순하고 빠르게 코드를 작성할 수 있으며, 유지보수 난이도가 낮다는 장점이 있다. 반면에, 쿼리메소드와 비례하여 Entity 사이즈가 커질 수 있다는 단점도 있다.

Data Mapper 패턴

Data Mapper 패턴은 Entity 외 Repository 혹은 Mapper 라고 불리는 별도의 클래스를 통해 데이터베이스 접근하는 방식이다. TypeORM 에서는 Repository 를 Custom 한다고 하여, Custom Repository 패턴이라고 부르기도 한다.

// User Repository
@EntityRepository(User)
class UserRepository extends Repository<User> {
  async findOneByEmail(email: string) {
    return this.findOne({ email })
  }
}

TypeORM 에서 Data Mapper 패턴은 위처럼 구현할 수 있으며, 특징은 아래와 같다.

  • 새로 생성한 Repository 클래스를 통해 데이터베이스에 접근한다.
  • Repository 클래스를 상속 받아 TypeORM 에서 지원하는 표준 쿼리메소드를 사용할 수 있다.
  • Custom 쿼리메소드를 Repository 클래스 내에 정의한다.

Active Record 패턴과 다르게 스키마와 쿼리메소드를 별도의 클래스(파일)로 관리하여 역할을 확실하게 분리할 수 있으며, 유지보수성이 높다는 장점이 있다. 반면에, Entity 수와 비례하여 파일 개수가 많아지고 매핑을 위해 추가적인 코드를 작성해야 한다는 단점도 있다.

@EntityRepository 가 사라진 이유

TypeORM 을 사용하는 대다수의 개발자들이 Service Layer 에는 비즈니스 로직만을 작성하고 Repository Layer 에는 쿼리 로직을 작성한다 (Data Mapper 패턴을 추구한다) 아마 단일 책임 원칙을 지키기 위해 or 단순히 파일간 역할을 분리해 코드를 작성하면 직관적이니깐 그러는 것 같다.

그러다보니... 아래와 같이 메소드를 불필요하게 생성해 사용하는 케이스가 생기기 시작했다.

// User Repository
@EntityRepository(User)
class UserRepository extends Repository<User> {
  async getAllUsers() {
    return this.find()
  }
}

모든 User 를 조회하는 로직을 작성하는데, TypeORM 에서 표준으로 지원하는 find() 메소드가 있음에도 불구하고 동일한 역할을 하는 getAllUsers() 라는 메소드를 Repository 클래스 내부에 생성했다. 추상화 레벨이 올라가기는 하겠다만, 꼭 Repository Layer 에 이렇게 생성해야하는지는.. 잘 모르겠다 + 메소드명이 치환되어 코드 가독성이 더 올라간다? 흠.. 이것도 잘 모르겠다.

위와 관련하여 여러 개발자들이 엄격한 Data Mapper 패턴의 사용이 좋지 않다는 목소리를 냈고, 이에 TypeORM 관리팀은 @EntityRepository 데코레이터를 Deprecated 하며 Active Record 패턴을 권장하는 듯한 뉘앙스를 풍기는 메시지를 남겼다.

결론

  • 대부분 이런 이슈들의 정답은, 우리(회사)에게 익숙하고 편한걸 하자.. 로 수렴한다.
  • 0.3.X 버전부터 이전과는 차원이 다른 기능들이 추가되었다.. 시간날 때 CHANGE LOG를 읽어보자.
  • Active Record / Data Mapper 두 패턴 모두 경험해 본 결과, 개인적으로 Active Record 패턴이 단순하고 생산성을 올려줘서 더 좋았다.
반응형

댓글