GraphQL에서 DataLoader부분을 사용한다면 왜 사용하는지 알아야 합니다.
DataLoader란 GraphQL의 Sub-Query를 호출할 때 사용하는 기능입니다.
일괄 처리하기 위해 사용합니다. 그럼 Sub-Query를 왜 일괄 처리해야 하냐로 질문이 이어질 것입니다. 그럼 일괄처리를 안 했을 때의 문제를 먼저 알아봅시다.
일괄처리를 안 했을 때 GraphQL N+1 문제
GraphQL에서 Sub-Query를 사용할 경우의 예시입니다.
EX) 성적 테이블 A와 유저 정보 테이블 B가 있을 경우를 예시로 보겠습니다.
A테이블에 존재하는 B_id라는 칼럼이 B테이블의 PK 값인 테이블 이 존재합니다.
그렇다면 데이터 호출을 시작해봅시다.
A테이블의 경우 범위를 지정하여 5개의 값을 한 번의 커넥션으로 호출한다고 가정합니다. 그리고 GraphQL의 Sub-Query를 이용하여 B_id 값으로 B테이블의 정보를 호출하려 합니다. A테이블에서 반환된 Row 5개의 B_id를 각각 따로 호출해서 정보를 얻습니다. 이경우 DB커넥션을 Row 개수만큼 해야 합니다.
A 테이블 커넥션 1 + B테이블 커넥션 5(Row 개수만큼) 이렇게 100개 1000000개 호출하다 보면 DB커넥션 1000000 하게 되어 리소스가 낭비되는 것이 문제가 됩니다.
DataLoader란
GraphQL 데이터를 호출할 때 N+1 문제를 Batch 기능을 이용해 해결해주는 라이브러리입니다.
GraphQL에서 Sub-Query로 데이터를 호출하는 구조는 A테이블의 Key값으로 B테이블을 Value를 가져오는 방식인데 이 구조를 효율적으로 풀어낸 것이 DataLoader 라이브러리입니다.
주요 기능을 살펴봅시다.
1. Batching
GraphQL에서 Sub-Query를 통해 데이터를 호출할 때의 프로세스입니다. API 호출 시 Query를 통해 데이터를 반환합니다. Sub-Query는 반환되는 데이터중 하나를 key로 하여 새로운 쿼리를 호출하고 기존 key의 위치에 Sub-Query의 데이터를 반환합니다. 이를 데이터 로더는 load 함수를 통해 Sub-Query에서 사용할 key를 배열에 모아서 데이터 로더 함수에 전달합니다. key값을 이용해 얻은 객체를 조건에 맞게 반환해줍니다. 조건은 2가지이며 key배열과 동일한 길이의 배열을 반환하는 것과 기존의 key와 알맞은 위치 즉 , 동일한 index에 key로 얻은 결과인 객체를 반환해야 합니다.
2. Caching
데이터 로더를 사용해 호출한 함수 별로 캐시가 생성됩니다. 자체적으로 CacheMap을 가지고 있습니다. 캐시가 한번 생성되면 동일한 호출이 반복될 경우 DB에 접속하지 않고 메모리에 있는 값을 반환하여 리소스 낭비를 줄입니다. 단 호출하는 API의 매개변수가 자주 변경된다면 많은 Cache를 계속해서 맵핑할 수 있기 때문에 이 부분도 낭비가 될 수 있습니다. 잘 고려하여 사용하거나 캐시 생성 기능을 사용하지 않는 것이 방법이 될 수 있습니다.
사용법
먼저 node 서버 하나 만들어서 소스를 실행해보고 나서 로직 흐름대로 살펴보시면 됩니다.
import { ApolloServer, gql } from "apollo-server"; //graphql 서버 추가
// import { ApolloServer } from "apollo-server-express";
import DataLoader from "dataloader"; // 데이터 로더 추가
let port = '4001'; // 포트 선언
// 유저 정보데이터
const users = [
{
id: 1,
name: 'chan',
age: 19,
},
{
id: 2,
name: 'yeong',
age: 20,
},
{
id: 3,
name: 'cho',
age: 21,
},
];
//문서 정보 데이터
const posts = [
{
id: 1,
title: 'test post title 1',
contents: 'test content',
user: 1,
},
{
id: 2,
title: 'test post title 2',
contents: 'test content',
user: 2,
},
{
id: 3,
title: 'test post title 3',
contents: 'test content',
user: 3,
},
{
id: 4,
titles: 'test post title 4',
content: 'test content',
user: 3,
},
{
id: 5,
title: 'test post title 5',
contents: 'test content',
user: 1,
},
];
// 그래프큐엘 타입 선언
const typeDefs = gql`
type User{
id:Int
name:String
age:Int
}
type Post{
id:Int
title:String
contents:String
user:User
}
type Query{
posts:[Post]
}
`;
// 그래프큐엘의 서브쿼리
const getUsers = (ids) => {
return new Promise(
(res) =>
res(users.filter(
(user) => ids.includes(user.id)
)
)
);
};
// 각각의 dataloader.load로 가져온 key들을 배열로 받아 데이터 요청
const batchGetUser = new DataLoader(async (keys) => {
//키값으로 유저를 가져옴
const _users = await getUsers(keys);
const usersMap = {}; // 객체 생성
// 유저 아이디 키, 유저 정보 벨류 로 usersMap 객체에 저장
_users.forEach((user) => (usersMap[user.id] = user));
//해당 id가 존재하면 반환
return keys.map((id) => usersMap[id] || null); //데이터가 배열에 존재할경우 반환
}, {
// 테스트를 위한 캐시 false 이거안하면 캐시호출을 해서 로직을 건너뜀
cache: false
});
//호출할 리졸버
const resolvers = {
Query: {
// Data Fetcher
posts: async () => {
const copiedPosts = JSON.parse(JSON.stringify(posts));
const result = copiedPosts.map((post) => {
post.user = batchGetUser.load(post.user);
return post;
});
return result;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers }); // 서버등록
//서버실행
server.listen(port).then( ({ url }) => console.log(`server ready at ${url}`));
웹에서 호출할 부분
//http://localhost:4001/graphql 호출했을때 사용할 호출문
query{
posts{
id
title
contents
user{
id
name
age
}
}
}
DB에서 호출하기 위해 정리한 쿼리
function getUsers async(user) {
try {
if (!user) return [];
let inArrayAll = [];
//id 값을 받아서 in에 넣기위해 정리
user.map((id)=>inArrayAll = [...inArrayAll, JSON.stringify(id)] );
//join으로 이쁘게 합체
const queryString =
"SELECT * FROM USERS_INFO WHERE ( userId ) in (" + inArrayAll.join() + ")";
let results = await db.exe(queryString);
return results;
} catch (err) {
logger.error(": " + err);
throw new Error(err);
}
key값을 사용하여 or 연산을 통해 데이터를 호출합니다. DB와의 커넥션 수를 줄이기 때문에 성능이 향상되고 호출한 데이터를 캐싱하기 때문에 같은 데이터를 호출할 경우 비용이 감소됩니다.
고생하셨습니다. 성실한 코딩 하세요.
참고사이트