Chat 도메인에서 TypeORM을 제거한 이유: 도메인 객체와 Persistence의 분리
2025. 6. 9. 12:40
728x90

왜 처음엔 TypeORM을 그대로 사용했을까?

처음 chat-service를 설계할 때는 익숙한 방식대로 TypeORM의 @Entity를 활용해 곧바로 테이블 구조를 정의하고, 그 위에서 도메인 로직을 작성했습니다. 아래는 그 당시 사용하던 ChatRoom 엔티티의 일부입니다.

@Entity()
export class ChatRoom {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  type: 'PUBLIC' | 'PRIVATE';

  @Column({ nullable: true })
  itemId: number;

  @Column()
  createdBy: number;

  @Column({ default: true })
  isActive: boolean;

  @CreateDateColumn()
  createdAt: Date;
}

이 구조는 단순한 기능 구현이나 빠른 MVP 단계에서는 효율적입니다. 그러나 이후 아래와 같은 문제들이 발생했습니다.


문제 1: 도메인 객체가 DB에 종속되어 버린다

TypeORM과 강하게 결합된 Entity는 도메인 로직과 DB 스키마가 한 곳에 섞이게 됩니다. 이로 인해 아래와 같은 문제들이 생겼습니다.

  • 테스트 시 DB가 꼭 필요해져서 단위 테스트가 아닌 통합 테스트로 흐름이 변질됨
  • 설계 변경(예: 특정 필드의 타입 수정, 새로운 비즈니스 속성 추가)이 ORM 스키마에 직접 영향을 미침
  • 단순한 도메인 객체 생성도 ORM의 생성자나 DI 컨테이너를 따라야 함

문제 2: 객체 생성의 책임이 흐려짐

도메인 객체가 ORM에 종속되면 생성 시점에서의 제약 조건(예: PRIVATE 채팅방은 반드시 itemId 필요)을 강제하기 어렵습니다. 예외 없이 그냥 new ChatRoom() 하면 다 만들어지는 구조가 되기 때문입니다.


구조를 분리하기로 결정하다

이후 서비스 구조를 전면적으로 리팩토링하면서 Entity는 순수한 도메인 객체로 분리하고, TypeORM과 DB 연동 로직은 Repository 계층에서만 담당하도록 구조를 재편했습니다.

리팩토링 후 구조

 
// 도메인 계층의 ChatRoom 객체
export class ChatRoom {
  constructor(
    public id: number,
    public type: 'PUBLIC' | 'PRIVATE',
    public itemId: number | null,
    public createdBy: number,
    public isActive: boolean,
    public createdAt: Date,
  ) {}
}
// ChatRoomFactory.ts
export class ChatRoomFactory {
  static createPrivateRoom(itemId: number, createdBy: number): ChatRoom {
    return new ChatRoom(0, 'PRIVATE', itemId, createdBy, true, new Date());
  }

  static createPublicRoom(createdBy: number): ChatRoom {
    return new ChatRoom(0, 'PUBLIC', null, createdBy, true, new Date());
  }
}
// ChatRoomRepositoryImpl.ts (Persistence Layer)
@Injectable()
export class ChatRoomRepositoryImpl implements IChatRoomRepository {
  async save(chatRoom: ChatRoom): Promise<ChatRoom> {
    const ormEntity = ChatRoomMapper.toOrmEntity(chatRoom);
    const saved = await this.ormRepo.save(ormEntity);
    return ChatRoomMapper.toDomain(saved);
  }
}

 


구조를 분리한 후 얻은 것들

1. 테스트가 쉬워졌다

  • ChatRoom 도메인 객체는 DB 없이도 테스트 가능
  • 실제 저장/조회는 Mock Repository로 대체 가능
  • 테스트 속도가 빨라졌고, 의존성도 줄어들었음

2. 비즈니스 규칙이 명확해졌다

  • PRIVATE/PUBLIC 방 생성 시 제약조건을 Factory에 명시적으로 정의
  • 객체 생성 시 어떤 상태에서 유효한지 눈에 보임

3. Persistence에 대한 관심사를 분리할 수 있었다

  • DB와 무관한 비즈니스 로직은 도메인 객체에서 해결
  • ORM 변경, DB 마이그레이션 시 영향이 최소화됨

마치며

처음에는 ORM이 제공하는 편의성에 의존해 설계를 시작했지만, 도메인이 복잡해지고 테스트 및 유지보수 필요성이 높아지면서 구조의 분리가 필수적이라는 것을 느꼈습니다. 지금은 “Entity는 DB 모델이 아니라 도메인 개념이다”라는 관점을 가지게 되었고, 모든 도메인 객체는 ORM과 분리된 상태로 유지하고 있습니다.

도메인과 Persistence의 분리는 단순한 설계의 차이가 아니라, 시스템이 성장하는 과정에서 겪는 문제를 해결하는 강력한 방법이라는 것을 이번 경험을 통해 깊이 느꼈습니다.

반응형