✍️ Stream
자바8에 추가된 기능 중에 가장 많은 관심을 받은 기능인 Stream에 대해 알아보려 한다.
Stream이란 컬렉션과 같은 연속된 데이터를 처리하는 오퍼레이션의 모음으로, Stream 그 자체로는 데이터가 아니다.
Stream API엔 filter, map, sorted 등 여러 중개 오퍼레이션이 있는데, 해당 오퍼레이션을 호출하면 연산을 수행하고 중간 처리된 Stream이 반환된다. 또한 반환된 Stream에 다시 중개 오퍼레이션을 호출해 다른 연산을 계속 이어나갈 수 있는데 이를 Stream pipeline(스트림 파이프라인)이라 한다.
Stream의 한 가지 특징은 소스 데이터를 변경하지 않는다는 것이다.
예를 들어, String을 담고 있는 List가 있고 List의 Stream을 얻어와서 소스 데이터를 대문자로 바꾸는 map 오퍼레이션을 적용한다고 했을 때 그 반환값은 또 다른 Stream이 될 뿐 소스 데이터에 영향을 주지 않는다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
Stream<String> stringStream = names.stream().map(String::toUpperCase);
names.forEach(System.out::println); // 소스 데이터 영향 x
}
// Kang
// Hello
// World!
Stream의 장점을 간략한 예제 코드로 살펴보면 코드가 간결해지고 의도가 명확하게 드러나 가독성이 증가한다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
// K로 시작하는 String을 대문자로 바꾸는 로직
// Stream 사용 안 한 코드
for(String name : names) {
if(name.startsWith("K")) {
System.out.println(name.toUpperCase());
}
}
System.out.println("===");
// Stream 사용 코드
List<String> collect = names.stream()
.filter(name -> name.startsWith("K")).map(String::toUpperCase)
.collect(Collectors.toList());
collect.forEach(System.out::println);
}
// KANG
// ===
// KANG
🍊 중개, 종료 오퍼레이션
Stream API가 제공하는 여러 오퍼레이션을 크게 두 가지로 나눌 수 있다. 중개 오퍼레이션과 종료 오퍼레이션
중개 오퍼레이션과 종료 오퍼레이션의 가장 큰 차이점은 중개 오퍼레이션은 Stream을 반환하고 종료 오퍼레이션은 Stream이 아닌 다른 타입을 반환한다.
중개 오퍼레이션
위 예제에서 봤던 map이 대표적인 중개 오퍼레이션 중 하나인데, 실제 예제에서 확인할 수 있듯 map은 Stream을 반환한다.
중개 오퍼레이션은 기본적으로 lazy한데, lazy하다는 게 무슨 뜻인지 예제를 보며 이해해 보자.
아래 코드에서 Stream의 map 메서드에 전달한 람다식을 처리하며 System.out.println(s);이 출력이 될까?
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
names.stream().map(s -> {
System.out.println(s);
return s.toUpperCase();
});
}
// 출력 없음
정답은 "그렇지 않다"이다. 근본적으로 종료 오퍼레이션이 없는 Stream pipeline은 아무 일도 하지 않는다.
즉, 중개 오퍼레이션은 종료 오퍼레이션이 실행되어야만 실행되는 lazy한 특징을 가지고 있다.
종료 오퍼레이션
위 예제에서 종료 오퍼레이션 collection을 추가하니 중개 오퍼레이션의 연산이 수행됨을 알 수 있다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
names.stream().map(s -> {
System.out.println(s);
return s.toUpperCase();
}).collect(Collectors.toList());
}
// Kang
// Hello
// World!
마지막으로 Stream pipeline의 결과물이 잘 동작하는지 코드로 확인해 보고 싶다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
List<String> collect = names.stream().map(String::toUpperCase).collect(Collectors.toList());
collect.forEach(System.out::println);
}
// KANG
// HELLO
// WORLD!
📜 병렬처리
Stream을 사용하면 손쉽게 병렬처리를 할 수 있다.
아래와 같은 for loop는 병렬적으로 처리하기 어렵다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
for(String name : names) {
if(name.startsWith("K")) {
System.out.println(name.toUpperCase());
}
}
}
반면, parallelStream을 사용하면 JVM이 병렬적으로 오퍼레이션을 처리해 준다.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Kang");
names.add("Hello");
names.add("World!");
List<String> collect2 = names.parallelStream().map(name -> {
System.out.println(name + " " + Thread.currentThread().getName());
return name.toUpperCase();
}).collect(Collectors.toList());
}
// Hello main
// World! main
// Kang ForkJoinPool.commonPool-worker-1
주의해야 할 점은, parallelStream을 사용한다고 해서 무조건 성능이 좋아진다는 보장이 없다.
병렬처리를 위해 스레드를 생성하는 비용, 스레드 별로 생성한 결과물을 수집하는 비용, 컨텍스트 스위치 비용 등 여러 부가적인 추가 비용이 발생해 여러 스레드에서 나눠 처리하는 것보다 한 스레드에서 처리하는 게 더 좋은 성능을 낼 수 있다.
그렇다면 parallelStream이 유용하게 사용되는 상황은 언제일까?
바로 처리해야 하는 데이터가 방대한 경우 parallelStream의 사용을 고려해 볼 수 있다. 무엇보다 개발자가 직접 테스트해 보고 trade-off를 계산해서 사용하는 것이 가장 바람직하다.
'Java > Java 8' 카테고리의 다른 글
[Java8] Chapter 4-1. Optional (1) (0) | 2022.02.20 |
---|---|
[Java8] Chapter 3-2. Stream API 실습 (2) (0) | 2022.02.20 |
[Java8] Chapter 2-1. 인터페이스 default, static 메서드 (0) | 2022.02.17 |
[Java8] Chapter 1-4. 메서드 레퍼런스 (0) | 2022.02.16 |
[Java8] Chapter 1-3. 람다 표현식과 변수 캡쳐 (0) | 2022.02.08 |