본문 바로가기
Study/DATABASE & ORM

[TypeORM] N+1 문제와 Eager and Lazy Relations

by DawIT 2021. 12. 19.
320x100

N+1 문제

ORM을 사용하면서 항상 주의해야 할 문제점 중 하나가 N + 1 문제이다.

 

N + 1 문제란, 어떤 테이블의 참조된 데이터를 가져오기 위해 해당 (테이블 조회(1) + 참조된 데이터 조회(N)) 회의 쿼리를 날리는 문제를 이야기한다. JPA에서 많이 언급되는데 JPA 에서만 국한된 이야기는 아니고 ORM을 잘못 사용한다면 어디서든 발생할 수 있는 문제이다.

 

TypeORM을 예로 들어 설명해 본다.

 

 

아주 간단한 구조를 가지고 있는 두 테이블이 설정된 상황이다. author에는 5개의 row 가 들어가 있다.

 

만약 이러한 상황에서, 예를 들어 저자명과 해당 저자가 작성한 책들을 각각 리턴해야하는 getBooksAndAuthorName() 이라는 함수를 작성한다고 하면, 다음과 같이 작성하고 싶을 것이다.

 

리턴이나 타입 등은 제쳐두고 핵심적인 부분만 작성했다.

 

그러나 상단의 코드처럼 작성하게 되면, author.books 는 undefined 이다. 이는 TypeORM에서 기본적으로 find 함수를 사용했을 때 하위 엔티티를 가져오지 않기 때문이다. 울며 겨자먹기로 다음과 같은 코드를 작성했다고 해보자.

 

 

상당히 기분이 언짢다. 이렇게 하고 실제로 어떤 쿼리가 날아가는지 확인한다.

 

 

처음 author를 전부 가져오는 쿼리(1) 와 각각의 author에 대한 books를 가저오는 쿼리(5) 해서 총 6번의 쿼리를 날린다. 이런 상황이 N + 1 문제이다. 지금은 Row 가 5개 밖에 없어서 6번이지만, 1억 건의 데이터가 있다고 했을 때 1억 1번의 쿼리를 날리는것은 확실히 바람직하지 못한 상황이다.

 

JOIN 을 통한 해결

상단과 같은 문제를 어떻게 해결해야 할까? 관계형 데이터베이스의 꽃이라고 부를 수 있는 JOIN을 사용하면 된다. 처음부터 authors를 조회할 때, 해당 author에 속한 books까지 함께 가져오는 것이다.

 

 

간단하게 find 시 relations 조건을 주어 JOIN 하도록 만들었다. 이렇게 하고 실행된 쿼리와 결과를 확인해본다.

 

 

정상적으로 author의 books 까지 JOIN을 통해 한번의 쿼리로 가져온 모습을 확인할 수 있다.

Eager Relations

TypeORM에서 Eager Relations관계를 설정해 두면, 상위 엔티티를 로드했을 때, 그 하위 엔티티까지 모두 로드되게 된다. 이는 Entity 클래스에서 eager 옵션을 true 로 두면 사용할 수 있다.

 

상단의 예제에서 적용해본다.

 

 

Author를 로드할때마다 relations를 넣지 않고 books도 함께 가져오고 싶다면 Author 클래스에서 books에 대한 OneToMany 데코레이터에서 eager 옵션을 true로 놓으면 된다.

 

 

이제 아까의 코드에서 find 에 relations를 따로 걸지 않아도 알아서 books 까지 가져오는 모습을 확인할 수 있다.

 

하위 엔티티가 무조건 가져와지기 때문에 상당히 편하다고 느낄 수 있지만, author를 사용할때 books가 필요하지 않은 모든 경우에 대해서도 books를 무조건 가져오기 때문에, 이또한 성능상 문제를 일으킬 수 있다.

Lazy Relations

Note: if you came from other languages (Java, PHP, etc.) and are used to use lazy relations everywhere - be careful. Those languages aren't asynchronous and lazy loading is achieved different way, that's why you don't work with promises there. In JavaScript and Node.JS you have to use promises if you want to have lazy-loaded relations. This is non-standard technique and considered experimental in TypeORM.

TypeORM의 Lazy Relation은 아직 실험 단계의 기능이라, JAVA나 PHP 등 Lazy Relation을 기본적으로 사용하는 언어처럼 막 쓰면 안된다고 합니다.

 

Lazy Relations을 설정해 두면, 해당 엔티티에 접근할 때 데이터를 가져온다.

 

 

Author 엔티티에 books의 타입을 Promise 로 설정한다. 이는 해당 관계를 Lazy Relations로 설정한다는 의미이다.

 

이제 author 에서 await author.books 를 통해 Book 엔티티에 접근할 수 있다.

 

 

이번에는 authorId를 통해 해당 저자의 책들만 가져오는 에시를 사용한다. 그리고 books에 접근하기 전과 후로 나누어 로그를 찍어본다.

 

 

로그를 확인해 보면 실제로 처음에는 author의 다른 정보만 가져오고, books에 접근하는 시점에 다시 쿼리를 날리는 것을 확인할 수 있다.

 

다른 몇몇 언어의 ORM(JPA, Laravel, ...) 은 기본적으로 이 Lazy Loading을 사용한다. 타 ORM에서 N + 1 문제가 자주 언급되는 것도 이때문이다. TypeORM은 별도로 설정해주지 않으면 Eager Loaging도, Lazy Loading도 하지 않는다. (하위 엔티티를 가져오지 않는다는 점에서 Lazy Loading이라고 생각할 수 있지만, 애초에 relation 을 정해주지 않으면 접근한다고 해도 undefined로 데이터를 가져오지조차 않는다는 점에서 Lazy Loading도 아니다)

결론

N + 1 문제의 간단한 개요와, 이를 해결하기 위한 JOIN, 그리고 Eager와 Lazy Relation(Loading)에 대해 알아보았다. N + 1 문제를 해결하기 위해서는 하위 엔티티의 필요 여부에 따라 이들을 적절히 선택하여 데이터를 가져오는것이 중요하다.

 

댓글