일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 백준
- mutation
- REST API
- 실패율
- html
- await
- 자바스크립트
- 코딩테스트
- Bandit
- typeorm
- kakao
- nestjs
- typescript
- 코딩태스트
- 카카오
- ROT13
- RestAPI
- javascript
- node
- Query
- 피보나치 수
- vanila js
- graphql
- 모던 자바스크립트
- js
- 프로그래머스
- linux
- tr명령어
- 자바스크립트의 역사
- JavaStritp
- Today
- Total
꿀 떨어지는 코딩 양봉장
Graphql + DataLoader를 이용한 N+1문제 해결 본문
DataLoader
Graphql에서는 N+1 문제를 해결하기 위한 방법인 DataLoader에 대해서 소개를 하겠습니다. 또 Graphql에서 DataLoader를 어떤 방법으로 적용해야 하는지 정리해 보려고 합니다.
N+1 문제
N+1 문제란 성능에 관한 문제 중 하나로 주로 관계형 데이터베이스에서 1:N 관계를 가지는 테이블에서 일어나는 문제입니다.
데이터베이스에 Author 테이블과 Post테이블이 있다고 가정해봅시다. 한 Author는 여러 개의 Post를 작성할 수 있습니다. 1:N 관계를 형성합니다. 이때, 모든 Author의 모든 Post를 가져오고자 한다면,
SELECT * FROM Author;
먼저, 모든 Author를 가져온 후, n개의 author에 대해서 post를 가져올 수 있습니다.
SELECT * FROM Post WHERE author_id = ?;
n개의 Author들을 가져오는 쿼리가 1번 실행됩니다. 그리고 n명의 author에 대한 Post를 가져오는 쿼리가 n번 실행됩니다. 즉 쿼리가 N+1번만큼 실행됩니다.
이것이 N+1 문제로, 위처럼 작성하면 성능 측면에서 지극히 비효율적이지만 로직을 이해하기는 쉽다는 장점이 있습니다.
그렇지만 이렇게 하위 엔티티들을 첫 쿼리 실행 시 한 번에 가져오지 않고, Lazy Loading으로 불러오기에 필요한 곳에서 사용되어야 할 때 쿼리가 실행되는 문제가 생깁니다.
위 N+1문제를 where ~ in
을 사용하거나, join
을 상용하여 하나의 쿼리로 해결할 수 있습니다.
const resolver = {
Query:{
posts: async()=>{
const authors = await Post.find({relations:['posts'})
return authors;
}
}
}
하자만 위와 같이 resolver를 구성한다면 query에서 posts 필드를 요청하지 않아도 join 해서 data를 가져오게 됩니다. client가 받는 데이터는 overfetching이 일어나지 않지만 실제 데이터를 load 하는 곳에서 overfetching이 일어나게 됩니다.
Graphql에서의 N+1 문제
내부적으로 authors쿼리는 모든 authors를 데이터베이스에서 가져오고, authors의 post필드는 author의 id를 받아 post를 가져오는 쿼리가 될 것입니다. 결국 위에서 말한 N+1문제가 일어나게 됩니다 이를 해결해 줄 수 있는 것이 Dataloader입니다.
Dataloader
Dataloader는 data fetch 할 때 나타나는 N+1 문제를 batching(일괄 처리)을 통해 1+1로 변환해주는 라이브러리입니다.
Dataloader는 Graphql에서 자주 쓰이지만, 다른 상황에서도 쓰일 수 있습니다. 기본 개념은 데이터베이스에 대한 요청을 Batching(일괄처리)과 Cacing(캐싱)하는 것입니다.
이 중 N+1문제를 해결하는 것은 Batching입니다. Dataloader는 같은 데이터베이스에 대한 각각의 요청을 한 번에 모아서 요청을 보냅니다.
Dataloader는 javascript의 event-loop를 이용합니다. 주요 기능은 batching은 event-loop 중 하나의 trick에서 실행된 data fetch에 대한 요청을 하나의 요청으로 모아서 실행하고 그 결과를 다시 알맞게 분배하는 역할을 합니다.
const DataLoader = require("dataloader");
// 임시 data
const posts = [
{ id: 1, title: "test1" },
{ id: 2, title: "test2" },
{ id: 3, title: "test3" },
{ id: 4, title: "test4" },
{ id: 5, title: "test5" },
];
// fake db operation
const findAllPosts = () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(posts);
}, 100);
});
// batchLoadFn 의 결과는 promise여야 한다.
const batchLoadFn = async (keys) => {
const results = await findAllPosts();
console.log(keys);
console.log("🐶");
// db 에서 받아온 결과를 요청온 key에 mapping
return keys.map((k) => results.find((p) => p.id === k));
};
const postLoader = new DataLoader(batchLoadFn);
// tick 1
postLoader.load(1).then(console.log);
postLoader.load(2).then(console.log);
// tick 2
setTimeout(() => {
postLoader.load(3).then(console.log);
postLoader.load(4).then(console.log);
}, 100);
Dataloader의 constructor는 batch요청을 어떻게 처리할지에 대한 함수를 인자로 받습니다. 함수의 역할은 하나의 tick에서 들어온 post들에 대한 요청을 모아서 하나의 요청을 만듭니다.
setTimeout은 event-loop 상에 하나의 tick에서 실행되지 않고 다음으로 실행을 미루게 됩니다.
위 코드의 결과는 아래와 같습니다.
sub-resolver에 대해서 공부하다가 dataloader에 대해서 알게 되었는데 다음번에는 graphql에서 실제 사용해보고 eventloop에 대해서도 더 자세히 알아보겠습니다.
'Computer Science > GraphQL' 카테고리의 다른 글
GraphQL subscription (0) | 2022.01.31 |
---|---|
GraphQL 핵심 요소 (0) | 2021.07.24 |
GraphQL이란? (0) | 2021.07.20 |