ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TypeORM] Entity의 constructor를 사용할 수 있을까?
    Javascript, Typescript 2022. 1. 13. 23:56
    반응형

    TypeORM의 Entity에 대한 공식문서를 찬찬히 읽어보면 다음 문구를 찾을 수 있다.

    엔티티 생성자의 인자들은 반드시 옵셔널이어야 한다라니... 굉장히 의미심장한 문구가 아닐 수 없다.

    다른 말로는 생성자의 인자들은 반드시 nullable 이어야한다 라고 생각할 수 있다. 그렇다면 인스턴스 생성시에 유효성 검증은 어떻게 하지?

    이 경고가 의미하는 것이 무엇인지, 그리고 문제를 해결할 수 있는 방법은 무엇인지 알아보자.

    무슨 소리일까?

    다음과 같은 간단한 엔티티가 있다.

    @Entity()
    export class User {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      name: string;
    
      @Column()
      age: number;
    }

    TypeORM의 Getting Started를 잘 읽고 따라해봤다면 엔티티의 인스턴스를 생성할 때 다음 두 가지 방법 중 하나로 생성할 것이다.

    // 1
    const user = new User();
    user.name = 'Kim';
    user.age = 28;
    console.log(user); // User { name: 'Kim', age: 28 }
    
    // 2
    const user = getRepository(User).create({
      name: 'Kim',
      age: 28,
    });
    console.log(user); // User { name: 'Kim', age: 28 }

    실제로 저 두가지 예제밖에 없다. 하지만 왜 인스턴스를 이렇게 생성해야 하지? 라는 의문이 든다. 우리에게는 인자를 받아서 인스턴스를 생성하는 생성자라는, 우아한 방법이 있다.

    엔티티의 필드들을 모두 private으로 숨기고, 생성자를 정의해서 직접 호출해보자.

    @Entity()
    export class User {
      @PrimaryGeneratedColumn()
      private id: number;
    
      @Column()
      private name: string;
    
      @Column()
      private age: number;
    
      constructor(
        name: string,
        age: number,
      ) {
        this.name = name;
        this.age = age;
      }
    }
    const user = new User('Kim', 28);
    user // User { name: 'Kim', age: 28 }

    코드는 잘 돌아간다.

    문제는 생성자 내부에서 입력값에 대한 검증을 추가했을 때 나타난다.

    @Entity()
    export class User {
      // ...
    
      constructor(
        name: string,
        age: number,
      ) {
        if (!name) {
          throw new Error('name must be provided');
        }
        if (age < 1) {
          throw new Error('age must be greater than or equal to 1');
        }
        this.name = name;
        this.age = age;
      }
    }
    // Error: name must be provided
    // 우리의 코드가 실행되기 전, 라이브러리가 초기화되는 과정에서 오류가 난다.
    const user = new User('Kim', 28); // <- 우리가 정의한 코드는 실행조차 안된다.

    TypeORM 라이브러리가 초기화되는 과정에 저 깊은 어딘가에서 우리가 데코레이터로 정의한 엔티티 클래스들을 new 키워드로 생성하는데, 이 때 공식문서의 예제처럼 아무 매개변수없이 호출을 하기 때문에 오류가 난다.(실제 구현부)

    typeorm/src/metadata/EntityMetadata.ts:534:23

    실제로 생성자 안에서 매개변수들을 console.log()로 찍어보면, 우리는 호출한 적이 없지만 undefined들이 찍히는 것을 확인할 수 있다.

    그럼 어떻게 인스턴스를 만들지?

    static 메서드

    인스턴스를 생성할 때 private 프로퍼티들의 입력값을 검증하고 싶은데 생성자는 안되고, repository의 create()메소드에 의존하고 싶지는 않다.

    가장 간단하게 구현할 수 있는 방법은 엔티티 클래스에 해당 인스턴스를 생성하는 static 메소드를 구현하는 것이다.

    @Entity()
    export class User {
      // ...
    
      static create(
        name: string,
        age: number,
      ) {
        if (!name) {
          throw new Error('name cannot be empty');
        }
        if (age < 1) {
          throw new Error('age must be greater than or equal to 1');
        }
        const user = new User();
        user.name = name;
        user.age = age;
        return user;
      }
    }
    const user = User.create('Kim', 28);
    console.log(user); // User { name: 'Kim', age: 28 }

    인스턴스를 생성하는 static 메소드의 이름은 create나 매개변수 데이터의 출처에 따라 from~ 등으로 많이 사용하는 것 같다.

    도메인 객체 분리

    @Entity 어노테이션이 붙는 클래스는 TypeORM이 영속성 모델을 나타내기 위한 클래스로만 사용하고, 실제 유스케이스에서 사용할 도메인 모델을 따로 정의하는 방법이다.

    // user.entity.ts
    @Entity()
    export class UserEntity {
      @PrimaryGeneratedColumn()
      private id: number;
    
      @Column()
      private name: string;
    
      @Column()
      private age: number;
    }
    
    // user.ts
    export class User {
      private id: number;
      private name: string;
      private age: number;
      
      constructor(
        name: string,
        age: number,
      ) {
        if (!name) {
          throw new Error('name cannot be empty');
        }
        if (age < 1) {
          throw new Error('age must be greater than or equal to 1');
        }
        this.name = name;
        this.age = age;
      }
      
      // 도메인 객체의 메소드들 구현
      // ...
    }

    이 방식을 이용하면, 도메인 객체에 영속성 모델과 관련된 데코레이터들을 제거할 수 있고, 생성자도 사용할 수 있다는 장점이 있다.

    하지만 영속성-도메인 계층 간에 필요한 모델이 다르기 때문에 계층 간의 모델 매핑이 필요하게 된다. 그리고 이것을 어떻게 구현할지는 개발자의 몫이다.

    References

    반응형

    댓글

Designed by Tistory.