오늘은 Nest에서 ORM 라이브러리로 많이 거론되는 둘을 비교해보고자 한다.
Nest를 공부하면서 공식문서에 많이 소개 되어있던 TypeORM(이하 타입오름)을 자연스럽게 사용하게 되었었지만,
이름이 무색하게도 TypeORM은 Typescript와 함께 사용할 때 종종 불편한 상황들을 많이 만들어냈던 것 같다.
TypeORM이 무엇인지, 어떤 불편한 점이 있는지 간단히 살펴보고 그 대항마로 점점 유명해지고 있는 Prisma는 어떻게 이런 불편한 부분을 해결해 인기를 끌고 있는 것인지 간단하게 알아보도록 하자.
ORM(Object Relational Mapping)
먼저 ORM이란 객체와 데이터베이스의 관계를 매핑해주는 도구이다.
ORM을 사용하면 직접 쿼리를 작성하지 않기에 데이터베이스와의 연결이 느슨해지고 재사용성을 높일 수 있다는 장점이 있다.
물론 프로젝트가 복잡해질수록 ORM의 사용 난이도 또한 올라가고 N+1문제와 같은 이슈들로 인해 성능 저하가 생길 수 있지만,
적절한 ORM의 사용은 개발자가 비즈니스 로직에 더 집중할 수 있도록 해주는 좋은 도구이다.
TypeORM
id, email, name, posts 필드를 가지는 user 엔티티가 있다고 가정하자.
이를 TypeORM으로 나타내려면 아래와 같이 Class와 Decorator 기반으로 나타내게 된다.
이제 TypeORM을 사용해 id가 1인 유저의 `id`와 `email`을 가져와보자.
위와 같은 코드를 실행하면, 아래와 같이 나온다.
매우 직관적이고 편해보이며 아무런 문제가 없어보인다.
그럼 유저의 이름 길이를 가져오는 비즈니스 로직이 있다고 가정을 해보자.
`userRepository`에 `getUser`라는 메소드가 이미 구현되어 있다면 아마 이를 그대로 사용하게 될 것이다.
그리고 `return user.name.length;`라는 코드를 작성하는 중간에 자동완성을 켜보면 아래와 같이 user가 가진 필드들도 볼 수 있을 것이다. 우리는 이름의 길이를 가져오는 로직을 작성중이니 name을 골라 length를 리턴해주면 될 것 같다.
정말 이러면 된걸까?
우리는 `getUser`메소드에서 select에 name필드를 넘겨주지 않았다.
그럼에도 불구하고 user의 type은 name 필드를 가지고 있다고 나오며 TypeORM을 사용할 때는 런타임 전에 아무런 에러도 나오지 않으며 컴파일도 문제없이 된다.
Typescript와 TypeORM을 쓰는데 Type-Safety가 지켜지지 않는다니.. 불편한 상황이다.
Prisma는 어떨까?
Prisma
Prisma는 자체가 ORM인 것은 아니고 Modern DB Toolkit이라고 할 수 있다.
- Client : 우리가 생각하는 ORM 모듈
- Migrate : DB Migrate 작업을 전담하는 모듈
- Studio : DB GUI 툴
로 구성되어 있으며 TypeORM과 다르게 Class가 아니라 별도의 파일로 모델을 정의하게 된다.
이제 위에서처럼 User의 이름 길이를 가져오는 함수를 프리즈마로 다시 작성해보자
위처럼 작성했을 때 Prisma는 name필드가 없는 타입이라고 에러가 뜨는 것을 확인할 수 있다.
이처럼 Prisma는 Type-Safe한 쿼리를 지원하며 모델을 클래스로 정의하지 않기 때문에 비즈니스 로직과 DB모델이 섞이며 비대해지는 안티패턴을 구조적으로 예방해준다.
그렇다면 Prisma는 어떻게 Type-Safety를 보장하는 것일까?
먼저 Prisma는 우리가 미리 정의해둔 모델에 따라 Type-Safety를 보장하기 위한 타입을 수천줄에 걸쳐 미리 생성해둔다. 미리 생성된 타입에서 위에서 사용한 findFirst함수를 뜯어보면 아래와 같이 생겼음을 확인할 수 있다.
findFirst메소드의 리턴 값이 Type-Safety를 유지하고 있으므로 리턴 타입을 보면 될 거 같다.
리턴 타입을 간단히 포현해보자.
HasReject<
GlobalRejectSettings,
LocalRejectSettings,
'findFirst',
'Cat'
> extends True
? Prisma__CatClient<CatGetPayload<T>>
: Prisma__CatClient<CatGetPayload<T> | null, null>
Cat모델의 findFirst메소드가, 주어진 어떠한 세팅값(GlobalRejectSettings, LocalRejectSettings)에 대해 True.
즉 에러가 있다면 Prisma__CatClient<CatGetPayload<T>>를 리턴하고
문제가 없다면 Prisma__CatClient<CatGetPayload<T> | null, null>을 리턴하며 이 타입이 우리의 코드에서 받은 타입이다.
Prisma__CatClient는 Promise를 Prisma에서 내부적으로 Wrapping한 타입으로 두번째 제네릭의 기본값이 Never로 되어있기 때문에 에러가 없을 때 위에서처럼 null을 넣어준다. 그렇다면 Type-Safety의 비밀을 파헤치기 위해 마지막으로 CatGetPayload 타입을 뜯어봐보자.
드디어 Type-Safety의 비밀을 발견했다!
select와 include를 동시에 쓸 수 없는 등의 제약조건 타입을 지나 맨 아래에 전달받은 타입 S가 select 필드를 갖고 있을 경우, 아래와 같은 타입을 리턴한다.
{
[P in TruthyKeys<S['select']>]: P extends keyof Cat ? Cat[P] : never;
}
select필드에서 truthy한 키값(false, null, undefined 등이 아닌 값) 중에서 Cat(해당 모델)타입의 key이기도 한다면 그 타입만을 리턴해준다.
우리는 위에서 `{ id: true, email: true } `를 select의 인자로 넘겨주었다.
따라서 name과 post가 제외된
{
id: number;
email: string;
}
과 같은 타입이 리턴 되었던 것이다!
findFirst메소드 외에도 Prisma에서 제공해주는 모든 메소드들이 이런 식으로 Type-Safety를 보장하기 위해 schema.prisma파일을 반여하는 순간에 자동으로 만들어진다.
총 750억 규모의 투자를 받은 Prisma는 커뮤니티의 규모도 점점 커지고 있고 툴 자체도 점점 발전하는 등 좋은 모습을 보여주고 있다. Prisma의 사용량 자체도 점점 증가하고 있으니 Typescript 환경에서 ORM 사용을 고민하는 사람들에게 좋은 선택지가 될 것 같다.
'Node.js' 카테고리의 다른 글
Github Action으로 NPM 라이브러리 버전 관리하기 (0) | 2023.05.26 |
---|