병렬 데이터 처리와 성능
자바 7은 쉽게 병렬화를 수행하면서 에러를 최소화할 수 있도록 fork/join 프레임워크 기능을 제공한다.
병렬 스트림
컬렉션에 parallelStream을 호출하면 병렬 스트림이 생성된다. 병렬 스트림은 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다. 따라서 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.
stream에서 parallel을 호출하면 내부적으로 이후 연산이 병렬로 수행한다. 반대로 sequential로 병렬 스트림을 순차 스트림으로 바꿀 수 있다. 이 두 메서드를 이용해서 병렬로 실행할지 순차로 실행할지 제어할 수 있다. 이 중 최종적으로 호출된 메서드가 전체 파이프라인에 영향을 미친다.
병렬 스트림 효과적으로 사용하기
- 직접 성능을 측정하는 것이 가장 정확하다.
- 박싱을 주의하라. 되도록 기본형 특화 스트림을 사용하자.
- 병렬 스트림에서 성능이 떨어지는 연산들을 피하라. limit나 findFirst 처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하려면 비싼 비용을 치러야한다.
- 소량의 데이터에서는 병렬 스트림이 도움되지 않는다. 병렬화 과정에서 생기는 오버헤드 비용을 상쇄할 수 있을만큼의 이득을 얻지 못하기 때문이다.
- 최종 연산의 병합 비용을 살펴보라.
포크/조인 프레임워크
작업을 분할(fork)하고 결과를 합치는(join) 방식으로 여러 작업을 동시에 처리하는 도구
- RecursiveTask<R>: 결과값이 있는 작업. R은 결과 타입
- RecursiveAction: 결과값이 없는 작업.
- compute() 메서드: 작업을 작게 나누거나 결과를 계산하는 메서드. RecursiveTask를 이용하기 위해서는 compute 메서드를 구현해야 한다.
if (작업이 충분히 작으면) { 작업을 직접 처리 (순차적 계산) } else { 작업을 둘로 나눔 (Fork) 나뉜 작업을 각각 재귀적으로 호출 결과를 합침 (Join) }
ForkJoinPool
ForkJoinTask를 실행하기 위한 스레드 풀이다. Task를 생성하면 생성한 ForkJoinTask를 풀의 invoke 메서드로 전달한다. 풀은 정적 필드에 싱글턴으로 저장한다.
포크/조인 프레임워크를 제대로 사용하는 방법
- join 메서드를 테스크에서 호출하면 서브테스크가 생산하는 결과가 준비될 때까지 호출자를 블록시킨다. 그러므로 두 서브테스크를 시작한 다음에 호출해야 효율적이다.
- 서브테스크 양쪽에 fork 메서드를 사용하는 대신 한쪽 작업에 compute를 호출하자. compute를 처리한 쪽은 같은 스레드를 재사용할 수 있으므로 포크/조인 풀에서 불필요한 테스크를 할당하는 오버헤드를 줄일 수 있다.
- 너무 작은 작업은 오히려 오버헤드가 커진다. 작업은 적절히 쪼개야 효율적이다.
작업 훔치기
포크/조인 프레임워크에서는 작업이 끝날 때마다 다른 스레드의 큐 꼬리에서 테스크를 가져와서 작업을 훔쳐온다. 모든 테스크가 작업을 끝낼때까지 이 과정을 반복한다. 따라서 테스크의 크기를 작게 나누어야 스레드 간의 작업 부하를 비슷한 수준으로 유지할 수 있다.
Spliterator
컬렉션 데이터를 잘게 쪼개서 병렬 처리할 수 있도록 돕는 인터페이스
- tryAdvance: Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소가 남아있으면 참을 반환한다.
- boolean tryAdvance(Consumer<? super T> action);
- trySplit: Spliterator의 일부 요소를 분할해서 두 번째 Spliterator를 생성하는 메서드
- Spliterator<T> trySplit();
- estimateSize: 탐색해야할 요소 수 정보 제공
- long estimateSize();
- characteristics: Spliter의 특성을 의미
- int characteristics();
컬렉션 API 개선
컬렉션 팩토리
기존에 Arrays.asList 나 Stream.of().collect(toSet())과 같은 방식으로 변환할 수 있는 집합을 만들 수 밖에 없었다.
자바 9에서는 불변 객체를 만들 수 있는 팩토리 메서드가 제공된다.
- List.of: 변경할 수 없는 불변 리스트를 만든다.
- Set.of: 변경할 수 없는 불변 집합을 만든다. 중복된 요소가 존재할 시 IllegalArgumentException이 발생한다.
- Map.of: 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있다.
- Map.ofEntries: Map.Entry<K,V> 객체를 인수로 받아 맵을 만들 수 있다.
List, Set에 추가된 메서드
removeIf
Predicate를 만족하는 요소를 제거한다.
replaceAll
UnaryOperator 함수를 이용해 요소를 바꾼다.
sort
List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
Map에 추가된 메서드
forEach
맵에서 키와 값을 반복할 수 있으며, BiConsumer를 인수로 받는 메서드를 지원한다.
정렬 메서드
값 또는 키를 기준으로 정렬할 수 있다. 정렬 메서드를 인자로 전달한다.
- Entry.comparingByValue
- Entry.comparingByKey
getOrDefault
찾으려는 키가 존재하지 않으면 Null이 반환되므로 NPE 방지를 위해 요청 결과가 Null인지 확인하는 메서드.
계산패턴
- computeIfAbsent: 제공된 키에 해당하는 값이 없거나 null이라면 키를 이용해 새로운 값을 계산하고 맵에 추가한다.
- computeIfPresent: 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다. 계산한 값이 null이라면 존재하던 키 또한 제거한다.
- compute: 제공된 키로 새 값을 계산하고 맵에 저장한다.
삭제패턴
제공된 키에 해당하는 맵 항목을 제거하는 기존의 remove 메서드와 더불어 키가 특정한 값에 연관되어 있을 때만 항목을 제거하는 오버로드된 메서드를 제공한다.
default boolean remove(Object key, Object value)
교체패턴
맵의 항목을 바꾸는데 사용할 수 있는 메서드
- replaceAll: BiFunction을 적용한 결과로 각 항목의 값을 교체한다. List의 replaceAll과 비슷한 동작을 수행한다.
- replace: 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 존재한다.
합침
두 개의 맵을 합칠 때 putAll 메서드를 사용했는데 중복된 키가 있다면 원하는 동작이 이루어지지 못할 수 있다.
새로 제공되는 merge 메서드는 중복된 키에 대한 동작(BiFunction)을 정의해줄 수 있다.
ConcurrentHashMap
리듀스와 검색
- foreach: 각 쌍에 주어진 액션을 수행
- reduce: 모든 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
- search: null이 아닌 값을 반환할 때까지 각 쌍에 함수를 적용
또한 연산에 병렬성 기준값(threshold)를 정해야 한다. 맵의 크기가 기준값보다 작으면 순차적으로 연산을 진행한다. 기준값을 1로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화할 수 있다.
계수
맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. 기존에 제공되던 size 함수는 int형으로 반환하지만 long 형으로 반환하는 mappingCount를 사용할 때 매핑의 개수가 int의 범위를 넘어서는 상황에 대하여 대처할 수 있다.
집합뷰
ConcurrentHashMap을 집합 뷰로 반환하는 keySet 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. newKeySet이라는 메서드를 통해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.
'Java' 카테고리의 다른 글
모던 자바 인 액션 - chapter 6 스트림 데이터 수집 (1) | 2024.11.29 |
---|---|
모던 자바 인 액션 - chapter 4, 5 스트림과 활용 (1) | 2024.11.20 |
모던 자바 인 액션 - chapter 2, 3 동작 파라미터화, 람다 표현식 (1) | 2024.11.19 |
모던 자바 인 액션 - chapter 1 자바 8 무슨 일이 일어나고 있는가 (0) | 2024.11.19 |
스트림(2) - Stream API의 다양한 연산 (0) | 2024.10.26 |