일괄 처리 기능이있는 Java 8 Stream


95

항목 목록이 포함 된 큰 파일이 있습니다.

항목의 일괄 처리를 만들고이 일괄 처리로 HTTP 요청을 만들고 싶습니다 (모든 항목은 HTTP 요청의 매개 변수로 필요함). for루프를 사용 하면 매우 쉽게 할 수 있지만, Java 8 애호가로서 Java 8의 Stream 프레임 워크로 이것을 작성해보고 싶습니다 (그리고 지연 처리의 이점을 누리고 있습니다).

예:

List<String> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < data.size(); i++) {
  batch.add(data.get(i));
  if (batch.size() == BATCH_SIZE) process(batch);
}

if (batch.size() > 0) process(batch);

나는 긴 줄을하고 싶다. lazyFileStream.group(500).map(processBatch).collect(toList())

이를 수행하는 가장 좋은 방법은 무엇입니까?


그룹화를 수행하는 방법을 잘 모르겠습니다. 죄송하지만 Files # lines 는 파일 내용을 느리게 읽습니다.
— Toby

1
그래서 기본적으로 flatMap(+ 스트림을 다시 축소하기 위해 추가 flatMap) 의 역이 필요 합니까? 나는 표준 라이브러리에서 편리한 방법으로 그런 것이 존재한다고 생각하지 않습니다. 타사 라이브러리를 찾거나 스플리터 및 / 또는 스트림 스트림을 방출하는 수집기를 기반으로 자체 라이브러리를 작성해야합니다
— the8472

3
어쩌면 당신은 결합 할 수 있습니다 Stream.generate로 reader::readLine하고 limit있지만, 문제는 스트림 예외로 잘되지 않는다는 것입니다. 또한 이것은 아마도 병렬화가 잘되지 않을 것입니다. for루프가 여전히 최선의 선택 이라고 생각합니다 .
— tobias_k 2015-06-04

방금 예제 코드를 추가했습니다. 나는 flatMap이 갈 길이라고 생각하지 않습니다. 내가 가질 수 있음을 의심하는 것은 사용자 정의 Spliterator 쓰기
— 앤디 젠장

1
나는 이와 같은 질문에 대해 "스트림 악용"이라는 용어를 만들고 있습니다.
— kervin

답변:


13

노트! 이 솔루션은 forEach를 실행하기 전에 전체 파일을 읽습니다.

단일 스레드, 순차 스트림 사용 사례를 위해 Java 8 스트림을 확장하는 라이브러리 인 jOOλ 를 사용하여 수행 할 수 있습니다.

Seq.seq(lazyFileStream)              // Seq<String>
   .zipWithIndex()                   // Seq<Tuple2<String, Long>>
   .groupBy(tuple -> tuple.v2 / 500) // Map<Long, List<String>>
   .forEach((index, batch) -> {
       process(batch);
   });

이면 zipWithIndex()은 다음과 같습니다.

static <T> Seq<Tuple2<T, Long>> zipWithIndex(Stream<T> stream) {
    final Iterator<T> it = stream.iterator();

    class ZipWithIndex implements Iterator<Tuple2<T, Long>> {
        long index;

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public Tuple2<T, Long> next() {
            return tuple(it.next(), index++);
        }
    }

    return seq(new ZipWithIndex());
}

...하지만 groupBy()API 편의는 다음과 같습니다.

default <K> Map<K, List<T>> groupBy(Function<? super T, ? extends K> classifier) {
    return collect(Collectors.groupingBy(classifier));
}

(면책 조항 : 나는 jOOλ 뒤에있는 회사에서 일합니다)


와. 이것이 바로 제가 찾고있는 것입니다. 이 자바 8로 이동합니다 잘 맞는 될 것이라고, 그래서 우리의 시스템은 일반적으로 공정 데이터를 순서대로 스트림
— 앤디 젠장

16
이 솔루션은 전체 입력 스트림을 중간에 불필요하게 저장합니다 Map(예 : Ben Manes 솔루션과 달리)
— Tagir Valeev

124

완전성을 위해 여기에 Guava 솔루션이 있습니다.

Iterators.partition(stream.iterator(), batchSize).forEachRemaining(this::process);

질문에서 컬렉션을 사용할 수 있으므로 스트림이 필요하지 않으며 다음과 같이 쓸 수 있습니다.

Iterables.partition(data, batchSize).forEach(this::process);

11
Lists.partition제가 언급 했어야 할 또 다른 변형입니다.
— Ben Manes

2
게 으르 죠? 그것은 전체 호출하지 않습니다 Stream관련 일괄 처리하기 전에 메모리를
— orirab

1
@orirab 예. batchSize반복 당 요소 를 소비하므로 배치간에 게으 릅니다.
— Ben Manes 2019


58

순수한 Java-8 구현도 가능합니다.

int BATCH = 500;
IntStream.range(0, (data.size()+BATCH-1)/BATCH)
         .mapToObj(i -> data.subList(i*BATCH, Math.min(data.size(), (i+1)*BATCH)))
         .forEach(batch -> process(batch));

JOOl과 달리 병렬로 잘 작동 할 수 있습니다 ( data임의 액세스 목록 인 경우).


1
데이터가 실제로 스트림이면 어떻게 될까요? (파일 또는 네트워크에서 줄을 말할 수 있습니다).
— Omry Yadan

6
@OmryYadan, 질문은 List( 질문에서 참조 data.size()) data.get()에서 입력을받는 것에 관한 것이 었습니다 . 나는 질문에 대답하고있다. 다른 질문이 있으면 대신 질문하세요 (스트림 질문도 이미 질문 된 것 같습니다).
— Tagir Valeev

1
배치를 병렬로 처리하는 방법은 무엇입니까?
— soup_boy

37

순수 Java 8 솔루션 :

이를 우아하게 수행하기 위해 사용자 지정 수집기를 만들 수 있으며, 각 배치를 처리 batch size하기 Consumer위해 a 와 a 를 사용할 수 있습니다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.Objects.requireNonNull;


/**
 * Collects elements in the stream and calls the supplied batch processor
 * after the configured batch size is reached.
 *
 * In case of a parallel stream, the batch processor may be called with
 * elements less than the batch size.
 *
 * The elements are not kept in memory, and the final result will be an
 * empty list.
 *
 * @param <T> Type of the elements being collected
 */
class BatchCollector<T> implements Collector<T, List<T>, List<T>> {

    private final int batchSize;
    private final Consumer<List<T>> batchProcessor;


    /**
     * Constructs the batch collector
     *
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     */
    BatchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        batchProcessor = requireNonNull(batchProcessor);

        this.batchSize = batchSize;
        this.batchProcessor = batchProcessor;
    }

    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return (ts, t) -> {
            ts.add(t);
            if (ts.size() >= batchSize) {
                batchProcessor.accept(ts);
                ts.clear();
            }
        };
    }

    public BinaryOperator<List<T>> combiner() {
        return (ts, ots) -> {
            // process each parallel list without checking for batch size
            // avoids adding all elements of one to another
            // can be modified if a strict batching mode is required
            batchProcessor.accept(ts);
            batchProcessor.accept(ots);
            return Collections.emptyList();
        };
    }

    public Function<List<T>, List<T>> finisher() {
        return ts -> {
            batchProcessor.accept(ts);
            return Collections.emptyList();
        };
    }

    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

선택적으로 도우미 유틸리티 클래스를 만듭니다.

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collector;

public class StreamUtils {

    /**
     * Creates a new batch collector
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     * @param <T> the type of elements being processed
     * @return a batch collector instance
     */
    public static <T> Collector<T, List<T>, List<T>> batchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        return new BatchCollector<T>(batchSize, batchProcessor);
    }
}

사용 예 :

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> output = new ArrayList<>();

int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> output.addAll(xs);

input.stream()
     .collect(StreamUtils.batchCollector(batchSize, batchProcessor));

누군가가 살펴보고 싶다면 GitHub에도 내 코드를 게시했습니다.

Github에 연결


1
스트림의 모든 요소를 ​​메모리에 넣을 수없는 경우가 아니라면 이것은 좋은 솔루션입니다. 또한 끝없는 스트림에서는 작동하지 않습니다. 수집 방법은 터미널입니다. 즉, 일괄 처리 스트림을 생성하는 대신 스트림이 완료 될 때까지 기다린 다음 결과를 일괄 처리합니다.
— Alex Ackerman

2
@AlexAckerman 무한 스트림은 피니셔가 절대 호출되지 않음을 의미하지만 누적 기는 계속 호출되므로 항목은 계속 처리됩니다. 또한 한 번에 항목의 배치 크기 만 메모리에 있으면됩니다.
— Solubris

@Solubris, 당신이 맞아요! 이 점을 지적 해 주셔서 감사합니다. 누군가 collect 메소드가 작동하는 방식에 대해 동일한 아이디어를 가지고 있다면 참조 용 주석을 삭제하지 않겠습니다.
— Alex Ackerman

소비자에게 전송 된 목록은 안전하게 수정할 수 있도록 복사해야합니다. 예 : batchProcessor.accept (copyOf (ts))
— Solubris

19

이와 같은 시나리오를 위해 사용자 지정 Spliterator를 작성했습니다. 입력 스트림에서 주어진 크기의 목록을 채 웁니다. 이 접근 방식의 장점은 지연 처리를 수행하고 다른 스트림 기능과 함께 작동한다는 것입니다.

public static <T> Stream<List<T>> batches(Stream<T> stream, int batchSize) {
    return batchSize <= 0
        ? Stream.of(stream.collect(Collectors.toList()))
        : StreamSupport.stream(new BatchSpliterator<>(stream.spliterator(), batchSize), stream.isParallel());
}

private static class BatchSpliterator<E> implements Spliterator<List<E>> {

    private final Spliterator<E> base;
    private final int batchSize;

    public BatchSpliterator(Spliterator<E> base, int batchSize) {
        this.base = base;
        this.batchSize = batchSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<E>> action) {
        final List<E> batch = new ArrayList<>(batchSize);
        for (int i=0; i < batchSize && base.tryAdvance(batch::add); i++)
            ;
        if (batch.isEmpty())
            return false;
        action.accept(batch);
        return true;
    }

    @Override
    public Spliterator<List<E>> trySplit() {
        if (base.estimateSize() <= batchSize)
            return null;
        final Spliterator<E> splitBase = this.base.trySplit();
        return splitBase == null ? null
                : new BatchSpliterator<>(splitBase, batchSize);
    }

    @Override
    public long estimateSize() {
        final double baseSize = base.estimateSize();
        return baseSize == 0 ? 0
                : (long) Math.ceil(baseSize / (double) batchSize);
    }

    @Override
    public int characteristics() {
        return base.characteristics();
    }

}

정말 도움이됩니다. 누군가 사용자 지정 기준 (예 : 컬렉션의 크기 (바이트))을 일괄 처리하려는 경우 사용자 지정 조건자를 위임하고 for-loop에서 조건으로 사용할 수 있습니다 (이때 imho while 루프가 더 읽기 쉽습니다)
— pls

구현이 올바른지 잘 모르겠습니다. 예를 들어, 기본 스트림이 SUBSIZED분할에서 반환되는 trySplit경우 분할 이전보다 더 많은 항목을 가질 수 있습니다 (분할이 배치 중간에 발생하는 경우).
— Malt

@Malt에 대한 이해 Spliterators가 정확하다면 trySplit항상 데이터를 거의 동일한 두 부분으로 분할하여 결과가 원본보다 크지 않아야합니까?
— Bruce Hamilton

@BruceHamilton 불행히도 문서에 따르면 부품이 대략 같을 수는 없습니다 . 그들은 해야한다 동일 :if this Spliterator is SUBSIZED, then estimateSize() for this spliterator before splitting must be equal to the sum of estimateSize() for this and the returned Spliterator after splitting.
— 맥아

예, 그것은 Spliterator 분할에 대한 나의 이해와 일치합니다. 그러나 "trySplit에서 반환 된 분할이 분할 이전보다 더 많은 항목을 가질 수 있음"을 이해하는 데 어려움을 겪고 있습니다. 그 의미에 대해 자세히 설명해 주시겠습니까?
— Bruce Hamilton

13

우리는 해결해야 할 비슷한 문제가있었습니다. 우리는 시스템 메모리보다 큰 스트림 (데이터베이스의 모든 개체를 반복)을 가져 와서 가능한 한 최선의 순서를 무작위로 지정하려고했습니다. 10,000 개의 항목을 버퍼링하고 무작위로 지정하는 것이 좋습니다.

대상은 스트림을받는 함수였습니다.

여기에 제안 된 솔루션 중 다양한 옵션이있는 것 같습니다.

  • 다양한 비 Java 8 추가 라이브러리 사용
  • 스트림이 아닌 것으로 시작-예 : 임의 액세스 목록
  • 분할기에서 쉽게 분할 할 수있는 스트림이 있습니다.

우리의 본능은 원래 커스텀 컬렉터를 사용하는 것이었지만 이것은 스트리밍을 중단하는 것을 의미했습니다. 위의 사용자 지정 수집기 솔루션은 매우 훌륭하며 거의 사용했습니다.

다음은 Streams가 탈출구Iterator 로 사용할 수 있다는 사실을 사용하여 속임수를 쓰는 솔루션입니다 . 는 자바 8의 또 다른 비트를 사용하여 스트림으로 변환 돌아 마법을.IteratorStreamSupport

/**
 * An iterator which returns batches of items taken from another iterator
 */
public class BatchingIterator<T> implements Iterator<List<T>> {
    /**
     * Given a stream, convert it to a stream of batches no greater than the
     * batchSize.
     * @param originalStream to convert
     * @param batchSize maximum size of a batch
     * @param <T> type of items in the stream
     * @return a stream of batches taken sequentially from the original stream
     */
    public static <T> Stream<List<T>> batchedStreamOf(Stream<T> originalStream, int batchSize) {
        return asStream(new BatchingIterator<>(originalStream.iterator(), batchSize));
    }

    private static <T> Stream<T> asStream(Iterator<T> iterator) {
        return StreamSupport.stream(
            Spliterators.spliteratorUnknownSize(iterator,ORDERED),
            false);
    }

    private int batchSize;
    private List<T> currentBatch;
    private Iterator<T> sourceIterator;

    public BatchingIterator(Iterator<T> sourceIterator, int batchSize) {
        this.batchSize = batchSize;
        this.sourceIterator = sourceIterator;
    }

    @Override
    public boolean hasNext() {
        prepareNextBatch();
        return currentBatch!=null && !currentBatch.isEmpty();
    }

    @Override
    public List<T> next() {
        return currentBatch;
    }

    private void prepareNextBatch() {
        currentBatch = new ArrayList<>(batchSize);
        while (sourceIterator.hasNext() && currentBatch.size() < batchSize) {
            currentBatch.add(sourceIterator.next());
        }
    }
}

이것을 사용하는 간단한 예는 다음과 같습니다.

@Test
public void getsBatches() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        .forEach(System.out::println);
}

위의 인쇄

[A, B, C]
[D, E, F]

사용 사례에서는 배치를 섞은 다음 스트림으로 유지하려고했습니다. 다음과 같이 보입니다.

@Test
public void howScramblingCouldBeDone() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        // the lambda in the map expression sucks a bit because Collections.shuffle acts on the list, rather than returning a shuffled one
        .map(list -> {
            Collections.shuffle(list); return list; })
        .flatMap(List::stream)
        .forEach(System.out::println);
}

이것은 다음과 같은 것을 출력합니다 (무작위 화되어 매번 다릅니다)

A
C
B
E
D
F

여기서 비밀 소스는 항상 스트림이 있기 때문에 배치 스트림에서 작업하거나 각 배치에 대해 작업을 수행 한 다음 flatMap스트림으로 다시 돌아갈 수 있다는 것입니다. 더 나은, 위의 유일한 모두 최종으로 실행 forEach하거나 collect또는 다른 종단 표현 PULL 스트림을 통해 데이터를.

그것은 스트림에 iterator대한 특수한 유형의 종료 작업 이며 전체 스트림이 실행되고 메모리에 들어오지 않게하는 것으로 밝혀졌습니다 ! 멋진 디자인에 대한 Java 8 녀석들에게 감사합니다!


그리고 수집 될 때 각 배치에 대해 완전히 반복하는 것이 매우 좋습니다 List. 소비자가 전체 배치를 건너 뛰기를 원할 수 있으므로 배치 내 요소의 반복을 연기 할 수 없습니다. 요소는 멀리 건너 뛰지 않을 것입니다. (이 중 하나를 C #으로 구현했지만 훨씬 쉬웠습니다.)
— ErikE

9

RxJava를 사용할 수도 있습니다 .

Observable.from(data).buffer(BATCH_SIZE).forEach((batch) -> process(batch));

또는

Observable.from(lazyFileStream).buffer(500).map((batch) -> process(batch)).toList();

또는

Observable.from(lazyFileStream).buffer(500).map(MyClass::process).toList();

8

cyclops-react를 살펴볼 수도 있습니다. 저는이 라이브러리의 저자입니다. jOOλ 인터페이스 (및 확장 JDK 8 스트림)를 구현하지만 JDK 8 병렬 스트림과 달리 비동기 작업 (예 : 잠재적으로 비동기 I / O 호출 차단)에 중점을 둡니다. 반면 JDK 병렬 스트림은 CPU 바운드 작업에 대한 데이터 병렬 처리에 중점을 둡니다. 내부에서 Future 기반 작업의 집계를 관리하여 작동하지만 최종 사용자에게 표준 확장 스트림 API를 제공합니다.

이 샘플 코드는 시작하는 데 도움이 될 수 있습니다.

LazyFutureStream.parallelCommonBuilder()
                .react(data)
                .grouped(BATCH_SIZE)                  
                .map(this::process)
                .run();

여기에 배치에 대한 튜토리얼이 있습니다.

그리고 여기에 더 일반적인 튜토리얼

자신의 스레드 풀 (I / O 차단에 더 적합 할 수 있음)을 사용하려면 다음을 사용하여 처리를 시작할 수 있습니다.

     LazyReact reactor = new LazyReact(40);

     reactor.react(data)
            .grouped(BATCH_SIZE)                  
            .map(this::process)
            .run();

3

병렬 스트림에서도 작동하는 순수 Java 8 예제입니다.

사용하는 방법:

Stream<Integer> integerStream = IntStream.range(0, 45).parallel().boxed();
CsStreamUtil.processInBatch(integerStream, 10, batch -> System.out.println("Batch: " + batch));

메서드 선언 및 구현 :

public static <ElementType> void processInBatch(Stream<ElementType> stream, int batchSize, Consumer<Collection<ElementType>> batchProcessor)
{
    List<ElementType> newBatch = new ArrayList<>(batchSize);

    stream.forEach(element -> {
        List<ElementType> fullBatch;

        synchronized (newBatch)
        {
            if (newBatch.size() < batchSize)
            {
                newBatch.add(element);
                return;
            }
            else
            {
                fullBatch = new ArrayList<>(newBatch);
                newBatch.clear();
                newBatch.add(element);
            }
        }

        batchProcessor.accept(fullBatch);
    });

    if (newBatch.size() > 0)
        batchProcessor.accept(new ArrayList<>(newBatch));
}

2

공정하게 말해서 우아한 Vavr 솔루션을 살펴보십시오 .

Stream.ofAll(data).grouped(BATCH_SIZE).forEach(this::process);

1

Spliterator를 사용한 간단한 예

    // read file into stream, try-with-resources
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        //skip header
        Spliterator<String> split = stream.skip(1).spliterator();
        Chunker<String> chunker = new Chunker<String>();
        while(true) {              
            boolean more = split.tryAdvance(chunker::doSomething);
            if (!more) {
                break;
            }
        }           
    } catch (IOException e) {
        e.printStackTrace();
    }
}

static class Chunker<T> {
    int ct = 0;
    public void doSomething(T line) {
        System.out.println(ct++ + " " + line.toString());
        if (ct % 100 == 0) {
            System.out.println("====================chunk=====================");               
        }           
    }       
}

Bruce의 대답은 더 포괄적이지만 많은 파일을 처리하기 위해 빠르고 더러운 것을 찾고있었습니다.


1

이것은 느리게 평가되는 순수한 자바 솔루션입니다.

public static <T> Stream<List<T>> partition(Stream<T> stream, int batchSize){
    List<List<T>> currentBatch = new ArrayList<List<T>>(); //just to make it mutable 
    currentBatch.add(new ArrayList<T>(batchSize));
    return Stream.concat(stream
      .sequential()                   
      .map(new Function<T, List<T>>(){
          public List<T> apply(T t){
              currentBatch.get(0).add(t);
              return currentBatch.get(0).size() == batchSize ? currentBatch.set(0,new ArrayList<>(batchSize)): null;
            }
      }), Stream.generate(()->currentBatch.get(0).isEmpty()?null:currentBatch.get(0))
                .limit(1)
    ).filter(Objects::nonNull);
}

1

apache.commons를 사용할 수 있습니다.

ListUtils.partition(ListOfLines, 500).stream()
                .map(partition -> processBatch(partition)
                .collect(Collectors.toList());

분할 부분은 느리게 수행되지만 목록이 분할 된 후에는 스트림 작업의 이점을 얻을 수 있습니다 (예 : 병렬 스트림 사용, 필터 추가 등). 다른 답변은 더 정교한 솔루션을 제안했지만 때로는 가독성과 유지 관리가 더 중요합니다 (때로는 그렇지 않습니다 :-))


확실하지을 downvoted하지만 구아바 사용할 수없는 사람들을위한 다른 답변 보충 대답했다 .. 왜 이해하는 것이 좋을 것이다 사람
— 탈 조페

스트림이 아닌 여기에서 목록을 처리하고 있습니다.
— Drakemor

@Drakemor 하위 목록 스트림을 처리하고 있습니다. 스트림 () 함수 호출 알
— 탈 조페

그러나 먼저 실제 스트리밍 된 데이터에 대해 제대로 작동하지 않는 하위 목록 목록으로 변환합니다 . 다음은 파티션에 대한 참조는 다음과 같습니다 commons.apache.org/proper/commons-collections/apidocs/org/...
— Drakemor

1
TBH 나는 당신의 주장을 완전히 이해하지는 못하지만 우리는 동의하지 않을 수 있다고 생각합니다. 여기에 대화를 반영하기 위해 내 대답을 편집했습니다. 토론 주셔서 감사합니다
— 탈 조페

1

Reactor를 사용하면 쉽게 할 수 있습니다 .

Flux.fromStream(fileReader.lines().onClose(() -> safeClose(fileReader)))
            .map(line -> someProcessingOfSingleLine(line))
            .buffer(BUFFER_SIZE)
            .subscribe(apiService::makeHttpRequest);

0

로 Java 8하고 com.google.common.collect.Lists, 당신이 뭔가를 같이 할 수 있습니다 :

public class BatchProcessingUtil {
    public static <T,U> List<U> process(List<T> data, int batchSize, Function<List<T>, List<U>> processFunction) {
        List<List<T>> batches = Lists.partition(data, batchSize);
        return batches.stream()
                .map(processFunction) // Send each batch to the process function
                .flatMap(Collection::stream) // flat results to gather them in 1 stream
                .collect(Collectors.toList());
    }
}

여기 T에는 입력 목록 U의 항목 유형과 출력 목록의 항목 유형이 있습니다.

그리고 다음과 같이 사용할 수 있습니다.

List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
    userKeys,
    10, // Batch Size
    partialKeys -> service.getUsers(partialKeys)
);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.