TypeORM N+1 - Lazy Loading
1. Lazy Loading이란?
레이지(Lazy) 로딩은 지연 로딩이라고도 불리우며, 즉시 로딩의 단점을 보완할 수 있다.
- READ 시 참조 데이터를 가져오지 않으므로 초기 로딩 시간이 상대적으로 짧으며, 자원 소비량이 상대적으로 적다.
또한, 지연 로딩 방식은 로딩 시 참조된 데이터까지 로딩하지 않고 현재 필요한 데이터만 가져오는 로딩 방식입니다. 즉, TypeORM의 초기 설정 상태로 데이터를 조회하는 경우 지연 로딩 방식으로 데이터를 가져올 것이라고 예측할 수 있다.
2. 지연 로딩 상태에서의 테스트
위에서 언급했던 예측의 결과가 맞는지 증명하기 위해 다시 User Entity의 설정 수정해주어야 한다. TypeORM에서 지연 로딩을 적용할 때 아래와 같이 두 가지 방법으로 나타낼 수 있다.
@OneToMany(() => Post, (post) => post.user, {
onDelete: 'CASCADE',
lazy: true,
})
posts: Post[];
@OneToMany(() => Post, (post) => post.user, {
onDelete: 'CASCADE',
lazy: true,
})
posts: Promise<Post[]>;
이어서 UsersService의 findAllUsers 메소드를 통해 모든 사용자 정보를 조회해보면 다음과 같다.
{
"user_id": 1,
"email": "choewy32@gmail.com",
"nickname": "choewy",
"password": "password",
"createdAt": "2022-04-24T19:48:16.156Z",
"updatedAt": "2022-04-24T19:48:16.156Z"
}
위의 결과를 보면 앞에서 예측한 결과가 정확하다는 것을 확인할 수 있다.
3. 참조 데이터 JOIN
지연 로딩 방식으로 참조된 데이터 정보까지 조회하기 위해서는 UsersService의 getOneUser() 메소드를 다음과 같이 수정해주어야 한다.
async getOneUser(userId: numer): Promise<User> {
const user: User = await this.userRepository.findOne(userId);
await user.posts;
return user;
}
이어서 getAllUsers() 메소드를 다음과 같이 수정하면 모든 사용자 정보와 게시물 정보를 한꺼번에 조회할 수 있다.
async getAllUsers(): Promise<User[]> {
const users: User = await this.userRepository.find();
users.forEach(async (user) => {
await user.posts;
});
return users;
}
{
"user_id": 1,
"email": "choewy32@gmail.com",
"nickname": "choewy",
"password": "password",
"createdAt": "2022-04-24T19:48:16.156Z",
"updatedAt": "2022-04-24T19:48:16.156Z",
"posts": [
{
"post_id": 1,
"content": "TEST1",
"image_url": null,
"createdAt": "2022-04-24T19:54:26.675Z",
"updatedAt": "2022-04-24T19:54:26.675Z",
"deletedAt": "2022-04-24T19:54:26.675Z",
"user_id": 1
},
{
"post_id": 2,
"content": "TEST2",
"image_url": null,
"createdAt": "2022-04-24T19:54:26.675Z",
"updatedAt": "2022-04-24T19:54:26.675Z",
"deletedAt": "2022-04-24T19:54:26.675Z",
"user_id": 1
},
{
"post_id": 3,
"content": "TEST3",
"image_url": null,
"createdAt": "2022-04-24T19:54:26.675Z",
"updatedAt": "2022-04-24T19:54:26.675Z",
"deletedAt": "2022-04-24T19:54:26.675Z",
"user_id": 1
}
]
}
4. 주의 사항 및 단점
기존에 Seqelize 등의 다른 ORM을 사용한 경험이 있다면 user의 posts를 조회해야 할 때, 굳이 await을 한 번 더 사용해서 조회해야 하는가에 대한 의문이 생길 수 있다. 이에 대해 TypeORM 측에서는 친절하게도 공식문서에 기록을 남겨놓았다.
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 archieved 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 ti have lazy-loaded relations. This is non-standard technique and considered experimental in TypeORM.
이를 해석해보면 다음과 같다.
Java, PHP 등과 같은 다른 언어에서 지연 로딩 방식을 사용하다가 TypeORM을 사용하는 경우 주의해야 한다. 그 언어들은 비동기화되지 않으며, 지연 로딩이 다른 방식으로 동작하므로 Promise를 사용할 수 없다. Javascript 또는 Node.js에서 지연 로딩 방식을 사용하기 위해서는 Promise가 사용된다. 이것은 비표준 방법이며, TypeORM에서의 실험적 기능이다.
정리해보자면, TypeORM에서 지연 로딩을 사용하기 위해서는 반드시 비동기 처리 객체인 Promise를 사용해야 하며, async/await 구문을 통해 비동기 처리를 쉽게 구현할 수 있다. 그런데, 지연 로딩 방식을 사용할 때 즉시 로딩 방식에 비해 많은 양의 쿼리가 발생한다는 단점이 있습니다. 예를 들어, 사용자가 여러명 존재하는 경우 즉시 로딩 방식의 쿼리문은 한 번 시행되는 반면, 지연 로딩 방식의 쿼리문은 사용자 별로 실행되는 것을 확인할 수 있다. 가령, 사용자가 10명인 경우, 1번의 사용자 테이블 조회 쿼리문과 10번의 사용자 별 게시물 테이블 조회 쿼리문이 실행된다.
query:
SELECT
`User`.`createdAt` AS `User_createdAt`,
`User`.`updatedAt` AS `User_updatedAt`,
`User`.`user_id` AS `User_user_id`,
`User`.`email` AS `User_email`,
`User`.`nickname` AS `User_nickname`,
`User`.`password` AS `User_password`,
FROM `users` `User`
query:
SELECT
`Post`.`deletedAt` AS `User_posts_deletedAt`,
`Post`.`createdAt` AS `User_posts_createdAt`,
`Post`.`updatedAt` AS `User_posts_updatedAt`,
`Post`.`post_id` AS `User_posts_post_id`,
`Post`.`content` AS `User_posts_content`,
`Post`.`image_url` AS `User_posts_image_url`,
`Post`.`user_id` AS `User_posts_user_id`
FROM `posts` `Post`
WHERE `Post`.`user_id` IN (?)
-- PARAMETERS: [1]
-- PARAMETERS: [2]
-- ...
-- PARAMETERS: [10]
따라서, 모든 데이터를 조회할 때에는 즉시 로딩 방식을, 특정 데이터를 조회할 때에는 지연 로딩을 적용시키는 등 상황에 따라 적절하게 적용시킬 필요가 있다.