Entity를 정의하는데 create_date와 update_date 컬럼이 여러 Entity에 중복하여 선언됨을 깨달았다. 이건 보통 많은 테이블들의 컬럼으로 사용되는데 그때마다 선언해준다면 코드의 중복이 높아질 것 같아 따로 빼서 상속받아야겠다고 생각했다.
그래서 BaseEntity를 만들고 정의하여 상속하려고 했는데 검색을 해보니 TypeORM이 제공하는 내장 BaseEntity가 존재했다.
갑자기 헷갈려서 찾아보고 정리한다!
BaseEntity (Active Record)이건 TypeORM 라이브러리('typeorm')에서 직접 import 해서 사용하는 클래스이다.
import { BaseEntity, Entity } from 'typeorm';
@Entity()
export class User extends BaseEntity { // typeorm에서 가져온 BaseEntity
...
}
이걸 상속받으면 엔티티 클래스 자체가 데이터베이스와 통신하는 함수(메서드)를 갖게 된다.
-> "Active Record(액티브 레코드)" 패턴
예시 (Active Record)
// User 클래스 자체에 save, findOne 같은 함수가 생김
const user = new User();
user.name = 'seha';
await user.save(); // <- !!! 엔티티 인스턴스가 스스로 DB에 저장
const foundUser = await User.findOne({ where: { id: 1 } }); // <- !!! 클래스가 스스로 DB 조회
BaseEntity (공통 컬럼 상속)이건 내가 공통되는 컬럼 속성을 따로 빼서 상속받아 쓰려고 하는 커스텀 BaseEntity.ts이다. 당연히 내가 선언했으니까 TypeORM 라이브러리와 아무 상관없는 직접 만든 abstract 클래스이다.
// src/entity/BaseEntity.ts (우리가 만든 파일)
export abstract class BaseEntity {
@CreateDateColumn(...)
create_date: Date;
@UpdateDateColumn(...)
update_date: Date | null;
}
이건 당연히 Active Record 기능을 전혀 제공하지 않는다. 내가 만들었으니까... 이 클래스의 유일한 목적은 @CreateDateColumn과 @UpdateDateColumn이라는 중복되는 코드를 한곳에 모아두고 상속(extends)을 통해 재사용하는 것뿐이다.
어려운 내용은 아니지만 처음 쓰려고 서치해봤을때는 조금 헷갈렸었다... BaseEntity를 쓰면 안되는건가? 싶었다.
TypeORM은 두 가지 방식을 모두 지원한다.
Active Record(AR)와 Data Mapper는 데이터 접근 계층(Persistence Layer) 의 패턴으로써, 객체(Entity)가 데이터베이스와 어떻게 통신하는가?에 대한 패턴을 정의한다.
user.save(), user.remove(), User.find()모든 Active Record entity 들은 위에서 말한 BaseEntity를 상속해야하며, 여러가지 메서드들을 제공한다.(save, remove, find,...)
BaseEntity 는 기본적인 Repository 가 가진 메서드를 수행가능하므로, Active Record 를 사용한다면 Repository나 Entity Manager를 사용할 필요가 없다.
Repository(저장소)라는 별도의 객체를 통해 처리하는 방식이다.Repository)가 객체와 데이터베이스 간의 변환 및 통신을 전담한다.userRepository.save(user), userRepository.remove(user), userRepository.find()// Entity (데이터 정의만)
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
// Service (로직)
// Nest.js 같은 프레임워크에서 DI로 주입받음
constructor(
@InjectRepository(User)
private userRepository: Repository<User>
) {}
async createUser(name: string) {
const user = new User();
user.name = name;
// DB 작업은 Repository가 대신 함
await this.userRepository.save(user);
// DB 조회도 Repository가 대신 함
const foundUser = await this.userRepository.findOne({ where: { id: 1 } });
}
Data Mapper 방식은 관심사 분리(SoC)가 명확해서, 애플리케이션이 커지고 테스트 코드를 작성할 때 훨씬 유리하다.
NestJS에서 가장 흔하게 사용하고 권장되는 조합은 Data Mapper + Transaction Script 패턴의 조합이다.
물론, 복잡한 비즈니스 로직을 다룰 때는 Data Mapper(Repository)를 사용하면서 (Rich) Domain Model 패턴을 적용하여 Entity 안에 비즈니스 로직을 넣고 Service는 조율만 하도록 구현할 수도 있다. (DDD 접근 방식)
본인의 프로젝트 규모, 상황에 따라서 적절하게 잘 사용하면 될 것 같다!