[Java 파싱] Stream API의 지연 연산과 동시성 문제
Stream API는 지연 연산(lazy evaluation) 방식을 사용하며, 이로 인해 발생할 수 있는 동시성 문제와 해결 방안을 설명합니다.
Stream의 지연 연산과 동시성 문제
Stream은 실행시점에 값을 읽는게 아니라 참조(주소)를 읽어 둔다. 즉, 지연 연산을 사용한다.
따라서 이미 데이터를 다 읽은 상태(즉시 연산)가 아니라서 문제가 발생할 수 있는것이다.
-
names.stream()
이 호출되면 스트림이 생성되지만, 아직 데이터는 처리X -
.filter(...)
와 같은 연산은 중간 연산이고, 최종 연산(.collect(...)
)이 호출될 때 실제로 데이터를 처리! - 이 과정에서 스트림은 데이터 소스를 반복(iterate)하며 필요한 작업을 수행
ArrayList 는 수정(추가, 삭제 등)될 경우, 반복 중에 ConcurrentModificationException 예외를 던지도록 설계되어 있다.
따라서 Stream 연산은 내부적으로 반복(iteration)을 수행하기 때문에, 구조적 수정이 감지되면 예외가 발생하고, 예외가 발생 안 해도 순서나, 데이터가 꼬여서 비정상적인 결과가 나타날 수 있다.
// 반복 중 데이터 제거로 인한 예외 발생 (다른 스레드에서 수정한 경우도 마찬가지로 예외 발생)
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> names.remove(name)); // ConcurrentModificationException
해결 방안
1. 복사본 사용: 스트림 연산 전에 데이터의 복사본을 만들어 사용
List<String> result = new ArrayList<>(names).stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
2. 불변 객체 사용: 외부 수정 가능성이 원천 차단
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
3. 동기화 사용: 줄 세우기. 단, 성능 저하 발생 가능
synchronized (names) {
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
}
💡 권장 사항
- 가능한 불변 객체 사용
- 필요한 경우 복사본 생성
- 동기화는 성능 저하를 고려하여 신중히 사용
스트림의 지연 연산 특성을 이해하고 적절한 동시성 제어 방법을 선택하는 것이 중요합니다.
댓글남기기