-
[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 키워드로 생성하는데, 이 때 공식문서의 예제처럼 아무 매개변수없이 호출을 하기 때문에 오류가 난다.(실제 구현부)
실제로 생성자 안에서 매개변수들을
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
반응형'Javascript, Typescript' 카테고리의 다른 글
[TypeORM] QueryBuilder 사용시 return 타입 구체화하기 (0) 2022.02.10 [TS] 모듈 import 경로에 별칭 사용하기(VS Code) (0) 2022.01.26 [TS] class-validator의 활용과 검증 옵션 (0) 2022.01.03 [Jest] object에 대한 다양한 matcher 함수들 (0) 2021.12.31 [TypeORM] repository.save()의 동작과 upsert() (5) 2021.12.19