타입스크립트 꽝꽝

[Typescript] TypeORM 0.3.x 메이저 Changes + 0.3.x에 대한 의견

bimppap 2022. 11. 21. 13:48

 

TypeORM은 2022년 3월 18일을 기준으로 0.3.0 버전을 출시했다. (링크) 0.2.x 와 다르게 달라지거나 deprecated 된 부분이 많기도 하고, 릴리즈 노트 또는 공식 docs에 적혀있지 않고 issue에만 언급된 changes도 있어 마이그레이션할 때 참고했던 것들을 정리하기로 했다. (매번 느끼지만 typeorm 공식 문서는 업데이트도 느리고 설명도 간단하게만 적혀있어 불편할 때가 많다ㅠㅠ)

 

차례는 다음과 같다.

Config 만들기
Repository 만들기
Repository 사용하기
0.3.x으로 바꾸는 게 좋을까?

Config 만들기

0.2.x에서 config를 주입할 때 사용하던 ConnectionOptions은 0.3.x에서 deprecated되었다. 대신 DataSoucreOptions라는, ConnectionOptions와 거의 유사한 타입을 만들었다. 다른 점은 ConnectionOptions에 있던 cli 옵션이 사라졌다는 것이다. 같은 말인 즉슨 엔티티 생성 또는 migration을 cli를 통해 시도할 경우 path를 터미널에서 직접 입력해줘야 한다는 거다. 
개인적으로 왜 cli를 없앴는지 지금도 이해가 가지 않는다...관련 이슈를 봐도 cli가 사라져 불만인 사람이 많은 듯 하다. 얼른 부활했으면ㅠㅠ

 

CLI 명령어에서 ormconfig 관련된 옵션도 depreated되었기에, package.json에서 typoemr script를 쓰는 방법 또한 달라졌다. --config 옵션 대신 -d 옵션으로 바꾸면 된다.

// 0.2.x 사용할 때
{
  ...(생략)...
  "scripts": {
    "typeorm": "ts-node -r tsconfig-paths/register -r ts-node/register ./node_modules/typeorm/cli.js --config src/config/database/ormconfig.ts",
  }
}

// 0.3.x 사용할 때
{
  ...(생략)...
  "scripts": {
    "typeorm": "ts-node -r tsconfig-paths/register -r ts-node/register ./node_modules/typeorm/cli.js -d src/config/database/ormconfig.ts",
  }
}

 

 

 

Repository 만들기

Repository를 선언하는 방식이 달라졌다. 0.2.x에선 @EntityRepository라는 데코레이터를 통해 해당 클래스가 레포지토리임을 알 수 있었지만 0.3.x는 @EntityRepository가 deprecated 됨에 따라 아래와 같이 DataSource.getRepository(Entity).exted({...}) 코드로 커스텀 레포지토리를 선언하라고 했다.

// (1)
export const UserRepository = myDataSource.getRepository(UserEntity).extend({
    findUsersWithPhotos() {
        return this.find({
            relations: {
                photos: true
            }
        })
    }
})

 

아래처럼 @Injectable 데코레이터와 생성자를 이용해 커스텀 레포지토리를 선언하는 방법도 있다. 개인적으론 기존에 쓰던 것과 비슷해 보이는 이 방식을 선호한다.

// (2)
@Injectable()
export class UserRepository extends Repository<User> {
  constructor(dataSource: DataSource) {
    super(User, dataSource.createEntityManager());
  }
  // add custom methods here
}

 

선언하는 방식이 달라짐에 따라 모듈에 주입하는 방식도 달라졌다. 0.2.x 는 imports에서 TypeOrmModule을 통해 레포지토리를 주입했으나 0.3.x부터 TypeOrmModule에 엔티티를 주입한다. (2)번 방식으로 커스텀 레포지토리를 사용하려면 providers에 레포지토리를 주입해야한다.

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [
    UserController,
  ],
  providers: [
    UserService,
    UserRepository,
  ],
})
export class UserModule {}

 

Repository 사용하기

아래 예제를 보자.

User는 자식 엔티티 Child와 Friend를 OneToMany 관계로 가지고 있다. 이 때 User의 성별(sex)가 여성, Child의 나이(age)가 10살 이상, Friend의 이름(name)이 'rinsabbit'인 User를 Child와 함께 1명만 가져오라.

0.2.x에서는 이렇게 쓴다.

this.createQueryBuilder('User')
  .leftJoinAndSelect(‘User.childs’, ‘Child’)
  .leftJoin(‘User.friends’, ‘Friend’)
  .where(‘Child.age’ > 10)
  .andWhere(‘Friend.name’ = ‘rinsabbit’)
  .getOne();

 

0.3.x에서는 이렇게 쓴다.

this.findOne(
{
  where: { 
    child: { age: MoreThan(10) },
    friend: { name: ‘rinsabbit’ }
   },
  relations: { child: true, }
});

 

보다시피 쿼리를 쓰는 방식이 굉장히 달라졌다. 0.3.x에서는 where, relation 조건절이 생겨 string이 아닌 변수 형식으로 조건을 넣을 수 있다. 이로 인해 sql문을 어느 정도 쓸 줄 알아야 복잡한 조건문을 쓸 수 있던 과거와 다르게 sql문을 쓸 줄 모르더라도 typeorm 사용방법만 알면 간단하게 조건문을 쓸 수 있다. 게다가 직관적이고 짧아 코드를 해석하는 데도 시간이 적게 걸린다. 0.3.x가 되면서 가장 눈부시게 발전한 기능이라고 본다.

 

물론 이러한 사용방식이 완벽하다는 건 아니다. 일단 0.3.x에서는 조건절을 통한 innerJoin을 지원하지 않는다. 0.2.x에서처럼 쿼리빌더를 사용해 innerJoin을 쓰는 방식은 여전히 유효하지만(join옵션을 쓰는 방법도 있긴 하다.) relation 조건절에서 join 방식을 고를 수 없는 건 조금 아쉽다.

 

 

0.3으로 바꾸는 게 조을까?

확실히 0.3.x는 0.2.x에 비해 더 직관적이고 간단하게 쿼리를 짤 수 있게 도와준다. 0.2.x에선 쿼리가 복잡해질 경우 쿼리빌더를 이용해 sql문을 직접 써야할 때가 많았으나 0.3.x는 제공하는 함수가 많아져 조건문을 만들기가 굉장히 쉬워졌다. 이런 점에서 0.3.x는 확실히 매력적인 업데이트다. 하지만 무작정 0.3.x로 버전업하기엔 아래와 같은 문제가 있다.

 

해결되지 않은 메모리 누수

typeorm은 0.2.35 버전부터 메무리 누수가 일어나고 있다는 이슈가 있었으며 현재진행형이다. 해당 이슈에서는 조인이 여러 개가 되면 메모리를 많이 잡아먹게 된다면서 nested 엔티티는 가져오지 않는 옵션을 추가하면 해결된다고 한다.

 이슈 타래에 의하면 0.3.x에서는 조건문 옵션 중 하나인 relationLoadStrategy를 "query"로 정해도 nestedObject가 따라오지 않는다 한다. (아래 코드 참고)

async getUser(userId: number) {
    return this.repository.findOne({
      where: {
        number: userId,
      },
      relationLoadStrategy: 'query',
    });
  }

그러나 공식 문서의 change log를 읽어보면 query를 사용하면 join을 사용하지 않고 relation마다 쿼리를 따로 보낸다고 되어있다. 따라서 위 해결법은 진짜 해결법이 아닌 버그를 사용한 우회법 같다... 🙃 이 버그에 대한 이슈도 등록되어있으며 아직 해결은 안 된 상태다.

핵심은 join을 많이 할수록 메모리 누수가 일어날 확률이 크다는 것이므로 엔티티에 nested 엔티티가 있는 경우 eager보다는 lazy로딩을 추천한다는 것이다. 다행히(?) 공식 문서에서 eager 또는 lazy를 고를 수 있는 방법을 제공해준다.

 

(+) 메모리 누수와 관련이 있는지는 모르겠지만 distinct 조건을 걸어줄 때도 성능이 눈에 띄게 느려진다. 찾아보니 필터 및 정렬이 추가되면 성능 저하 이슈가 있는 듯 하다. 또한 디비에 쌓인 데이터 양이 많아질 수록 성능이 기하급수적으로 느려지는 게 느껴진다ㅠㅠ

 

 

트랜잭션 관련 데코레이터 depreacted 및 미지원

0.3.x가 되면서 트랜잭션 데코레이터인 @TransactionRepository, @TransactionManager, @Transaction 가 제거되었다. Typeorm에서 지원하는 범주 밖의 기능이라는게 이유였다. (Spring JPA에 있던 @Transactional 의문의 1승🙃) 이미 NestJS에서 @Transaction은 사용하지 않는 걸 추천했기에(테스트할 때 모킹이 되지 않는 게 이유였다ㅎ...) 원래도 사용하지 않았지만 Connection을 통해 직접 트랜잭션을 컨트롤하기 귀찮았기에... 이부분은 아예 손을 놓고 있었다.

 

최근 찾아보니 typeorm-transactional-cls-hooked라는 라이브러리가 트랜잭션 처리에 용이하다며 많이 사용하고 있는 듯하다. issue와 PR은 적지만 꾸준한 주간 다운로드 수가 신뢰감을 주었다.. typeorm에서 제공하던 데코레이터와 다르게 단위 테스트용 모킹도 지원한다고 해서 조만간 프로젝트에 적용해볼까 싶다.

 

결론

개인적으로 큰 프로젝트가 아니라면 typeorm 0.3 버전을 사용해도 무방할 것 같다. 하지만 많은 양의 데이터와 복잡한 쿼리를 필요로 하는 경우라면 메모리 누수 때문에 한 번즘은 고민해봐야 할 듯 하다. 물론 앞서 말한 0.3에서 볼 수 있는 문제들은 서드파티 라이브러리나 리팩토링을 통해 어느 정도 개선할 수 있으니, 인지 및 개선의 여지가 있다면 0.3으로 충분히 마이그레이션 해도 될 것이다. 0.3의 조건절을 쓰는 방법이 충분히 매력적이기 때문이다.

 

(+)

개인적으로 한국어로 된 글이 적어 힘들었기 때문에(ㅠㅠ) 한 명에게라도 도움이 됐으면 싶어 글을 적었다. typeorm이 빠른 시일 내에 안정적이고 성능 좋은 1.0.0을 출시하길 응원하고 있다. 오픈소스이니만큼 돈을 받고 만드는 게 아니기 때문에 후원을 해주면 개발에 도움이 될 것이다. 아래 링크에서 후원을 할 수 있다.

 

TypeORM - Open Collective

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

opencollective.com

 

 

참고자료

https://github.com/typeorm/typeorm/releases/tag/0.3.0

https://www.npmjs.com/package/typeorm-transactional-cls-hooked

https://hou27.tistory.com/entry/TypeORM-%EB%B2%84%EC%A0%84-03-ORM%EC%9D%B4%EB%9E%80

https://orkhan.gitbook.io/typeorm/

https://kscodebase.tistory.com/524

 

 

잘못된 정보가 있다면 댓글 달아주세요. 🙂