지난 포스트에서는 일반 for문과 for-each 문과 Iterator에 대하여 포스팅했었습니다.
이번 포스팅은 for-each문에서 발생하는 ConcurrentModificationException에 대해서 포스팅하겠습니다.
ConcurrentModificationException 이란?
concurrentModifyException 은 Java 진영에서 컬렉션을 수정하는 동안 다른 스레드에서 동시에 해당 컬렉션을 수정하려고 할 때 발생하는 예외입니다.
자바에서 발생하는 예외(Exception) 중 하나로, 동시 수정이 일어날 때 발생합니다.
Java진영 에서의 컬렉션은 여러 요소를 저장하고 관리하는 데 사용되는 자료구조입니다.
일반적으로 Collection은 동기화되지 않은 (Non-Synchronized) 상태로 사용됩니다.
따라서 여러 스레드가 동시에 동일한 컬렉션을 수정하면 예측 불가능한 결과가 발생할 수 있습니다.
ConcurrentModificationException은 이런 문제를 방지하고자 Java Collection을 수정하는 동시성 작업을 제어하기 위해 도입되었습니다.
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 첫 번째 스레드에서 names 컬렉션을 반복(iterate)하고 있습니다.
Thread thread1 = new Thread(() -> {
for (String name : names) {
System.out.println(name);
}
});
// 두 번째 스레드에서 names 컬렉션을 수정하고 있습니다.
Thread thread2 = new Thread(() -> {
names.add("David"); // 동시성 문제 발생 시도
});
thread1.start();
thread2.start();
}
}
위의 코드에서 두 번째 스레드가 names 컬렉션을 수정하려고 할 때,
첫 번째 스레드가 반복하고 있는 도중에 ConcurrentModificationException이 발생할 수 있습니다.
ConcurrentModificationException 이 발생하는 이유
위의 예시를 보고 오시면 아... ConccurrentModificationException 이 발생하는 이유는 스레드의 동시성 때문이구나....
라고 생각할 수도 있지만 foe-each 문에서의 ConcurrentModificationException이 발생하는 경우는 멀티스레드 프로그래밍과는 직접적인 연관이 없습니다.
ConcurrentModificationException 은 주로 Collection(컬렉션)을 다중 스레드에서 동시에 수정하려고 할 때, 발생합니다.
하지만, for-each문은 멀티스레드를 생성하거나 다중 스레드를 생성하거나 다중 스레드를 사용하는 것이 아니기 때문에,
for-each문 자체가 ConcurrentModificationException를 발생시키지는 않습니다.
for-each 문에서 발생하는 주요 원인은 2가지입니다.
반복 도중 Collection을 수정하는 경우
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
for (int number : numbers) {
if (number == 2) {
// 반복 중에 컬렉션을 수정하는 작업 (요소 삭제)
numbers.remove(Integer.valueOf(number)); // ConcurrentModificationException 발생
}
}
}
}
위의 예시에서는 for-each 문을 사용하여 numbers 리스트를 순회하고 있습니다.
그러나 number 변수가 2일 때, numbers 리스트에서 해당 요소를 삭제하려고 한다면
ConcurrentModificationException이 발생합니다.
이는 반복 중에 컬렉션을 수정하면서 for-each문이 예상치 못한 동작을 하게 되기 때문입니다.
다른 스레드에 의해 컬렉션을 수정하는 경우
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
// 새로운 스레드 생성
Thread thread = new Thread(() -> {
// 다른 스레드에서 컬렉션을 수정하는 작업 (요소 추가)
numbers.add(4);
});
thread.start();
// 메인 스레드에서 컬렉션을 순회하는 도중 다른 스레드가 컬렉션을 수정
for (int number : numbers) {
System.out.println(number); // ConcurrentModificationException 발생
}
}
}
위의 예시에서는 새로운 스레드를 생성하고, 이 스레드에서 Numbers 리스트에 새로운 요소 4를 추가하고 있습니다.
동시에 메인 스레드에서도 number 리스트를 for-each 문으로 순회하고 출력하려고 합니다.
하지만 다른 스레드에서 numbers 리스트를 수정하는 동안에 메인 스레드가 해당 컬렉션을 순회하고 있으므로
ConcurrentModificationException 이 발생합니다.
for-each문의 순회중 요소 추가, 삭제 시 문제 되는 이유
for-each 문은 내부적으로 컬렉션의 Iterator를 사용하여 요소를 순회합니다.
좀 더 자세히 알고 싶다면 아래 링크를 갔다 오는 것을 추천드립니다.
https://jyyoun1022.tistory.com/10
Iterator 은 컬렉션의 요소를 순회하는 데 사용되는 인터페이스로서, for-each 문은 이 Iterator를 활용하여 순회 작업을 수행합니다.
1. for-each 문이 실행될 때, 컬렉션의 Iterator가 생성됩니다.
- 이 Iterator는 순회 도중에 컬렉션의 변경 여부를 감지할 수 있도록 설계되어 있습니다.
2. for-each 문이 Iterator를 사용하여 컬렉션의 요소를 하나씩 가져옵니다.
3. 만약 for-each문에서 직접 remove() 메서드를 호출하여 현재 요소를 삭제하면, Iterator는 컬렉션의 내부 상태가 변경(추가 및 삭제)되었다는 것을 감지합니다.
4. Iterator는 요소를 삭제한 후에도 다음 순회를 진행하기 위해 다음 요소를 가져오려고 할 때, 컬렉션의 구조가 변경되었음을 감지합니다.
5. Iterator는 컬렉션의 변경 여부를 확인한 결과 변경이 있었다는 것을 파악하고, 이 상태에서 추가적인 순회를 중단시키고
ConcurrentModificationException을 발생시킵니다.
결론(중요)
따라서, for-each 문은 Iterator를 사용하여 컬렉션을 순회하는데,
이때의 Iterator는 읽기 전용(Read-Only)입니다.
따라서, 모든 요소를 순회하면서 읽기만 가능하며, 컬렉션의 구조를 변경하는 작업(요소 추가, 삭제, 변경)은 허용되지 않습니다.
만약 for-each 문에서 순회 중에 요소를 직접 변경하면 Iterator가 해당 변경을 감지하고
해당 변경 작업에 대해 순회가 더 이상 의미가 없다고 판단하여
ConcurrentModificationException을 발생시키는 것입니다.
이를 통해 컬렉션의 구조를 안전하게 유지할 수 있도록 합니다.
올바른 순회도중의 요소 변경하는 방법
컬렉션(Collection)의 요소를 순회하면서 요소를 변경해야 할 경우에는 for-each 문 대신 일반 반복문과 인덱스를 사용하거나,
Iterator를 직접 활용하여 변경할 수 있습니다.
이러한 방법을 사용하면 순회 중의 요소를 변경할 수 있으며, ConcurrentModificatinException을 피할 수 있습니다.
일반 for문과 인덱스 사용하기
import java.util.ArrayList;
import java.util.List;
public class ElementModificationExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
for (int i = 0; i < numbers.size(); i++) {
int number = numbers.get(i);
if (number == 2) {
// 순회 중에 요소를 변경하는 작업
numbers.set(i, number * 10);
}
}
// 변경된 요소 확인
for (int number : numbers) {
System.out.println(number);
}
}
}
위의 예시 코드에서는 for 문과 인덱스를 사용하여 numbers 리스트를 순회하고 있습니다.
순회 중에 number 변수가 2일 때, 해당 요소를 10배로 변경을 시도하였습니다.
일반 for 문은 왜 가능한가?
이렇게 일반 for 문과 인덱스를 사용하여 컬렉션을 순회하면 구조가 변경되는지에 대한 감지가 Iterator를 사용하는 경우보다 미세하게 이루어지기 때문입니다.
따라서 일반 for 문과 인덱스를 사용하는 경우, 개발자가 직접 컬렉션을 제어하고 요소를 수정하기 때문에 ConcurrentModificationException을 피할 수 있습니다.
단, 이러한 방식으로 순회 중에 요소를 변경하더라도 멀티스레드 환경에서는 여전히 동시성 문제에 주의해야 합니다.
멀티스레드 환경에서는 적절한 동기화를 고려하여 요소를 변경해야 안전하게 작업할 수 있습니다.
Iterator를 직접 활용하기
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ElementModificationExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
int number = iterator.next();
if (number == 2) {
// 순회 중에 요소를 변경하는 작업
iterator.set(number * 10);
}
}
// 변경된 요소 확인
for (int number : numbers) {
System.out.println(number);
}
}
}
Iterator 은 일반 for문과 인덱스를 활용하는 방법보다 안전합니다.
일반적으로 Collection의 요소를 변경할 때는 Iterator를 사용하는 방식이 더 안전합니다.
Iterator를 사용하는 방식이 for-each문이나 일반적인 for 문과 index를 사용하는 방식보다 안전한 이유
- ConcurrentModificationException 예방
- Iterator는 컬렉션의 구조가 변경되었는지를 확인하면서 순회하기 때문에, 순회 도중에 다른 스레드나 코드에 의해 컬렉션의 구조가 변경되더라도 ConcurrentModificationException을 발생시키지 않고 안전하게 순회할 수 있습니다.
- 삭제 및 추가 메서드 지원
- Iterator는 remove() 메서드를 통해 요소를 안전하게 삭제할 수 있도록 지원합니다.
- 또한 Java 8부터는 Iterator의 forEachRemaining() 메서드를 사용하여 순회 도중에 요소를 추가할 수 있는 방법도 제공합니다.
- forEachRemaining() 메서드는 남은 요소들을 순회하는 데 사용되며, 순회 중에 요소를 추가하거나 변경하는 작업은 예외를 발생시킬 수 있습니다.
- ConcurrentModificationException 예외를 피할 수 없기 때문에 요소를 추가할 때는 일반적인 for 문을 사용하는 것이 안전합니다.
- 독립적인 인터페이스
- Iterator는 독립적인 인터페이스이므로, 순회하는 동안 컬렉션 자체를 수정하는 코드와는 독립적으로 동작할 수 있습니다.
추가 팁! listIterator() 사용하기
package cocode.cocodeMarket.entity.member;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Lab {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
ListIterator<Integer> listIterator = numbers.listIterator();
while (listIterator.hasNext()) {
int number = listIterator.next();
if (number == 2) {
// ListIterator를 사용하여 요소를 추가
listIterator.add(number * 10);
}
}
// 변경된 요소 확인
for (int num : numbers) {
System.out.println(num);
}
}
}
listIterator() 메서드는 List 인터페이스에서 제공하는 메서드로 Iterator를 확장한 인터페이스로, Iterator 보다 더 많은 기능을 제공합니다.
이렇게 ListIterator를 사용하면 순회 중에 요소를 안전하게 추가하고 변경할 수 있으며,
ConcurrentModificationException을 피할 수 있습니다.
이를 통해 리스트의 순회 중에 요소를 수정하는 작업을 안전하게 수행할 수 있습니다.
'JAVA' 카테고리의 다른 글
[Spring] 스프링 제어의 역전(IoC) 와 의존성 주입(DI) 완벽 이해하기 Feat.빈(Bean) (4) | 2023.08.01 |
---|---|
스프링의 인터셉터(Interceptor)와 필터(Filter) 완벽 이해하기 (8) | 2023.07.31 |
[JAVA] 스레드(Thread), 스레드의 동시성, 멀티 스레드(Multi-Thread) (5) | 2023.07.30 |
[JAVA] 일반 for문과 향상된 for문의 차이와 진실 Feat.Iterator (5) | 2023.07.29 |
ObjectCopyHelper 만들기 (DTO,VO를 Entity로 ) (0) | 2023.07.28 |