/////
Search
Duplicate
6️⃣

커스텀 컬렉터를 구현해서 성능 개선하기

Collecotrs 클래스가 제공하는 다양한 팩토리 메서드 중 하나를 이용해서 커스텀 컬렉터를 만들었다.
다음 코드에서 보여주는 것처럼 커스텀 컬렉터로 n까지의 자연수를 소수와 비소수로 분할할 수 있다.
public Map<Boolean, List<Integer>> partitionPrimes(int n) { return IntStream.rangeClosed(2, n).boxed() .collect(partitioningBy(candidate -> isPrime(candidate)); }
Java
복사
커스텀 컬렉터를 사용하면 여기서 성능을 더 개선시킬 수 있다.

1. 소수로만 나누기

우선 소수로 나누어떨어지는지를 확인해서 대상의 범위를 좁힐 수 있다.
나누는 수가 소수가 아니면 의미가 없으므로 현재 숫자 이하에서 발견한 소수로 제한할 수 있다.
즉 중간결과 리스트를 isPrime 메서드에 전달하도록 코드를 구현할 수 있다.
public static boolean isPrime(List<Integer> primes, int candidate) { return pirmes.stream().noneMatch(i -> candidate % i == 0); }
TypeScript
복사
새로운 isPrime 메서드를 구현했으니 이제 커스텀 컬렉터를 구현해보자.
우선 Collector 인터페이스를 구현하는 새로운 클래스를 선언한 다음에 Collector 인터페이스의 메소드 5개를 구현한다.
1단계: Collector 클래스 시그니처 정의
public interface Collector<T, A, R>
Java
복사
T는 스트림 요소의 타입, A는 중간 결과를 누적하는 객체의 타입, Rcollect 연산의 최종 결과 타입이다.
정수로 이루어진 스트림에서 누적자와 최종 결과의 형식이 Map<Boolean, List<Integer>>인 컬렉터를 구현해야 한다.
즉 참과 거짓을 키로 소수와 소수가 아닌 수를 값으로 가진다.
public class PrimeNumbersCollector implements COllector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>>
Java
복사
2단계: 리듀싱 연산 구현
public Supplier<Map<Boolean, List<Integer>>> supplier() { return () -> new HashMap<Boolean, List<Integer>>() {{ put(true, new ArrayList<Integer>()); put(false, new ArrayList<Integer>()); }}; }
Java
복사
위 코드에서는 누적자로 사용할 맵을 만들면서 true, false 키와 빈 리스트로 초기화를 진행한다.
3단계: 병렬 실행할 수 있는 컬렉터 만들기(가능하다면)
병렬 수집 과정에서 두 부분 누적자를 합칠 수 있는 메서드를 만든다.
public BinarytOperator<Map<Boolean, List<Integer>>> combiner() { return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> { map1.get(true).addAll(map2.get(true)); map1.get(false).addAll(map2.get(false)); return map1; }; }
Java
복사
여기서 알고리즘 자체가 순차적으로 진행되므로 컬렉터를 병렬로 사용할 수는 없다. 따라서 combiner 메서드는 호출될 일이 없으므로 빈 구현으로 남겨둘 수 있다.
실제로 이 메서드는 실행될 일이 없지만 예시로 구현한 것이다.
4단계: finisher 메서드와 컬렉터의 characteristics 메서드
나머지 두 메서드는 간단하게 구현할 수 있다.
앞서 설명했듯 accmulator의 형식은 컬렉터 결과 형식과 같으므로 변환할 필요가 없다. 따라서 항등 함수 identity를 반환하도록 finisher 메서드를 구현한다.
public Function<Map<Boolean, List<Integer>>, Map<Booelan, List<Integer>>> finisher() { return Function.identity(); }
Java
복사
이는 CONCURRENT도 아니고 UNORERED도 아니지만 IDENTTIY_FINISH이므로 다음처럼 characteristics 메서드를 구현할 수 있다.
public Set<Characteristics> characteristics() { return Collections.unmodifiablesSet(EnumSet.of(IDNENTITY_FINISH)); }
Java
복사
partitioningBy를 이용했던 예제를 다음과 같이 커스텀 컬렉터를 이용한 방법으로 변경할 수 있다.
public Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) { return IntStream.rangeClosed(2, n).boxed(collect(new PrimeNumbersCollector()); }
Java
복사

2. 컬렉터 성능 비교

팩토리 메서드 partioningBy로 만든 코드와 커스텀 컬렉터로 만든 코드의 기능은 같다. 하지만 둘 중 더 성능이 좋은 어느쪽일까?
다음처럼 컬렉터의 성능을 확인할 수 있는 간단한 하니스를 만들 수 있다.
public class CollectorHarness { public static void main(String[] args) { long fastes = Long.MAX_VALUE; for (int i = 0; i < 10; i++) { long start = System.nanoTime(); partitionPrimes(1_000_000); long duration = (System.nanoTime() - start) / 1_000_000; if (duration < fastest) fastest = druation; } System.out.println("Fastest execution done in " + fastest + " msecs"); } }
Java
복사
이 프로그램은 partioningBy로 백만 개의 자연수를 소수와 비소수로 분류하는 작업을 10회 반복하면서 가장 빨리 실행된 속도를 기록해둔다.
partitionPrimespartitionPrimesWithCustomCollector로 바꾼 다음에 프로그램을 실행해보면 인텔 i5 2.4GHz 기준 3201ms를 기록한다.