TypeORM의 내장 BaseEntity VS 커스텀 BaseEntity

세하·2025년 10월 23일

NestJS

목록 보기
4/8
post-thumbnail

Entity를 정의하는데 create_date와 update_date 컬럼이 여러 Entity에 중복하여 선언됨을 깨달았다. 이건 보통 많은 테이블들의 컬럼으로 사용되는데 그때마다 선언해준다면 코드의 중복이 높아질 것 같아 따로 빼서 상속받아야겠다고 생각했다.
그래서 BaseEntity를 만들고 정의하여 상속하려고 했는데 검색을 해보니 TypeORM이 제공하는 내장 BaseEntity가 존재했다.

갑자기 헷갈려서 찾아보고 정리한다!

TypeORM의 내장 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를 쓰면 안되는건가? 싶었다.

Active Record vs Data Mapper (권장)

TypeORM은 두 가지 방식을 모두 지원한다.
Active Record(AR)와 Data Mapper는 데이터 접근 계층(Persistence Layer) 의 패턴으로써, 객체(Entity)가 데이터베이스와 어떻게 통신하는가?에 대한 패턴을 정의한다.

Active Record

  • 모델(객체) 자체가 데이터베이스 접근 로직(CRUD)을 가진다.
  • 객체가 스스로를 저장하고, 찾고, 삭제할 수 있다.
    • 예: user.save(), user.remove(), User.find()
  • 장점: 사용하기 매우 간편하고 코드가 단순해짐
  • 단점: 객체가 데이터베이스 구조에 강하게 의존하게 되어, 비즈니스 로직과 데이터 접근 로직이 섞이기 쉽다. (SOLID 원칙 중 단일 책임 원칙(SRP) 위반 가능성)

모든 Active Record entity 들은 위에서 말한 BaseEntity를 상속해야하며, 여러가지 메서드들을 제공한다.(save, remove, find,...)
BaseEntity 는 기본적인 Repository 가 가진 메서드를 수행가능하므로, Active Record 를 사용한다면 Repository나 Entity Manager를 사용할 필요가 없다.

Data Mapper

  • 엔티티는 오직 데이터 정의만 하고(POJO), DB 작업은 Repository(저장소)라는 별도의 객체를 통해 처리하는 방식이다.
  • 별도의 매퍼(TypeORM에서는 Repository)가 객체와 데이터베이스 간의 변환 및 통신을 전담한다.
    • 예: userRepository.save(user), userRepository.remove(user), userRepository.find()
  • 장점: 비즈니스 로직(도메인)과 데이터 접근 로직(영속성)이 명확하게 분리됨 (SRP 준수)
  • 단점: 코드가 조금 더 장황해지고(Repository 계층 필요), 구조가 복잡해짐

예시 - Data Mapper / Repository

// 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 (TypeORM): Repository를 사용. @InjectRepository()를 통해 Service에 Repository를 주입받고, Entity는 데이터 필드 정의에 집중
  • Transaction Script (NestJS): Service의 메서드 안에 비즈니스 로직의 대부분을 구현함. 이 서비스 메서드가 주입받은 Repository(Data Mapper)를 호출하여 데이터를 처리.

물론, 복잡한 비즈니스 로직을 다룰 때는 Data Mapper(Repository)를 사용하면서 (Rich) Domain Model 패턴을 적용하여 Entity 안에 비즈니스 로직을 넣고 Service는 조율만 하도록 구현할 수도 있다. (DDD 접근 방식)

본인의 프로젝트 규모, 상황에 따라서 적절하게 잘 사용하면 될 것 같다!

0개의 댓글