
TypeORM의 Entity를 생성할 때 아래처럼 생성하다가, "내 코드가 그렇게 이상한가요?" 책을 읽고, 악마가 있다는 것을 깨달았습니다. (악마가 뭔지 궁금하다면 책을 읽는 것을 추천해요. 🙂)
클래스를 생성할 때, 항상 건강한 상태의 인스턴스임을 확신할 수 있도록 생성자 내에 검증 코드를 넣고 싶었습니다.
const post = new Post();
post.title = '제목';
post.content = '이건 내용입니다.';
post.subtitle = 'HI';
ORM의 엔티티 생성자는 함부로 건들면 안되는 것이기에 TypeORM 공식 문서를 한 번 확인해봤습니다.
대충 해석해보자면, "ORM은 entity의 인스턴스를 DB에서 로드할 때 생성하므로 우리가 만든 생성자의 인자를 알지 못하기 때문에, Entity 생성자의 인자는 optional이어야 합니다." 라는 내용입니다.
~~사실 잘 이해가 가지 않습니다.
https://typeorm.io/entities#what-is-entity
When using an entity constructor its arguments must be optional. Since ORM creates instances of entity classes when loading from the database, therefore it is not aware of your constructor arguments.
이게 도통 무슨 말인지 TypeORM 구현 코드를 뜯어보기 위해 오류를 일부러 발생시켜 봤습니다. 바로 인자(optional이 아닌)가 있는 생성자를 직접 정의했습니다.
아래처럼 간단한 comment(댓글) entity 클래스 내에 하나의 인자를 받는 생성자를 정의하고, 실행만 시켜도 바로 오류가 발생했습니다.
@Entity()
export class Comment {
@Column()
content: string;
@Column({ default: false })
isHide: boolean;
@PrimaryColumn()
id: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date;
constructor(content: string) {
if (!content) throw new Error('content is required');
this.content = content;
}
}
한 번 오류를 살펴봅시다. 아래는 오류 전문입니다.
Error: content is required
at new Comment (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/domain/comment/comment.entity.ts:50:25)
at EntityMetadata.create (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/metadata/EntityMetadata.ts:563:23)
at EntityMetadataValidator.validate (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/metadata-builder/EntityMetadataValidator.ts:211:47)
at /Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/metadata-builder/EntityMetadataValidator.ts:43:18
at Array.forEach (<anonymous>)
at EntityMetadataValidator.validateMany (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/metadata-builder/EntityMetadataValidator.ts:42:25)
at DataSource.buildMetadatas (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/data-source/DataSource.ts:724:33)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at DataSource.initialize (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/data-source/DataSource.ts:259:13)
at EntityMetadata.create (/Users/kimhalin/project/AccidentlyNest/nest-comment-3/src/metadata/EntityMetadata.ts:563:23)
이 부분이 결정적인 원인일거라 생각해, 구현 부분을 보았더니 아래와 같았습니다.
https://github.com/typeorm/typeorm/blob/master/src/metadata/EntityMetadata.ts#L554
해당 부분을 자세히보면, 무조건 인자가 없는 생성자를 호출하는 것을 알 수 있었습니다.

결론적으로는 TtypeORM이 MetaData를 설정할 때, 항상 인자가 없는 생성자를 호출하니, Entity 생성자의 인자는 무조건 optional이어야 하는 것이었습니다.
사실 여태까지 Entity 생성자와 싸운 이유는 Entity 생성 시, 유효 검증 로직이 들어간 생성자를 직접 정의하고 싶었기 때문입니다. 그래서 아래와 같은 과정을 겪었습니다.
Spring boot를 사용했었기 때문에, 사실 여러 개의 생성자 정의가 가능한 줄 알았지만, 안됩니다.😂

아래처럼 정적 팩토리 메서드를 entity 내에 정의해, 유효성 검증을 넣을 수 있습니다.
static of(content: string): Comment {
if (
content.length < contentLength.MIN_LENGTH ||
content.length > contentLength.MAX_LENGTH
) {
throw new Error(
`댓글은 ${contentLength.MIN_LENGTH}자 이상 ${contentLength.MAX_LENGTH}자 이하로 입력해주세요.`,
);
}
const comment = new Comment();
comment.content = content;
comment.isHide = commentDefault.IS_HIDE;
return comment;
}
사실 java 같은 경우엔 디폴트 생성자의 접근제어를 private로 설정한 후, 정적 팩토리 메서드를 정의하기 때문에 개발자가 초기화되지 않은 빈 인스턴스를 생성할 수 있다는 염려를 하지 않아도 됩니다. 하지만 이 경우엔 생성자를 건들 수 없기 때문에 개발자는 무조건 정적 팩토리 메서드를 사용할 수 있도록 팀 내의 코딩 컨벤션을 정하는 등과 같은 조치가 필요할 것 같습니다.
결론적으로는 "Entity를 생성할 때, 유효성 검증을 하고 싶다면? 정적 팩토리 메서드를 사용하자" 라는 내용의 글이었습니다. 요즘 느끼는 건, 궁금한 것을 해결하기 위해, 빠른 구글링을 통해 필요한 정보만 읽은 후, "그렇구나~" 하고 넘어가게 된다면 개발 속도는 빠르지만 오랫동안 기억에 남지 않고, 후에 "내가 그때 그래서 어떻게 했지?" 하며 또 다시 구글링의 과정을 거치게 되는 것 같습니다.
따라서 제가 어떻게 문제를 해결했는 지, 그 과정에서 어떤 걸 학습했는 지 기록하려고 합니다. 글은 아직 거칠게 작성될 수 있지만, 이것이 습관이 된다면 저와 똑같이 고민해, 이 글을 읽으러 온 개발자 분들에게도 읽기 쉬운 글이 될 거라고 믿어요. 🥹
TypeScript에서도 생성자 오버로딩을 지원합니다!
다만 하나의 생성자에서 매개변수 조건에 따라 로직을 분기시켜 구현해야합니다.
JavaScript에서 함수 오버로딩을 지원하지 않기에
호환성 측면에서 TypeScript에서 이를 직접적으로 지원할 수는 없나보네요.
생성자 별로 다른 동작을 정의하는 것이 불가능하다는 점이 Java와는 다르지만
TypeScript에서도 생성자 오버로딩을 지원한다는 것을 알아주세요 ;)