
Stream.forEach가 종단 연산이라는 것을 공부하다보니 여기까지 왔다.
1. Collection.forEach란?
`collection.forEach`는 `Iterable` 인터페이스에서 제공하는 메서드로, 컬렉션의 모든 요소를 순회하면서 하나씩 작업을 수행합니다.
특징
- 내부 반복자(internal iterator)를 사용합니다.
- 순차적으로 요소를 처리합니다.
- 기본적으로 데이터 변경에 적합합니다.
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// Collection.forEach 사용
names.forEach(name -> System.out.println("Hello, " + name + "!"));
}
}
2. Stream.forEach란?
Stream.forEach는 Stream API에서 제공하는 종단 연산으로, 스트림의 모든 요소를 하나씩 처리합니다.
특징
- 지연 실행(lazy evaluation): 스트림에서 중간 연산을 포함한 모든 작업은 forEach와 같은 종단 연산이 호출될 때 실행됩니다.
- 병렬 스트림(parallel stream)에서 사용하면 병렬적으로 요소를 처리할 수 있습니다.
- 데이터 변경에는 적합하지 않음: 데이터를 조회하거나 소비할 때 주로 사용합니다.
3. 병렬 처리의 차이
Stream.forEach는 병렬 처리에서 달라진다!
Stream.forEach를 병렬 스트림과 함께 사용할 때, 작업이 병렬적으로 처리됩니다. 이때 순서가 보장되지 않을 수 있습니다.
List<String> names = List.of("A", "B", "C", "D", "E");
// Collection.forEach 사용
names.forEach(name -> System.out.println(Thread.currentThread().getName() + " processing " + name));
System.out.println("------------");
// 병렬 스트림에서 Stream.forEach 사용
names.parallelStream().forEach(name -> System.out.println(Thread.currentThread().getName() + " processing " + name));
}
코드를 여러 번 실행하면 `Collection.forEach()`가 삽입 순서대로 항목을 처리하는 반면, `stream.parallelStream().forEach()`는 실행할 때마다 다른 결과를 생성하는 것을 볼 수 있습니다.

❗️만약 병렬처리를 하면서 순서를 유지하고 싶다면 `forEachOrdered`를 사용하시면 됩니다.
names.parallelStream()
.forEachOrdered(name -> System.out.println(Thread.currentThread().getName() + " processing " + name));

4.컬렉션 수정
많은 컬렉션(ex: ArrayList or HashSet)은 반복하는 동안 구조적으로 수정되어서는 안 됩니다. 반복하는 동안 요소가 제거되거나 추가되면 `ConcurrentModification`예외가 발생합니다.
더욱이 컬렉션은 빠르게 실패하도록 설계되어 있기 때문에 수정이 있으면 즉시 예외가 발생합니다.
마찬가지로, 스트림 파이프라인을 실행하는 동안 요소를 추가하거나 제거하면 `ConcurrentModification`예외가 발생합니다. 그러나 이 예외는 나중에 throw됩니다.
먼저 `Collection.forEach()`먼저 보겠습니다.
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Consumer<String> removeElement = s -> {
System.out.println(s + " " + list.size());
if (s != null && s.equals("A")) {
list.remove("D");
}
};
list.forEach(removeElement);
}

`Collection.forEach()`는 실패 속도가 빠르므로 반복을 멈추고 다음 요소가 처리되기 전에 예외가 발생합니다 .
그럼 이번에는 위 코드에서 `stream.forEach()`를 사용해보도록 하겠습니다.
list.stream().forEach(removeElement);

forEach가 끝나고 예외를 발생시킵니다.
왜 예외가 나중에 발생할까?
stream().forEach에서 예외가 스트림 처리 이후 발생하는 이유는 다음과 같습니다
- 스트림의 비동기적 처리 가능성:
- 스트림은 기본적으로 병렬 처리가 가능하도록 설계되었습니다. 예를 들어, `parallelStream()`을 사용하면 여러 스레드에서 동시에 작업이 수행될 수 있습니다.
- `synchronized` 키워드가 없더라도 각 스레드는 독립적으로 데이터를 처리할 수 있지만, 공유된 컬렉션에서 구조적 변경이 발생하면 스플리터의 무결성이 깨져 나중에 검증 시 문제가 됩니다.
- Spliterator의 설계:
- 스트림은 `Spliterator`를 사용하여 데이터를 분할하고 처리합니다. 이 과정에서 `Spliterator`는 컬렉션의 상태를 추적합니다.
- 스트림이 종료된 후, 최종적으로 컬렉션의 변경 여부를 확인합니다. 이때 변경 사항이 발견되면 예외가 발생합니다.
- 스트림의 지연 평가:
- 스트림은 결과를 요구받기 전까지 실제로 작업을 수행하지 않습니다.
- 따라서 스트림의 모든 작업이 끝난 후에야 컬렉션 변경 여부를 검증합니다.
핵심 정리
- forEach의 동작:
- List.forEach는 Iterator를 사용하여 컬렉션의 요소를 순차적으로 처리합니다.
- 순회 중 컬렉션이 수정되면 즉시 `ConcurrentModificationException`을 발생시킵니다.
- 컬렉션의 변경 여부는 `modCount`라는 필드를 통해 확인됩니다. `modCount`가 변경되면 즉시 예외가 발생합니다.
- stream().forEach의 동작:
- stream().forEach는 스트림의 각 요소를 처리하며, 내부적으로 `Spliterator`를 사용합니다.
- 스트림이 한 번에 처리된 후에 변경 여부를 확인합니다. 따라서 모든 요소를 처리한 다음에 예외가 발생할 수 있습니다.
- 이 동작은 스트림이 지연 평가(lazy evaluation)로 동작하기 때문에 가능합니다.
- 병렬로 처리할 때 순서가 보장되지 않습니다.
📚 Reference
- https://www.baeldung.com/java-collection-stream-foreach
- https://tecoble.techcourse.co.kr/post/2020-09-30-collection-stream-for-each/
'JAVA > 개념' 카테고리의 다른 글
private 생성자를 왜 만드시나요? (0) | 2025.02.15 |
---|---|
언제 equals를 재정의 해야할까? (0) | 2024.11.22 |
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |
스트림의 중간 연산, 종단 연산 (0) | 2024.11.18 |
자바 정적 팩토리 메서드 쉽게 이해하기 - 왜 사용하고 어떻게 활용할까? (0) | 2024.10.24 |

Stream.forEach가 종단 연산이라는 것을 공부하다보니 여기까지 왔다.
1. Collection.forEach란?
collection.forEach
는 Iterable
인터페이스에서 제공하는 메서드로, 컬렉션의 모든 요소를 순회하면서 하나씩 작업을 수행합니다.
특징
- 내부 반복자(internal iterator)를 사용합니다.
- 순차적으로 요소를 처리합니다.
- 기본적으로 데이터 변경에 적합합니다.
public class Main { public static void main(String[] args) { List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie")); // Collection.forEach 사용 names.forEach(name -> System.out.println("Hello, " + name + "!")); } }
2. Stream.forEach란?
Stream.forEach는 Stream API에서 제공하는 종단 연산으로, 스트림의 모든 요소를 하나씩 처리합니다.
특징
- 지연 실행(lazy evaluation): 스트림에서 중간 연산을 포함한 모든 작업은 forEach와 같은 종단 연산이 호출될 때 실행됩니다.
- 병렬 스트림(parallel stream)에서 사용하면 병렬적으로 요소를 처리할 수 있습니다.
- 데이터 변경에는 적합하지 않음: 데이터를 조회하거나 소비할 때 주로 사용합니다.
3. 병렬 처리의 차이
Stream.forEach는 병렬 처리에서 달라진다!
Stream.forEach를 병렬 스트림과 함께 사용할 때, 작업이 병렬적으로 처리됩니다. 이때 순서가 보장되지 않을 수 있습니다.
List<String> names = List.of("A", "B", "C", "D", "E"); // Collection.forEach 사용 names.forEach(name -> System.out.println(Thread.currentThread().getName() + " processing " + name)); System.out.println("------------"); // 병렬 스트림에서 Stream.forEach 사용 names.parallelStream().forEach(name -> System.out.println(Thread.currentThread().getName() + " processing " + name)); }
코드를 여러 번 실행하면 Collection.
forEach()
가 삽입 순서대로 항목을 처리하는 반면, stream.parallelStream().forEach()
는 실행할 때마다 다른 결과를 생성하는 것을 볼 수 있습니다.

❗️만약 병렬처리를 하면서 순서를 유지하고 싶다면 forEachOrdered
를 사용하시면 됩니다.
names.parallelStream() .forEachOrdered(name -> System.out.println(Thread.currentThread().getName() + " processing " + name));

4.컬렉션 수정
많은 컬렉션(ex: ArrayList or HashSet)은 반복하는 동안 구조적으로 수정되어서는 안 됩니다. 반복하는 동안 요소가 제거되거나 추가되면 ConcurrentModification
예외가 발생합니다.
더욱이 컬렉션은 빠르게 실패하도록 설계되어 있기 때문에 수정이 있으면 즉시 예외가 발생합니다.
마찬가지로, 스트림 파이프라인을 실행하는 동안 요소를 추가하거나 제거하면 ConcurrentModification
예외가 발생합니다. 그러나 이 예외는 나중에 throw됩니다.
먼저 Collection.forEach()
먼저 보겠습니다.
public static void main(String[] args) { List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); Consumer<String> removeElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } }; list.forEach(removeElement); }

Collection.forEach()
는 실패 속도가 빠르므로 반복을 멈추고 다음 요소가 처리되기 전에 예외가 발생합니다 .
그럼 이번에는 위 코드에서 stream.forEach()
를 사용해보도록 하겠습니다.
list.stream().forEach(removeElement);

forEach가 끝나고 예외를 발생시킵니다.
왜 예외가 나중에 발생할까?
stream().forEach에서 예외가 스트림 처리 이후 발생하는 이유는 다음과 같습니다
- 스트림의 비동기적 처리 가능성:
- 스트림은 기본적으로 병렬 처리가 가능하도록 설계되었습니다. 예를 들어,
parallelStream()
을 사용하면 여러 스레드에서 동시에 작업이 수행될 수 있습니다. synchronized
키워드가 없더라도 각 스레드는 독립적으로 데이터를 처리할 수 있지만, 공유된 컬렉션에서 구조적 변경이 발생하면 스플리터의 무결성이 깨져 나중에 검증 시 문제가 됩니다.
- 스트림은 기본적으로 병렬 처리가 가능하도록 설계되었습니다. 예를 들어,
- Spliterator의 설계:
- 스트림은
Spliterator
를 사용하여 데이터를 분할하고 처리합니다. 이 과정에서Spliterator
는 컬렉션의 상태를 추적합니다. - 스트림이 종료된 후, 최종적으로 컬렉션의 변경 여부를 확인합니다. 이때 변경 사항이 발견되면 예외가 발생합니다.
- 스트림은
- 스트림의 지연 평가:
- 스트림은 결과를 요구받기 전까지 실제로 작업을 수행하지 않습니다.
- 따라서 스트림의 모든 작업이 끝난 후에야 컬렉션 변경 여부를 검증합니다.
핵심 정리
- forEach의 동작:
- List.forEach는 Iterator를 사용하여 컬렉션의 요소를 순차적으로 처리합니다.
- 순회 중 컬렉션이 수정되면 즉시
ConcurrentModificationException
을 발생시킵니다. - 컬렉션의 변경 여부는
modCount
라는 필드를 통해 확인됩니다.modCount
가 변경되면 즉시 예외가 발생합니다.
- stream().forEach의 동작:
- stream().forEach는 스트림의 각 요소를 처리하며, 내부적으로
Spliterator
를 사용합니다. - 스트림이 한 번에 처리된 후에 변경 여부를 확인합니다. 따라서 모든 요소를 처리한 다음에 예외가 발생할 수 있습니다.
- 이 동작은 스트림이 지연 평가(lazy evaluation)로 동작하기 때문에 가능합니다.
- 병렬로 처리할 때 순서가 보장되지 않습니다.
- stream().forEach는 스트림의 각 요소를 처리하며, 내부적으로
📚 Reference
- https://www.baeldung.com/java-collection-stream-foreach
- https://tecoble.techcourse.co.kr/post/2020-09-30-collection-stream-for-each/
'JAVA > 개념' 카테고리의 다른 글
private 생성자를 왜 만드시나요? (0) | 2025.02.15 |
---|---|
언제 equals를 재정의 해야할까? (0) | 2024.11.22 |
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |
스트림의 중간 연산, 종단 연산 (0) | 2024.11.18 |
자바 정적 팩토리 메서드 쉽게 이해하기 - 왜 사용하고 어떻게 활용할까? (0) | 2024.10.24 |