본 포스트는 공식 레퍼런스를 참고해 GraphQL을 공부하며 직접 작성한 가이드 입니다. 본 포스트는 2021년 7월 최신 버전인 v16.2를 기준으로 작성되어 있습니다.
DataLoader
우리는 GraphQLResolver를 소개하는 포스트에서 LocalContext를 사용해 요청을 최적화하는 방법을 확인했었다. 다만 LocalContext는 요청에대한 리졸버 전역에서 공유하는 객체라 모든 리졸버에서 사용하기엔 어려운 부분이 있을 수 있다. 이런 방법을 위해 DataLoader를 통해 요청에 대한 Async Bulk Get을 이용해 캐싱처리하고 필요에 따라 데이터를 꺼내 바인딩을 할 수 있다. 무슨말인지 쉽게 이해하기 위해 GraphQL Pagination의 요청을 재사용해보자.
2021-07-31 17:09:34.278 INFO 22598 --- [nio-8080-exec-8] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : a434aaa4-05da-4747-be9c-ea60dc7a8dd8 2021-07-31 17:09:34.278 INFO 22598 --- [nio-8080-exec-8] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2, 3
하나의 웹툰 요청에 있어서 에피소드를 찾기 위한 호출이 한번 call되었음을 볼 수 있다. 만약 다수의 웹툰 요청을 한다면 어떻게 될까? 요청수를 3으로 바꾸고 요청해보자.
1 2 3 4 5 6
2021-07-31 17:11:12.640 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : a434aaa4-05da-4747-be9c-ea60dc7a8dd8 2021-07-31 17:11:12.640 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2, 3 2021-07-31 17:11:12.641 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : 7c38418d-3d43-4f10-b163-66784ea89609 2021-07-31 17:11:12.641 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2, 3 2021-07-31 17:11:12.642 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : 53c4891a-7825-4f40-a715-e9cecedd6a86 2021-07-31 17:11:12.642 INFO 22598 --- [nio-8080-exec-9] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2, 3
로그에서 볼 수 있듯이 서로 다른 웹툰에 있어서 에피소드를 찾는 콜이 3번이 호출되었다. 즉 이 코드는 N + 1 문제를 야기하는 코드인것이다. 이제 이 코드를 DataLoader를 사용해 최적화 하도록 리팩토링해보자.
public CompletableFuture<List<Episode>> episodes(WebToon webToon, List<Integer> episodeNos, DataFetchingEnvironment env) { log.info("request episode data for webToon id : {}", webToon.getId()); log.info("episode no of requests : {}", episodeNos.stream().map(Object::toString).collect(Collectors.joining(", ")));
2021-07-31 17:41:40.674 INFO 22801 --- [nio-8080-exec-6] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : 34d501bb-fb93-4a6f-8b53-7d185e517847 2021-07-31 17:41:40.675 INFO 22801 --- [nio-8080-exec-6] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2 2021-07-31 17:41:40.675 INFO 22801 --- [nio-8080-exec-6] c.s.g.resolver.webtoon.EpisodeResolver : request episode data for webToon id : c0db0e15-d890-4d55-8294-439d2d359639 2021-07-31 17:41:40.675 INFO 22801 --- [nio-8080-exec-6] c.s.g.resolver.webtoon.EpisodeResolver : episode no of requests : 1, 2 2021-07-31 17:41:40.676 INFO 22801 --- [eTaskExecutor-1] c.s.g.d.DataLoaderRegistryFactory : DataLoading of WebtoonIds For: 34d501bb-fb93-4a6f-8b53-7d185e517847, c0db0e15-d890-4d55-8294-439d2d359639
실제로 로그를 확인하면 DataLoader에 요청에 대한 정보가 여러건이 캐싱되고 한번의 Bulk Get으로 정보를 가져오는걸 확인할 수 있다. 하지만 코드가 뭔가 보기 좋지 않다. Pair.class로 정보를 넘기고 있기 때문이다. 만약 넘겨야 할 데이터가 더 커지면 어떻게될까? 매번 넘겨줄 데이터를 위한 클래스를 만들어줄 수 없는 노릇이다. 이제 이를 개선해보자. 이를 위해 MappedBatchLoaderWithContext를 사용하자.
public CompletableFuture<List<Episode>> episodes(WebToon webToon, List<Integer> episodeNos, DataFetchingEnvironment env) { log.info("request episode data for webToon id : {}", webToon.getId()); log.info("episode no of requests : {}", episodeNos.stream().map(Object::toString).collect(Collectors.joining(", ")));