ORM에 대한 찬반양론
JPA와 TypeORM이 꽤 많이 쓰이는 것으로 인식되고 있지만, ORM의 효용성 여부는 아직까지도 실무자들 사이에 큰 논란이 되고 있다.
https://martinfowler.com/bliki/OrmHate.html
https://www.reddit.com/r/golang/comments/t08oox/are_orms_considered_an_antipattern_in_go/
ORM은 매우 구체적 사항이 누출된 추상화입니다.
사소하지 않은 일에 대해서도 SQL의 전문가이자 ORM의 전문가여야 합니다.
나는 너무 많은 똑똑한 사람들이 ORM 도구를 사용하여 SQL로 5분 짜리 작업이 되었을 작업을 수행하려고 씨름하는 것을 보았습니다.
위와 같이 ORM은 쓸데없이 시니어 프로그래머를 부르는 도구로 인식되고 있다.
ORM의 장점
기술면접에서 ORM의 장점을 쿼리를 대신 짜준다로 답변하면 아마 탈락할 것이다.
ORM의 장점은 Persistent Layer를 추상화하여, 애플리케이션 레벨에서 DBMS 종속성을 제거할 수 있다는 것이다.
나머지 장점은 덤이라 생각한다. 그리고 ORM의 복잡한 애노테이션이나 데코레이터가 없어도 가볍게 쓸 수 있는 방법은 있다.
ORM의 단점
복잡한 쿼리를 작성하는 데는 적합하지 않다. (JPQL or QueryDSL 활용)
생각했던 대로 쿼리가 나가지 않는다. (n+1 문제)
ORM과 DBMS 둘 다 높은 러닝 커브가 있다. 즉 학습 부하가 증가한다.
DBMS에 대한 전문지식은 BE 엔지니어로서 반드시 필요한 것이다.
ORM에 대한 지식은 라이브러리 Specific하고, 안쓰는 프로젝트도 많아서 필수적인 것은 아니다.
ORM이 꼭 필요한가?
ORM, 혹은 SQL query builder는 DBMS 추상화를 위해 존재하는 것이 좋다고 생각한다.
ORM을 객체 지향적으로 활용하지 않더라도, DBMS에 저장된 데이터 형태과, 애플리케이션에 로드할 데이터 모양은 다르기 때문에,
매퍼 형태로 존재하는것이 좋다. (보안 사항 등)
(영속 모델과 도메인 모델은 분리해야 한다.)
또한, Presentation Layer를 미리 추상화해 두는것은 분명 장점이다.
(실무에서 DBMS를 MySQL에서 PostgreSQL로 변경하라는 권고 사항을 들은 적이 있는데,
전부 raw sql로 작성되어 있어서 간단해보이지 않아 일단 보류했다.
물론 DBMS 자체를 마이그레이션 하는것도 간단하지는 않다.
DBMS를 사용하는 도구가 애플리케이션만 있는게 아닐 수 있다.)
문제 해결 : 성급한 추상화의 문제
개인적으로 액티브 레코드 패턴은 안티패턴이라 생각한다.
도메인 모델과 repository 레이어는 분리해야 한다.
아래는 TypeORM의 예제다
const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.age = 25
await user.save()
const allUsers = await User.find()
const firstUser = await User.findOneBy({
id: 1,
})
const timber = await User.findOneBy({
firstName: "Timber",
lastName: "Saw"
})
await timber.remove()
User 객체가 findOneBy를 갖고있다는 것에 대해 좀 생각해 봐야 한다.
이는 DMBS에서 해당 정보를 어떻게 갖고오는지(HowToDo)가 누출된 것이다.
반대로 레포지토리 레이어를 만들어, sql과 가장 간단한 문법을 추상화 하는 것은 어떤가? 아래는 knex.js를 사용한 예제다.
interface User {
id: number;
name: string;
age: number;
}
function getUserById(id:number){
return knex('users')
.where('id',id)
.first(); // Resolves to any
}
또한, JPA, JPQL, TypeORM, MicroORM 등 ORM 자체도 하나의 인프라스트럭쳐 기술이다.
특히 js 진영의 경우 JPA처럼 완전한 인터페이스가 존재한다고 보기 애매하다.
이를 위해 nest.js가 존재하지만... java처럼 닥치고 스프링인 상황이 아니기 떄문에, 생각보다 ORM은 교체하기가 어렵다.
따라서 Repository 레이어룰 인터페이스로 추상화하여 DIP를 적용하는 수밖에 없다.
(나는 꼭 nest.js를 사용해야 한다고 생각하지 않는다. 오히려 함수형 패러다임 측면에선 프레임워크가 불필요할 수도 있다.)
문제 해결 : n+1 문제
https://stackoverflow.com/questions/68981877/how-do-i-cause-n-1-problems-with-nestjs-typeorm
이는 영속성의 단위에 대한 이해도가 부족하여 발생하는 문제이다.
도메인 주도 설계에는 이를 해결하기 위한 단위인 Aggregate가 존재한다.
즉, 쿼리는 aggregate단위로 CRUD를 수행해야 한다.
aggregate는 트랜잭션의 원자성 단위다.
따라서 for문 안에서 findBy...를 하는 것은 애초에 잘못된 행위이다.
- Aggregate는 도메인의 일관성을 보장하고 데이터 트랜잭션의 원자성 단위로 사용되며, 단일 구성 요소로 처리되는 관련된 Entity의 모음입니다. 다른 Entity는 "Root"로 알려진 Aggregate의 "최상위" 구성원 ID인 식별자로만 Aggregate를 참조해야 합니다.
https://itchallenger.tistory.com/418
대부분의 ORM은 n+1 문제를 해결하기 위한 단위를 설정하는 기능을 지원한다.
TypeORM은 relation 설정에 eager를 true로 주는 것으로 해결한다만,
때때로 꼭 모든 테이블을 조인해서 가져올 필요가 없다는 점이 단점이다.
JPA는 EntityGraph 등의 기능도 지원하는데 말이다.
해당 문제는 아래 문단의 쿼리 빌더를 사용하면 해결된다.
쿼리빌더는 해당 쿼리를 그대로 DBMS에 날리기 때문에, eager, lazy 따위는 생각하지 않아도 된다.
문제 해결 : 복잡한 쿼리 문제
https://itchallenger.tistory.com/230?category=1057505
Java가 QueryDSL과 JOOQ등을 사용하듯,
타입스크립트의 경우 TypeORM의 QueryBuilder기능을 사용한다.
한마디로 SQL을 추상화하는 것이다.
SQL을 ANSI 표준 만으로 짜는건 어떠냐고 질문할 수도 있겠다. 그것도 괜찮다.
모든 DBMS에서 호환된다는 가정이 있다면 말이다.
하지만, 대부분의 SQL Builder 라이브러리가 이와 같은 Heavy Lifting(추상화)를 해주기 때문에,
코드 레벨에서는 그냥 SQL Builder를 가져다 쓰는게 좋다고 생각한다. 인텔리센스 지원도 받고 말이다.
문제는 TypeORM의 QueryBuilder가 생각보다 기능이 빈약하다는 점이다.
TypeORM인데 타입 세이프티가 없다.
그래서 나는 개인적으론, TypeORM을 추천하지 않는다.
Prisma도 비추한다. 쓸데없이 알 내용이 너무 많다고 생각한다. 그래서 typeorm을 못이기는것 같다.
(해당 라이브러리 specific한 내용이 넘많음~)
물론 나는 FE엔지니어 이기에 여러분이 걸러들어야 할 내용들도 있다.
TypeORM의 대체품
https://vincit.github.io/objection.js/
날쿼리만 추상하는 방법. 의외로 상당히 많이 쓰임.
꼭 TypeORM을 써야겠으면 아래 라이브러리도 Check-Out
https://github.com/samchon/safe-typeorm
'FrontEnd' 카테고리의 다른 글
XState와 비동기 1편: useEffect 안의 비동기 코드는 위험합니다! (0) | 2022.04.30 |
---|---|
XState : 상태 머신과 상태차트 소개 (0) | 2022.04.30 |
XState : 액터 모델 간단히 알아보기 (0) | 2022.04.30 |
타입스크립트 : 탬플릿 리터럴 타입으로 타입 안전하게 코딩하기 (0) | 2022.04.29 |
React와 Typescript를 함께 사용하기 : 유용한 유틸리티 타입 (0) | 2022.04.28 |