Spring Boot에서 GraphQL DataLoader로 N+1 해결
Spring Boot와 GraphQL DataLoader를 사용해 N+1 문제의 원리와 구현 방법, 검증 절차를 단계별로 설명
목차
개요
GraphQL을 도입한 애플리케이션에서 흔히 마주하는 성능 문제 중 하나가 N+1 문제다. 이 문제는 클라이언트에서 하나의 쿼리로 여러 연관 데이터를 요청할 때, 데이터베이스에 불필요하게 많은 쿼리가 발생하는 현상을 뜻한다. Spring Boot 환경에서 GraphQL DataLoader를 활용하면 이 문제를 효과적으로 완화할 수 있다. 아래에서는 N+1 문제의 개념부터 DataLoader 적용 예시, 검증 방법까지 실무 관점에서 차근차근 설명한다.
N+1 문제 이해
무슨 일이 벌어지는가
예를 들어 게시글 목록을 요청하면 각 게시글의 작성자 정보를 함께 반환하는 경우가 많다. 게시글 10개와 함께 작성자 정보가 필요하면, 게시글을 가져오는 1번 쿼리와 각 작성자 정보를 가져오는 추가 10번의 쿼리가 발생할 수 있다. 이렇게 총 N+1번의 쿼리가 실행되면 응답 지연과 DB 부하가 커진다.
왜 문제가 되는가
쿼리 수가 늘어나면 네트워크 왕복, DB 커넥션 사용, 쿼리 파싱 비용 등이 누적된다. 트래픽이 늘어나면 지연이 크게 증가하고, 스케일링 비용도 커진다. 따라서 동일한 데이터를 모아서 한 번의 배치 쿼리로 처리하는 것이 중요하다.
DataLoader 개념
DataLoader는 여러 요청을 모아서(batch) 한 번에 처리하고 결과를 요청자에게 분배하는 패턴을 제공한다. 요청을 지연시키지 않으면서 같은 이벤트 루프 또는 요청 컨텍스트 내에서 발생한 데이터 접근을 묶어 쿼리 수를 줄인다. GraphQL과 자연스럽게 결합되어 각 요청별로 DataLoader 인스턴스를 생성해 사용한다.
Spring Boot에서의 구현 흐름
구현의 핵심은 다음과 같다.
- DataLoader 등록: 각 HTTP 요청별로 DataLoaderRegistry를 생성
- Resolver에서 DataLoader 사용: 연관 데이터를 직접 조회하지 않고 DataLoader에게 위임
- BatchLoader 구현: 여러 키를 받아 한 번에 DB 조회를 수행
- 검증: 로그나 쿼리 모니터링으로 실제 쿼리 수 감소 확인
설정 및 코드 예시
다음은 간단한 설정과 Resolver 예시다. 게시글(Post)과 사용자(User)가 있고, Post에서 작성자 정보를 DataLoader로 조회한다고 가정한다.
BatchLoader 구현
package com.example.graphql;
import org.dataloader.BatchLoader;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class UserBatchLoader implements BatchLoader<Long, User> {
private final UserRepository userRepository;
public UserBatchLoader(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public CompletableFuture<List<User>> load(List<Long> ids) {
return CompletableFuture.supplyAsync(() -> {
List<User> users = userRepository.findAllById(ids);
Map<Long, User> map = users.stream()
.collect(Collectors.toMap(User::getId, u -> u));
return ids.stream().map(id -> map.get(id)).collect(Collectors.toList());
});
}
}
DataLoaderRegistry 등록
package com.example.graphql;
import org.dataloader.DataLoader;
import org.dataloader.DataLoaderRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataLoaderConfig {
@Bean
public DataLoaderRegistry dataLoaderRegistry(UserRepository userRepository) {
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("userLoader",
DataLoader.newDataLoader(new UserBatchLoader(userRepository)));
return registry;
}
}
Resolver에서 DataLoader 사용
package com.example.graphql;
import graphql.schema.DataFetchingEnvironment;
import org.dataloader.DataLoader;
import java.util.concurrent.CompletableFuture;
public class PostResolver {
public CompletableFuture<User> author(Post post, DataFetchingEnvironment env) {
DataLoader<Long, User> loader = env.getDataLoader("userLoader");
return loader.load(post.getAuthorId());
}
}
검증 방법
실제 적용 후에는 다음 항목을 점검한다.
- 쿼리 로그: N+1 발생 전후의 쿼리 수 비교
- 응답 시간: 평균 응답 시간과 p95 지표 확인
- 동시성 테스트: 동시에 많은 요청을 보냈을 때 DB 부하 변화
주의사항
- DataLoader는 요청 범위(Request-scoped)로 생성해야 데이터 누수 방지
- 캐싱 전략을 적절히 설정하면 동일 요청 내 불필요한 중복 조회를 줄일 수 있음
- 복잡한 연관 관계에서는 배치 쿼리의 결과 정렬과 매핑에 유의
마무리
Spring Boot에서 GraphQL DataLoader를 도입하면 N+1 문제로 인한 불필요한 쿼리 폭증을 효과적으로 줄일 수 있다. 핵심은 각 요청마다 DataLoader를 생성해 BatchLoader에 위임하고, 결과를 클라이언트에 분배하는 구조를 만드는 것이다. 적용 후에는 로그와 성능 지표를 통해 실제 개선 효과를 검증하는 것이 중요하다. 이 과정을 통해 시스템의 응답 속도와 확장성을 개선할 수 있다.