NodeJS에서의 ORM 선택 (1) : TypeORM
2021년 03월 14일

배경

신규 프로젝트를 구축할 일이 있었습니다. 기존에 사내에서 주력으로 사용하던 언어 및 프레임워크는 PHP, Laravel이었지만, 유지보수, 확장성, 구성원들의 경험 등의 다양한 이유로 NodeJS를 활용하여 새로운 코드 베이스를 구축하기로 결정되었습니다.

소위 맨땅에서부터 시작했고, NodeJS 생태계 특성상 많은 스택들을 직접 결정해야 했습니다. 이를테면 그나마 NestJS를 사용하고, NestJS 문서에서 권정하는 써드파티 라이브러리를 우선적으로 사용한다는 점이 합의가 되어 초기 결정에 대한 부담을 줄일 수 있었습니다.

그러나 데이터베이스 - 소위 MVC에서의 모델 같은 경우 선택지가 많아 고민이이었고, 이에 대한 고민 과정을 간략히 정리한 글 입니다.

언제나 고민하게 되는 DB

데이터베이스를 다루는 방법은 여러가지가 있습니다.

  • ORM - Object Relation Mapping - Database Layer를 wrapping 해줌으로써 query를 매번 직접 입력하지도 않아도 application 단에서 편하게 개발할 수 있는 방법입니다.
  • Query Builder - 이전까지 사용해오던 방식으로, 빌더 패턴을 통해 쿼리를 간접적으로 다룸으로써 유연성과 구조성 중간에 있는 전략 입니다.
  • Raw Query - 말 그대로 쿼리를 직접 사용하며, 자유도가 가장 높으나 규모가 커질 시 유지보수가 어렵습니다.
  • Data Mapper - 자바 진영에서 사용한다는 방법인데, xml 베이스의 mapper에 사용할 쿼리를 정의해놓고 호출시켜 사용한다고는 하는데 저와는 관련 없으므로 생략했습니다.

Sequelize

기존 nodejs 진영 및 npm에서 가장 점유율이 높습니다.

그러나 migration, typescript 지원이 부족하다 느껴 제외되었습니다.

또한 migration 기능을 제공하기는 하지만 TypeORM이나 django처럼 up, down function generate를 제공해야 생산성에서 의미가 있다 생각했습니다.

class Project extends Model {} Project.init({ title: Sequelize.STRING, description: Sequelize.TEXT }, { sequelize, modelName: 'project' }); class Task extends Model {} Task.init({ title: Sequelize.STRING, description: Sequelize.TEXT, deadline: Sequelize.DATE }, { sequelize, modelName: 'task' })
// search for known ids Project.findByPk(123).then(project => { // project will be an instance of Project and stores the content of the table entry // with id 123. if such an entry is not defined you will get null }) // search for attributes Project.findOne({ where: {title: 'aProject'} }).then(project => { // project will be the first entry of the Projects table with the title 'aProject' || null }) Project.findOne({ where: {title: 'aProject'}, attributes: ['id', ['name', 'title']] }).then(project => { // project will be the first entry of the Projects table with the title 'aProject' || null // project.get('title') will contain the name of the project })

Knex.js ( query builder )

조율 과정에서 탈락되었지만, 개인적으로는 제가 개발하던 패턴과 가장 비슷하여 눈여겨 보고 있습니다.

knex.select('title', 'author', 'year') .from('books') knex.select() .table('books')

MikroORM

document만 봤을 떄는 단점이 크게 없었지만, 생태계가 아직 크지 않아 프로덕션에 올리기에는 리스크가 있다 판단하여 제외되었습니다.

@Entity() export class Book extends CustomBaseEntity { @Property() title!: string; @ManyToOne(() => Author) author!: Author; @ManyToOne(() => Publisher, { ref: true, nullable: true }) publisher?: Ref<Publisher>; @ManyToMany({ entity: 'BookTag', fixedOrder: true }) tags = new Collection<BookTag>(this); }
const author = await em.findOne(Author, 123); const books = await em.find(Book, {}); for (const author of authors) { console.log(author.name); // Jon Snow for (const book of author.books) { console.log(book.title); // initialized console.log(book.author.isInitialized()); // true console.log(book.author.id); console.log(book.author.name); // Jon Snow console.log(book.publisher); // just reference console.log(book.publisher.isInitialized()); // false console.log(book.publisher.id); console.log(book.publisher.name); // undefined } } const books = await em.find(Book, { foo: 1 }, { populate: ['author.friends'] });

TypeORM을 채택하다.

  • NestJS에서도 가이드에 가장 먼저 등장하고, 개인적으로 사이드 프로젝트에 적용시켜본 경험이 있었기 때문에 가장 유력한 후보였습니다. 데코레이터 패턴을 사용하여 Typescript, NestJS 및 관련 라이브러리들과 궁합이 잘맞고, 추후 실험적으로 GraphQL을 도입하기도 용이합니다. 또한 당시 구성원들이 모두 어느 정도 TypeORM에 대한 기본 지식이 있기 때문에 이 라이브러리를 사용하기로 결정했습니다.
// class와 decoratro를 통해 entity를 정의 @Entity() class Contact { @PrimaryGeneratedColumn() id: number @Column() name: string @ManyToOne((type) => Address) address: Address } @Entity() class Address { @PrimaryGeneratedColumn() id: number @Column() street: string @Column() city: string }
// entity manager 혹은 repository를 통해 기본적인 데이터 CRUD를 수행 userRepository.find({ where: { firstName: "Timber", lastName: "Saw" } })
  • 만약 제공되지 않는 복잡한 쿼리 액션이 필요 시 직접 queryBuilder를 사용한다.

짧은 TypeORM 사용 후기 및 한계점 발견

그리하여 TypeORM을 기반으로 기초 코드를 다졌는데, 생각보다 여러 문제가 생겼습니다.

  1. 위에 특징에서 언급한 TypeQueryBuilder의 남용
    • 복잡도가 증가함에 따라 제공안되는 기능이 많아 queryBuilder를 울며 겨자먹기로 남용한 결과, 순수한 ORM 코드와 quileryBilder 코드가 혼용되어 가독성 하락 및 타입 보장이 되지 않습니다.
    • [이미지1]
    • https://github.com/typeorm/typeorm/issues/3890#issuecomment-524009201
    • 예) Author.name이 Brown인 Book의 목록을 가져오기 → 이 정도는 기본 기능을 활용할 수 있을 것으로 생각했으나 queryBuilder를 사용해야 했다.
  2. TypeORM이지만 생각보다 Type이 strict하지 않다.
  3. 전반적 개발 인력 부족
    • 빈약한 문서화
  4. 관련 이슈 링크 모음 이러한 염증을 저만 느끼는 것이 아니었는지, 커뮤니티 등에 잊을만 하면 TypeORM 관련하여 토론이 열립니다.