일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch1
- 자바로 키오스크 만들기
- 스프링 mvc1 - 스프링 mvc
- 자바로 계산기 만들기
- 자바의 정석 기초편 ch11
- 람다
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch4
- 자바 고급2편 - 네트워크 프로그램
- 자바 중급1편 - 날짜와 시간
- @Aspect
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch2
- 자바 고급2편 - io
- 스프링 입문(무료)
- 스프링 mvc2 - 검증
- 스프링 트랜잭션
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch14
- 데이터 접근 기술
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch7
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch5
- 2024 정보처리기사 시나공 필기
- 자바 기초
- 자바 중급2편 - 컬렉션 프레임워크
- Today
- Total
개발공부기록
Chapter 17 - 스트림 요소 처리2(Stream), 요소 정렬, 요소를 하나씩 처리(루핑), 요소 조건 만족 여부(매칭), 요소 기본 집계, 요소 커스텀 집계, 요소 수집, 요소 병렬 처리 본문
Chapter 17 - 스트림 요소 처리2(Stream), 요소 정렬, 요소를 하나씩 처리(루핑), 요소 조건 만족 여부(매칭), 요소 기본 집계, 요소 커스텀 집계, 요소 수집, 요소 병렬 처리
소소한나구리 2025. 3. 14. 13:42요소 정렬
요소를 오름차순 또는 내림차순으로 정렬하는 중간 처리 기능도 제공한다
리턴 타입 | 메서드(매개변수) | 설명 |
Stream<T> | sorted() | Comparable 요소를 정렬한 새로운 스트림 생성 |
Stream<T> | sorted(Comparator<T>) | 요소를 Comparator에 따라 정렬한 새 스트림 생성 |
DoubleStream | sorted() | double 요소를 오름차순으로 정렬 |
IntStream | sorted() | int 요소를 오름차순으로 정렬 |
LongStream | sorted() | long 요소를 오름차순으로 정렬 |
Comparable 구현 객체의 정렬
스트림의 요소가 객체일 경우 객체가 Comparable을 구현하고 있어야만 sorted() 메서드를 사용하여 정렬할 수 있으며 그렇지 않으면 ClassCastException이 발생함
만약 내림차순으로 정렬하고 싶다면 Comparator.reverseOrder()메서드가 리턴하는 Comparator를 매개값으로 제공하면 됨
Student
- 정렬을 위해 Comparable을 구현하고 있음
public class Student implements Comparable<Student>{
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
@Override
public int compareTo(Student o) {
// score와 o.score가 같을 경우 0을 리턴, 작을 경우 음수 리턴, 클 경우 양수 리턴
return Integer.compare(score, o.score);
}
}
SortingExample
public class SortingExample {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("김학생", 50));
studentList.add(new Student("학생김", 70));
studentList.add(new Student("생김학", 20));
// 점수를 기준으로 오름차순으로 정렬한 새 스트림 얻기
studentList.stream()
.sorted()
.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
System.out.println();
// 점수를 기준으로 내림차순으로 정렬한 새 스트림 얻기
studentList.stream()
.sorted(Comparator.reverseOrder())
.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
}
}
/* 실행 결과
생김학: 20
김학생: 50
학생김: 70
학생김: 70
김학생: 50
생김학: 20
*/
Comparator를 이용한 정렬
요소 객체가 Comparable을 구현하고 있지 않다면 비교자를 제공하여 요소를 정렬시킬 수 있음
람다식으로 작성할 수 있으며 중괄호 안에는 o1이 o2보다 작으면 음수, 같으면 0, 크면 양수를 리턴하도록 작성하면 됨
정수일 때는 Integer.compare(o1, o2)를 실수일 때는 Double.compare(o1, o2)를 호출해서 리턴값을 리턴해도 좋음
sorted((o1, o2) -> { ... }
Student
- 비교자가 없는 일반 객체임
public class Student {
private String name;
private int score;
// 생성자, 게터
}
SortingExample
public class SortingExample {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("삐약이", 30));
studentList.add(new Student("비삼이", 20));
studentList.add(new Student("조약이", 60));
// 점수를 기준으로 오름차순으로 정렬한 새 스트림 얻기
studentList.stream()
.sorted((s1, s2) -> Integer.compare(s1.getScore(), s2.getScore()))
.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
System.out.println();
// 점수를 기준으로 내림차순으로 정렬한 새 스트림 얻기
studentList.stream()
.sorted((s1, s2) -> Integer.compare(s2.getScore(), s1.getScore()))
.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
}
}
/* 실행 결과
비삼이: 20
삐약이: 30
조약이: 60
조약이: 60
삐약이: 30
비삼이: 20
*/
요소를 하나씩 처리(루핑)
루핑(looping)은 스트림에서 요소를 하나씩 반복해서 가져와 처리하는 것을 말하며 루핑 메서드에는 peek()과 forEach()가 있음
리턴 타입 | 메서드(매개변수) | 설명 |
Stream<T> IntStream DoubleStream |
peek(Consumer<? super T>) | T 반복 |
peek(IntConsumer action) | int 반복 | |
peek(DoubleConsumer action) | double 반복 | |
void | forEach(Consumer<? super T> action) | T 반복 |
forEach(IntConsumer action) | int 반복 | |
forEach(DoubleConsumer action) | double 반복 |
peek()과 forEach()는 동일하게 요소를 루핑하지만 peek()은 중간 처리 메서드이고 forEach()는 최종 처리 메서드이므로 peek()은 최종 처리가 뒤에 붙지 않으면 동작하지 않음
매개타입인 Consumer는 함수형 인터페이스로 다음과 같은 종류가 있음
인터페이스명 | 추상 메서드 | 설명 |
Consumer<T> | void accept(T t) | 매개값 T를 받아 소비 |
IntConsumer | void accept(int value) | 매개값 int를 받아 소비 |
LongConsumer | void accept(long value) | 매개값 long을 받아 소비 |
DoubleConsumer | void accept(double value) | 매개값 double 받아 소비 |
LoopingExample
- peek()을 사용시 최종 처리가 없으면 동작하지 않음
public class LoopingExample {
public static void main(String[] args) {
int[] intArr = {1, 2, 3, 4, 5};
// 잘못 작성한 경우
Arrays.stream(intArr)
.filter(a -> a % 2 == 0)
.peek(n -> System.out.println(n)); // 최종 처리가 없으므로 동작하지 않음
// 중간 처리 메서드 peek()을 이용해서 반복 처리
int total = Arrays.stream(intArr)
.filter(a -> a % 2 == 0)
.peek(n -> System.out.println(n))
.sum();
System.out.println("total = " + total);
System.out.println();
// 최종 처리 메서드 forEach()를 이용하여 반복 처리
Arrays.stream(intArr)
.filter(a -> a % 2 == 0)
.forEach(n -> System.out.println(n));
}
}
/* 실행 결과
2
4
total = 6
2
4
*/
요소 조건 만족 여부(매칭)
매칭은 특정 조건에 만족하는지 여부를 조사하는 최종 처리 기능임
리턴 타입 | 메서드(매개변수) | 설명 |
boolean | allMatch(Predicate<T> predicate) allMatch(IntPredicate predicate) allMatch(LongPredicate predicate) allMatch(DoublePredicate predicate) |
모든 요소가 만족하는지 여부 |
boolean | anyMatch(Predicate predicate) anyMatch(IntPredicate predicate) anyMatch(LongPredicate predicate) anyMatch(DoublePredicate predicate) |
최소한 하나의 요소가 만족하는지 여부 |
boolean | noneMatch(Predicate predicate) noneMatch(IntPredicate predicate) noneMatch(LongPredicate predicate) noneMatch(DoublePredicate predicate) |
모든 요소가 만족하지 않는지 여부 |
allMatch(), anyMatch(), noneMatch() 메서드는 매개값으로 주어진 Predicate가 리턴하는 값에 따라 true또는 false를 리턴함
예를 들어 allMatch()는 모든 요소의 Predicate가 true를 리턴해야만 true를 리턴함
MatchingExample
- 정수 스트림에서 모든 요소가 2의 배수인지 하나라도 3의 배수가 존재하는지, 모든 요소가 3의 배수가 아닌지를 조사하는 예제
public class MatchingExample {
public static void main(String[] args) {
int[] intArr = {2, 4, 6};
boolean result = Arrays.stream(intArr)
.allMatch(a -> a % 2 == 0);
System.out.println("모두 2의 배수인가요? " + result);
result = Arrays.stream(intArr)
.anyMatch(a -> a % 3 == 0);
System.out.println("하나라도 3의 배수인가요? " + result);
result = Arrays.stream(intArr)
.noneMatch(a -> a % 3 == 0);
System.out.println("3의 배수가 없나요? " + result);
}
}
/* 실행 결과
모두 2의 배수인가요? true
하나라도 3의 배수인가요? true
3의 배수가 없나요? false
*/
요소 기본 집계
집계(Aggregate)는 최종 처리 기능으로 요소들을 처리하여 카운팅, 합계, 평균값, 최대값, 최소값등과 같이 하나의 값으로 산출하는 것을 말함
즉, 대량의 데이터를 가공해서 하나의 값으로 축소하는 리덕션(Reduction)이라고 볼 수 있음
리턴 타입 | 메서드(매개변수) | 설명 |
long | count() | 요소 개수 |
OptionalXxx | findFirst() | 첫 번째 요소 |
Optional<T> OptionalXxx |
max(Comparator<T>) max() |
최대 요소 |
Optional<T> OptionalXxx |
min(Comparator<T>) min() |
최소 요소 |
OptionalDouble | average() | 요소 평균 |
int, long, double | sum() | 요소 총합 |
집계 메서드가 리턴하는 OptionalXxx는 Optional, OptionalDouble, OptionalInt, OptionalLong 클래스를 말함
이 클래스들은 최종값을 저장하는 객체로 get(), getAsDouble(), getAsInt(), getAsLong()을 호출하면 최종값을 얻을 수 있음
AggregateExample
public class AggregateExample {
public static void main(String[] args) {
// 정수 배열
int[] arr = {1, 2, 3, 4, 5};
// 카운팅
long count = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.count();
System.out.println("2의 배수의 개수는? " + count);
// 총합
int sum = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.sum();
System.out.println("2의 배수의 합은? " + sum);
// 평균
double avg = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.average()
.getAsDouble();
System.out.println("2의 배수의 평균은? " + avg);
// 최대값
int max = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.max()
.getAsInt();
System.out.println("최대값: " + max);
// 최소값
int min = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.min()
.getAsInt();
System.out.println("최소값: " + min);
// 첫 번째 요소
int first = Arrays.stream(arr)
.filter(n -> n % 3 == 0)
.findFirst()
.getAsInt();
System.out.println("첫 번째 3의 배수: " + first);
}
}
/* 실행 결과
2의 배수의 개수는? 2
2의 배수의 합은? 6
2의 배수의 평균은? 3.0
최대값: 4
최소값: 2
첫 번째 3의 배수: 3
*/
Optional 클래스
Optional, OptionalDouble, OptionalInt, OptionalLong 클래스는 단순히 집계값만 저장하는 것이 아니라 집계값이 존재하지 않은 경우 디폴트 값을 설정하거나 집계값을 처리하는 Consumer를 등록할 수 있음
Optional 클래스가 제공하는 메서드
리턴 타입 | 메서드(매개변수) | 설명 |
boolean | isPresent() | 집계값이 있는지 여부 |
T double int long |
orElse(T) orElse(double) orElse(int) orElse(long) |
집계값이 없을 경우 디폴트 값 설정 |
void | ifPresent(Consumer) ifPresent(DoubleConsumer) ifPresent(IntConsumer) ifPresent(LongConsumer) |
집계값이 있을 경우 Consumer에서 처리 |
컬렉션의 요소는 동적으로 추가되는 경우가 많은데 만약 컬렉션에 요소가 존재하지 않으면 집계 값을 산출할 수 없으므로 NoSuchElementException 예외가 발생할 수 있는데, 위의 표에 언급된 메서드들을 이용하면 예외 발생을 막을 수 있음
OptionalExample
- 1. isPresent() 메서드가 true를 리턴할 때만 집계값을 얻기
- 2. orElse() 메서드로 집계값이 없을 경우를 대비해서 디폴트 값을 정해놓기
- 3. ifPresent() 메서드로 집계값이 없을 경우에 동작하는 Consumer 람다식을 제공
public class OptionalExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
/*
// NoSuchElementException 예외 발생
double avg = list.stream()
.mapToInt(Integer::intValue)
.average()
.getAsDouble();
*/
// 방법 1 -> 반환된 Optional을 isPresent()로 검증하여 true인 경우에만 동작하도록 작성
OptionalDouble optAvg = list.stream()
.mapToInt(Integer::intValue)
.average();
if (optAvg.isPresent()) {
System.out.println("방법1_평균: " + optAvg.getAsDouble());
} else {
System.out.println("방법1_평균: 0.0");
}
// 방법2 -> orElse(): 집계값이 없을 경우 동작할 디폴트 값을 지정
double avg = list.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println("방법2_평균: " + avg);
// 방법3 -> ifPresent(): 집계값이 있을 경우에만 동작하는 람다식을 제공
list.stream()
.mapToInt(Integer::intValue)
.average()
.ifPresent(a -> System.out.println("방법3_평균: " + a));
}
}
/* 실행결과
방법1_평균: 0.0
방법2_평균: 0.0
*/
요소 커스텀 집계
스트림은 기본 집계 메서드인 sum(), average(), count(), max(), min()을 제공하지만 다양한 집계 결과물을 만들 수 있도록 reduce()메서드를 제공함
인터페이스 | 리턴 타입 | 메서드(매개변수) |
Stream | Optional<T> | reduce(BinaryOperator<T> accumulator) |
T | reduce(T identity, BinaryOperator<T> accumulator) | |
IntStream | OptionalInt | reduce(IntBinaryOperator op) |
int | reduce(int identity, IntBinaryOperator op) | |
LongStream | OptionalLong | reduce(LongBinaryOperator op) |
long | reduce(long identity, LongBinaryOperator op) | |
DoubleStream | OptionalDouble | reduce(DoubleBinaryOperator op) |
double | reduce(double identity, DoubleBinaryOperator op) |
매개값인 BinaryOperator는 함수형 인터페이스이며 BinaryOperator는 두 개의 매개값을 받아 하나의 값을 리턴하는 apply()를 가지고 있음
reduce()는 스트림에 요소가 없을 경우 NoSuchElementException 예외가 발생하지만 identity 매개값이 주어지면 이 값을 디폴트 값으로 리턴함
Student
public class Student {
private String name;
private int score;
// 생성자, 게터
}
ReductionExample
- 기본 집계 메서드인 sum() 동일한 결과를 산출하는 reduce() 메서드 사용 방법
public class ReductionExample {
public static void main(String[] args) {
List<Student> studentList = Arrays.asList(
new Student("조학생", 80),
new Student("박학생", 40),
new Student("김너굴", 100));
// sum()
int sum = studentList.stream()
.mapToInt(Student::getScore)
.sum();
// reduce
int reduce = studentList.stream()
.mapToInt(Student::getScore)
.reduce(0, (a, b) -> a + b);
System.out.println("sum = " + sum);
System.out.println("reduce = " + reduce);
}
}
/* 실행 결과
sum = 220
reduce = 220
*/
요소 수집
스트림은 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메서드인 collect()를 제공하며 이 메서드를 이용하면 필요한 요소만 컬렉션에 담을 수 있고 요소들을 그룹핑한 후에도 집계도 할 수 있음
필터링한 요소 수집
stream의 collect(Collector<T,A,R> collector) 메서드는 필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집하고 이 컬렉션을 리턴함
매개값인 Collector는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정함
리턴 타입 | 메서드(매개변수) | 인터페이스 |
R | collect(Collector<T,A,R> collector) | Stream |
타입 파라미터의 T는 요소, A는 누적기(accumulator), R은 요소가 저장될 컬렉션을 뜻하며 풀어서 해석하면 T요소를 A누적기가 R에 저장한다는 의미임
Collector의 구현 객체는 다음과 같이 Collectors 클래스의 정적 메서드로 얻을 수 있음
리턴 타입 | 메서드 | 설명 |
Collector<T, ?, List<T>> | toList() | T를 List에 저장 |
Collector<T, ?, Set<T>> | toSet() | T를 Set에 저장 |
Collector<T, ?, Map<K,U>> | toMap( Function<T,K> keyMapper, Function<T,U> valueMapper) |
T를 K와 U로 매핑하여 K를 Key로 U를 값으로 Map에 저장 |
리턴값인 Collector를 보면 A(누적기)가 ?로 되어있는데 이것은 Collector가 List, Set, Map 컬렉션에 요소를 저장하는 방법을 알고 있어 별도의 누적기가 필요 없기 때문임
Java 16부터는 요소 스트림에서 List 컬렉션을 조금 더 편리하게 얻을 수 있는 toList() 메서드를 사용할 수 있음
Student
public class Student {
private String name;
private String sex;
private int score;
// 생성자, 게터
}
CollectExample
public class CollectExample {
public static void main(String[] args) {
List<Student> totalList = new ArrayList<>();
totalList.add(new Student("감자바", "남", 80));
totalList.add(new Student("조수영", "여", 50));
totalList.add(new Student("김식군", "남", 20));
totalList.add(new Student("사자수", "여", 90));
// 남학생만 묶어서 List 생성
List<Student> collectListMale = totalList.stream()
.filter(s -> s.getSex().equals("남"))
.collect(Collectors.toList());
List<Student> toListMale = totalList.stream()
.filter(s -> s.getSex().equals("남"))
.toList();
collectListMale.forEach(s -> System.out.println(s.getName()));
toListMale.forEach(s -> System.out.println(s.getName()));
System.out.println();
// 여학생 이름을 키, 학생의 점수를 값으로 갖는 Map 생성
Map<String, Integer> girlsStudents = totalList.stream()
.filter(s -> s.getSex().equals("여"))
.collect(Collectors.toMap(s -> s.getName(),s -> s.getScore()));
System.out.println("girlsStudents = " + girlsStudents);
}
}
/* 실행 결과
감자바
김식군
감자바
김식군
girlsStudents = {조수영=50, 사자수=90}
*/
요소 그룹핑
collect() 메서드는 단순히 요소를 수집하는 기능 외에 컬렉션의 요소들을 그룹핑해서 Map 객체를 생성하는 기능도 제공함
Collectors.groupingBy() 메서드에서 얻은 Collector를 collect() 메서드를 호출할 때 제공하면 됨
리턴 타입 | 메서드 |
Collector<T, ?, Map<K, List<T>>> | groupingBy(Function<T, K> classifier) |
groupingBy()는 Function을 이용해서 T를 K로 매핑하고 K를 키로 해서 List<T>를 값으로 갖는 Map 컬렉션을 생성함
CollectExample
- "남","여"를 키로 설정하고 List<Student>를 값으로 갖는 Map을 생성하고 출력
public class CollectExample {
public static void main(String[] args) {
List<Student> totalList = new ArrayList<>();
totalList.add(new Student("감자바", "남", 80));
totalList.add(new Student("조수영", "여", 50));
totalList.add(new Student("김식군", "남", 20));
totalList.add(new Student("사자수", "여", 90));
Map<String, List<Student>> groupMap = totalList.stream()
.collect(
Collectors.groupingBy(s -> s.getSex()) // 그룹핑 key를 리턴
);
List<Student> maleList = groupMap.get("남");
List<Student> femaleList = groupMap.get("여");
System.out.println("남학생");
maleList.forEach(s -> System.out.println(s.getName()));
System.out.println();
System.out.println("여학생");
femaleList.forEach(s -> System.out.println(s.getName()));
}
}
/* 실행 결과
남학생
감자바
김식군
여학생
조수영
사자수
*/
Collectors.groupingBy() 메서드는 그룹핑 후 매핑 및 집계(평균, 카운팅, 연결, 최대, 최소, 합계)를 수행할 수 있도록 두 번째 매개값인 Collector를 가질 수 있음
두 번째 매개값으로 사용될 Collector를 얻을 수 있는 Collectors의 정적 메서드들은 아래와 같음
리턴 타입 | 메서드(매개변수) | 설명 |
Collector | mapping(Function, Collector) | 매핑 |
Collector | averagingDouble(ToDoubleFunction) | 평균값 |
Collector | counting() | 요소 수 |
Collector | maxBy(Comparator) | 최대값 |
Collector | minBy(Comparator) | 최소값 |
Collector | reducing(BinaryOperator<T>) reducing(T identity, BinaryOperator<T>) |
커스텀 집계 값 |
CollectExample
- 학생들을 성별로 그룹핑하고 각각의 평균 점수를 구해서 Map으로 얻는 예제
public class CollectExample {
public static void main(String[] args) {
List<Student> totalList = new ArrayList<>();
totalList.add(new Student("감자바", "남", 80));
totalList.add(new Student("조수영", "여", 50));
totalList.add(new Student("김식군", "남", 20));
totalList.add(new Student("사자수", "여", 90));
Map<String, Double> map = totalList.stream()
.collect(Collectors.groupingBy(
s -> s.getSex(),
Collectors.averagingDouble(s -> s.getScore())
));
System.out.println("map = " + map);
}
}
/* 실행 결과
map = {남=50.0, 여=70.0}
*/
요소 병렬 처리
요소 병렬 처리(Parallel Operation)이란 멀티 코어 CPU 환경에서 전체 요소를 분할하여 각각의 코어가 병렬적으로 처리하는 것을 말함
요소 병렬 처리의 목적은 작업 처리 시간을 줄이는 것에 있으며 자바는 요소 병렬 처리를 위해 병렬 스트림을 제공함
동시성과 병렬성
멀티 스레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행되기 때문에 이들 용어에 대해 정확히 이해하는 것이 좋음
동시성은 멀티 작업을 위해 멀티 스레드가 하나의 코어에서 번갈아 가며 실행하는 것을 말하고 병렬성은 멀티 작업을 위해 멀티 코어를 각각 이용해서 병렬로 실행하는 것을 말함
동시성은 한 시점에 하나의 작업만 실행하는데 번갈아 작업을 실행하는 것이 워낙 빠르다보니 동시에 처리되는 것처럼 보일 뿐임
병렬성은 한 시점에 여러 개의 작업을 병렬로 실행하기 때문에 동시성보다 좋은 성능을 냄
병렬성을 데이터 병렬성과 작업 병렬성으로 구분할 수 있음
데이터 병렬성(Data parallelism)
- 전체 데이터를 분할해서 서브 데이터셋으로 만들고 이 서브 데이터셋들을 병렬처리해서 작업을 빨리 끝내는 것을 말함
- 자바 병렬 스트림은 데이터 병렬성을 구현한 것임
작업 병렬성(Task parallelism)
- 서로 다른 작업을 병렬처리 하는 것을 말함
- 작업 병렬성의 대표적인 예는 서버 프로그램을 예로 들 수 있는데 서버는 각각의 클라이언트에서 요청한 내용을 개별 스레드에서 병렬로 처리함
포크조인 프레임워크
자바 병렬 스트림은 요소들을 병렬 처리하기 위해 포크조인 프레임워크(ForkJoin Framework)를 사용함
포크조인 프레임워크는 포크 단계에서 전체 요소들을 서브 요소셋으로 분할하고 각각의 서브 요소셋을 멀티 코어에서 병렬로 처리하며 조인 단계에서는 서브 결과를 결합해서 최종 결과를 만들어냄
예를 들어 쿼드 코어 CPU에서 병렬 스트림으로 요소들을 처리할 경우 먼저 포크 단계에서 스트림의 전체 요소들을 4개의 서브 요소셋으로 분할하고 각각의 서브 요소셋을 개별 코어에서 처리한 후 조인 단계에서 3번의 결합 과정을 거쳐 최종 결과를 산출함
병렬 처리 스트림은 포크 단계에서 요소를 순서대로 분할하지 않으며 그림에서는 이해를 쉽게 하기 위해 앞에서부터 차례대로 4등분 했지만 내부적으로 요소들을 나누는 알고리즘이 있기 때문에 개발자들이 별도로 신경쓸 필요는 없음
포크조인 프레임워크는 병렬 처리를 위해 스레드풀을 사용하는데 각각의 코어에서 서브 요소셋을 처리하는 것은 작업 스레드가 해야 하므로 스레드 관리가 필요함
포크조인 프레임워크는 ExecutorService의 구현 객체인 ForkJoinPool을 사용하여 작업 스레드를 관리함
병렬 스트림 사용
자바 병렬 스트림을 이용할 경우에는 백그라운드에서 포크조인 프레임워크가 사용되기 때문에 개발자는 매우 쉽게 병렬 처리를 할 수 있음
리턴 타입 | 메서드(매개변수) | 제공 컬렉션 또는 스트림 |
Stream | parallelStream() | List 또는 Set 컬렉션 |
Stream | parallel() | java.util.Stream |
IntStream | java.util.IntStream | |
LongStream | java.util.LongStream | |
DoubleStream | java.util.DoubleStream |
parallelStream() 메서드는 컬렉션(List, Set)으로부터 병렬 스트림을 바로 리턴함
parallel() 메서드는 기존 스트림을 병렬 처리 스트림으로 변환함
ParallelExample
- 1억 개의 점수에 대한 평균을 얻을 때 일반 스트림과 병렬 스트림의 처리 시간을 측정하고 비교하는 예제
public class ParallelExample {
public static void main(String[] args) {
Random random = new Random();
List<Integer> scores = new ArrayList<>();
for (int i = 0; i < 100_000_000; i++) {
scores.add(random.nextInt(101)); // 1 ~ 100까지의 랜덤값 저장
}
double avg = 0.0;
long startTime = 0;
long endTime = 0;
long time = 0;
// 일반 스트림 처리
Stream<Integer> stream = scores.stream();
startTime = System.nanoTime();
avg = stream.mapToInt(s -> s.intValue())
.average()
.getAsDouble();
endTime = System.nanoTime();
time = endTime - startTime;
System.out.println("avg: " + avg + ", 일반 스트림 처리 시간: " + time + "ns");
// 병렬 스트림 처리
Stream<Integer> parallelStream = scores.parallelStream();
startTime = System.nanoTime();
avg = parallelStream.mapToInt(s -> s.intValue())
.average()
.getAsDouble();
endTime = System.nanoTime();
time = endTime - startTime;
System.out.println("avg: " + avg + ", 병렬 스트림 처리 시간: " + time + "ns");
}
}
/* 실행 결과
avg: 50.00329739, 일반 스트림 처리 시간: 72045250ns
avg: 50.00329739, 병렬 스트림 처리 시간: 26342791ns
*/
병렬 처리 성능
스트림 병렬 처리가 스트림 순차 처리보다 항상 실행 성능이 좋다고 판단해서는 안되며 먼저 병렬 처리에 영향을 미치는 3가지 요인을 살펴보아야 함
- 요소의 수와 요소당 처리 시간
- 컬렉션에 전체 요소의 수가 적고 요소당 처리 시간이 짧으면 일반 스트림이 병렬 스트림보다 빠를 수 있음
- 병렬 처리는 포크 및 조인 단계가 있고 스레드 풀을 생성하는 추가적인 비용이 발생하기 때문임
- 스트림 소스의 종류
- ArrayList와 배열을 인덱스로 요소를 관리하기 때문에 포크 단계에서 요소를 쉽게 분리할 수 있어 병렬 처리 시간이 절약됨
- 반면 HashSet, TreeSet은 요소 분리가 쉽지 않고 LinkedList 역시 링크를 따라가야하므로 요소 분리가 쉽지 않아 이 소스들은 상대적으로 병렬 처리가 늦음
- 코어의 수
- CPU 코어(Core)의 수가 많으면 많을수록 병렬 스트림의 성능은 좋아지지만 코어의 수가 적을 경우에는 일반 스트림이 더 빠를 수 있는데, 병렬 스트림은 스레드 수가 증가하여 동시성이 많이 일어날 수 있어 오히려 느려질 수 있기 때문임
'기타 개발 강의 및 도서 > 이것이자바다 개정판' 카테고리의 다른 글
Chapter 17 - 스트림 요소 처리1(Stream), 스트림이란?, 내부 반복자, 중간 처리와 최종 처리, 스트림 얻기, 요소 걸러내기(필터링), 요소 변환(매핑) (0) | 2025.03.13 |
---|---|
Chapter 16 - 람다식 (0) | 2025.03.10 |
Chapter19 - 네트워크 입출력, IP 주소 얻기, TCP 네트워킹, UDP 네트워킹 (0) | 2025.02.11 |
chapter4 - 조건문과 반복문 확인문제, 추가내용 / 이것이 자바다(개정판) (0) | 2024.02.13 |
chapter3 - 연산자 확인문제, / 이것이 자바다(개정판) (0) | 2024.02.12 |