graphql에서의 n+1문제
예전에 jpa로 orm을 처음 접했을 때 n+1문제에 대해 처음 알게되었는데 graphql을 사용하다 보니 같은 문제가 있다는 걸 알게되었다. 중첩된 형태로 객체를 요청해서 응답 받는 구조가 비슷하게 느껴지는데, 그래서 같은 문제가 발생하는 것 같다.
query{
getPhotos{
photoId
author{
nickname
}
}
}
예를 들어 위 처럼 작가의 닉네임이 포함된 사진정보 리스트를 요청하는 쿼리가 있고, Photo author 리졸버는 db에 접근해서 해당 photo의 author 정보를 가져온다고 했을 때, 10장의 사진을 요청한다면 photo list를 쿼리하는 db요청 1번에 각 photo에 대한 author 정보를 쿼리하는 db요청 10번이 발생할 것이다. 조회된 사진 갯수를 N이라 한다면 N+1번의 요청이 발생한다.
이전까지 나는 이 경우에 db에서 join으로 모든 데이터를 가져와서 Photo와 Author 형식에 맞게 바꿔서 클라이언트에게 응답을 보냈다. 그러나 같은 타입이라도 어느 쿼리에서 요청이 될 지 모르고, 모든 경우에 대해 적절히 데이터를 미리 담아서 응답할 수 없기 때문에 최적화에 굉장한 노력이 들어갔다.
dataloader
dataloader는 결국 db에서 데이터 조회를 캐시와 배치를 이용해서 최적화해주는 라이브러리이다.
import DataLoader from 'dataloader';
const userLoader = new DataLoader(
userIds => getUsersByIds(userIds)
);
위는 사용자 아이디들을 가지고 유저 리스트를 불러오는 userLoader 함수이다. 아래처럼 유저 정보를 불러오는 곳에서 사용하면 된다.
const userPromiseA = userLoader.load(1);
const userPromiseB = userLoader.load(2);
await Promise.all([userPromiseA,userPromiseB])
const userPromiseC = userLoader.load(1);
dataloader는 userPromiseA와 userPromiseB 처럼 동일한 실행 프레임에 있는 코드를 한 번에 처리한다.
[1,2] 코드를 가지고 dataloader에 정의한 함수로 userA, B를 불러온다.
주의할 점은 요청할 때 userIds와 getUsersByIds의 결과가 index와 길이가 같아야한다는 점이다.
그렇지 않으면 잘못된 결과를 받을 가능성이 있다. 따라서
userPromiseC 요청은 이전 프레임에서 발생했지만 1번 id를 가지고 있는 유저는 dataloader에 캐시되어있어 따로 요청을 보내지않고 바로 해당 유저를 리턴한다.
다음 리팩토링 때 context에 dataloader를 담아서 사용할 예정이다.