You will be fine

<GraphQL> 6. DataLoader with Spring Boot

by BFine
반응형

가. DataLoader 란?

 a. Docs

  -  지난 포스팅[2022.04.06 - [공부(2021)/GraphQL] - 5. 비동기 처리 / N+1 문제 with Spring Boot]에서 DataLoader를 사용해보았다.
  -  좀 더 자세히 살펴보면 위의 클래스 설명을 보면 요청한 key값들에 대한 배치 로딩을 지원하는 유틸 클래스이다.
     => 일반적으로 각각의 key값에 대한 실행은 비동기로 처리 된다. (return은 CompletableFuture)
  -  아래에서 DataLoader의 각각의 필드가 어떤 역할인지 확인해보자.

 

 b. DataLoaderHelper

  -  DataLoaderHelper   : DataLoader 클래스의 기능을 분리한 클래스 

  -  StatisticsCollector  : DataLoader 실행에 관련된 상태 정보들을 저장해두는 클래스

 

 c. CacheMap & ValueCache

  -  DataLoader의 cache 옵션이 true일때 사용되는 저장소들로 2단계 전략을 가지고 있다.

  -  CacheMap은 일반적인 Map으로 구성되어있고 Value 값이 CompletableFuture<V> 형태이다.
  -  ValueCache는 수명이 길거나 외부 cache 처리가 필요할때 사용한다.
    => 디폴트는 NoOpValueCache를 사용하기 때문에 값을 이중 처리 하지는 않는다.
    => NoOpValueCache 외에 다른 구현체가 없어서 추가로 필요한 경우에 직접 커스텀 클래스로 만들어야 하는 것으로 보인다.

 

나. DataLoader 만들기

private DataLoaderRegistry dataLoaderRegistry(){

    BatchLoader<String,String> locationBatchLoader = new BatchLoader<String, String>() {
        @Override
        public CompletionStage<List<String>> load(List<String> kindList) {
            return CompletableFuture.supplyAsync(()-> placeService.getLocationList(kindList));
        }
    };

    DataLoader<String, String> locationDataLoader = DataLoaderFactory
            .newDataLoader(locationBatchLoader);

    return DataLoaderRegistry.newRegistry()
            .register("locationDataLoader",locationDataLoader)
            .build();
}

 a. BatchLoader 

  -  지난번에 동물들의 위치를 확인하는 DataLoader를 등록하는 부분을 만들었었는데 람다를 풀어서 다시 써보았다.
     => 위의 .load 의  kindList에는 동물의 종류가 들어온다. (ex. 개, 호랑이, 기린 등)

  -  일반적으로 DataLoaderFactory를 통해 DataLoader 생성해야하며 이를 위한 파라미터 값으로 BatchLoader를 받는다.
  -  BatchLoader는 FunctionalInterface이며 각각의 key 값에 대해서 값을 load하는 역할을 하는 DataLoader의 핵심 부분이다.

  -  DataLoaderFactoryDataLoader 생성 메서드는 다양한 BatchLoader 클래스들을 이용하여 생성한다. 아래에서 살펴보자. 

 

 b.  MappedBatchLoader

  -  MappedBatchLoader는 클래스명에서 느껴지듯 return은 Map형태, 파라미터로는 Set을 가지는 것을 볼 수 있다.   

  -  로직도 맞게 변경해보았는데 List로 변환 작업을 줄여서 좀 더 간결하게 만들 수 있었다. 

  -  내부로직을 비교해보면 key값에 대해 한번 Set으로 변환하여 중복을 제거한 후 .load 메서드에 파라미터로 전달해서 실행한다.

    => 여기서 .load 메서드는 위의 BatchLoader를 익명함수로 구현하여 service 로직을 등록한 그 부분이 실행된다.
  -  그리고 아래 List와는 다르게 Map으로 결과를 받아서 List 형태로 다시 변환해주는 작업이 추가되어 있는 것을 볼 수 있다. 

  -  여기서 주의할 부분은 BatchLodaer를 쓰는 경우에 전체 중복포함된 keys값이 들어올거라 생각 할 수도 있는데 cache 설정에 따라 다르다.

    => cache 설정이 true인 경우에는 앞에서 CacheMap으로 처리하기 때문에 BatchLoader도 마찬가지로 중복이 제거 되어 keys 값에 추가가 된다.

 

 c. Try

  -  DataLoaderFactory 메서드를 살펴보면 BatchLoader 의  Value 값이 Try<V> 로 되어있는 것을 볼 수 있다. 어떤건지 살펴보자

  -  내용을 보면 실행결과나 Exception을 가지고 있는 상태를 담는 wrapper 클래스로 보인다.

  -  배치 처리에서 중요한 부분은 100개를 처리하다 1개라도 오류가 났을때 어떻게 처리할 것 인지가 중요하다.

     => 이부분을 위해 Try 클래스로 결과를 wrap 해서 성공과 예외에 대한 개별적인 파악이 가능하다.

    private DataLoaderRegistry dataLoaderRegistry(){

        BatchLoader<String, Try<String>> locationBatchLoaderTry = new BatchLoader<String, Try<String>>() {
            @Override
            public CompletionStage<List<Try<String>>> load(List<String> keys) {

                return CompletableFuture.supplyAsync(()->placeService.getLocationListWithTry(keys));
            }
        };

       DataLoader<String, String> locationDataLoaderTry = DataLoaderFactory.newDataLoaderWithTry(locationBatchLoaderTry);
       return DataLoaderRegistry.newRegistry()
                .register("locationDataLoaderTry",locationDataLoaderTry)
                .build();
    }
}
@Slf4j
@RequiredArgsConstructor
@Service
public class PlaceService {

    private final PlaceRepository placeRepository;

    public List<Try<String>> getLocationListWithTry(List<String> kindList){
        log.info("Service 실행");
        List<Place> placeList = placeRepository.findAll();

        List<Try<String>> locationListTry = new ArrayList<>();
        for (String kind : kindList){
            Try<String> location = Try.tryCall(() -> getLocation(placeList, kind));
            locationListTry.add(location);
        }
        return locationListTry;
    }

    private String getLocation(List<Place> placeList, String kind) {
        log.info("Try 실행");
        if("개".equals(kind)){
            throw new IllegalArgumentException("[개]일 경우 IllegalArgumentException");
        }
        Map<String, String> placeMap = placeList.stream()
                .collect(Collectors.toMap(Place::getKind, Place::getPlaceName));
        String location = placeMap.get(kind);
        return location;
    }
}

  -  Try 를 테스트 해보기위해 살짝 이상하지만 private 메서드로 분리해 실제 값을 가져오는 부분은 람다로 추가해주었다.

  -  그리고 예외 상황에서 어떤 결과 값과 로그가 나오는지 확인하기 위해 throw를 추가하고 테스트 해보았다.

  -  결과를 보면 error 메세지가 발생을 했지만 결과로는 kind가 개 인 경우를 제외하고 정상적인 데이터를 받아오는 것을 볼 수 있다.

  -  로그도 살펴보면 어떤 Element에 어느 필드가 오류났는지에 대한 내용이 출력되는 것을 볼 수 있다. 
  -  처음엔 Try 가 무슨 클래스 인지 했는데 해보니 상당히 중요한 클래스로 느껴졌고 최대한 활용해야 할 것 같다. 

 

 d. DataLoaderOptions

   -  DataLoaderOptions 를 통해 DataLoader의 동작을 제어할 수 있다. 위의 이미지에서 보이는 것 처럼 여러 설정을 지원하고 있다.

   -  여기서 .setMaxBatchSize 는 keys 값의 개수를 제어해서 service 로직 호출 횟수가 달라질 수 있다. (디폴트는 -1)

다. DataLoaderHelper

 a. 핵심기능

  -  위에서 살펴보았던 DataLoader의 가장 중요한 핵심 기능을 처리하는 DataLoaderHelper의 내부를 살펴봐야겠다.

  -  주의할 부분은 DataLoader 를 필드값으로 가지고 있는 것을 볼 수가 있다.

 

 b. loaderQueue

  -  loaderQueue는 배치로딩을 하기 전 key 값을 저장해두고 이를 이용해 실행결과(CompletableFuture)를 저장해두는 역할을 한다.  
   => dataLoader를 synchroized하는 것으로 보아 Thread-safe하게 처리하고 있는 것으로 보인다.

 

 c. dispatch

  -  loaderQueue에서 저장했던 key 값과 CompletableFuture(빈껍데기) 및 Context를 꺼내서 하위로 전달한다.

  -  위에서 받은 값들 통해 .invokeLoader 를 실행하여 CompletableFuture로 결과들을 받아온다. 
    => 추가 처리로 실행처리한 값으로 loaderQueue에 있던 CompletableFuture에 실행결과를 complete 처리를 한다.

  -  이 결과(CompletableFuture)는 DispatchResult 형태로 wrap 되어 DataLoaderRegistry로 전달된다.

 

 

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기