개요
자바에서는 Fail-Fast라는 용어가 있습니다. 말 그대로 빠르게 실패한다는 의미로, 자바 컬렉션을 사용할 때 언제 어떻게 사용한는지 알아보겠습니다.
Fail-Fast vs Fail-Safe Iterator
자바 컬렉션은 fail-safe와 fail-fast 2가지 Iterator 타입이 있습니다. fail-safe는 컬렉션이 순환 도중에 변경이 가능한 경우이고, fail-fast는 컬렉션이 순환 도중에 변경이 불가능한 경우입니다. 즉, 비동기적인 작업 중에 동시적인 변경을 보장하지 못합니다. fail-fast를 해결하기 위해서는, 복사본을 사용하거나, 동기화를 보장하거나, Iterator 대신에 Enumeration을 사용하면 됩니다.
예를 들어, ArrayList의 경우, 순환을 할 때, 다른 쓰레드가 해당 컬렉션을 변경해서는 안됩니다. 만약에 Iterator가 이미 순환을 하고 있는데 새로운 요소의 추가나 삭제 시도를 감지한다면, ArrayList는 fail-fast이므로 ConccurrentModificationException을 던집니다. Iterator.remove()에서는 예외를 던지 않고 컬렉션의 remove()에서만 예외를 던집니다.
동시에 발생하는 수정(Concurrent Modification) : 다른 작업이 이미 동작하고 있는 중에 동시적으로 객체가 변경되는 것을 의미합니다. 자바의 경우, 다른 쓰레드가 iterator를 수행하는 도중에 collection을 변경하는 경우입니다. 이 때 ConcurrentModificationException을 던집니다.
// Java code to demonstrate remove
// case in Fail-fast iterators
import java.util.ArrayList;
import java.util.Iterator;
public class FailFastExample {
public static void main(String[] args)
{
ArrayList<Integer> al = new ArrayList<>();
al.add(1);
al.add(2);
al.add(3);
al.add(4);
al.add(5);
Iterator<Integer> itr = al.iterator();
while (itr.hasNext()) {
if (itr.next() == 2) {
// will not throw Exception
itr.remove();
}
}
System.out.println(al);
itr = al.iterator();
while (itr.hasNext()) {
if (itr.next() == 3) {
// will throw Exception on
// next call of next() method
al.remove(3);
}
}
}
}
단, itr.remove()는 복사본을 제거하기 때문에 오류를 발생시키지 않고 al.remove()는 복사본이 아닌 collection을 제거하기 때문에 예외를 던집니다.
- 왜 컬렉션은 Fail-Fast 전략을 사용할까요?
Fail-Fast는 문제를 감지하는 즉시 예외를 던집니다. 왜냐하면 미래에 예측 불가능한 의도하지 않은 문제가 발생하는 것을 원천적으로 차단하기 위해서입니다.
Fast-Fail 예방하기
Fast-Fail을 예방하기 위해 복사본을 만드는 방법과 동기화를 보장하는 방법을 알아보겠습니다.
1. 복사본 만들기
CopyOnWriteArrayList로 배열을 복사하여 사용하면, 순환 도중에 추가 혹은 삭제를 하여로 오류를 던지지 않습니다.
// Java code to illustrate
// Fail Safe Iterator in Java
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Iterator;
class FailSafe {
public static void main(String args[])
{
CopyOnWriteArrayList<Integer> list
= new CopyOnWriteArrayList<Integer>(new Integer[] { 1, 3, 5, 8 });
Iterator itr = list.iterator();
while (itr.hasNext()) {
Integer no = (Integer)itr.next();
System.out.println(no);
if (no == 8)
// This will not print,
// hence it has created separate copy
list.add(14);
}
}
}
2. 동기화를 보장하는 컬렉션 사용
동기화를 보장하는 컬렉션을 사용하면 해결 할 수 있습니다. 예를들어, ConcurentHashMap 컬렉션을 사용하면 fail-safe하기 때문에 멀티쓰레드 환경에서 안정으로 동작하며 오류를 던지지 않습니다. 주기적으로 동기화되고 특정한 시점의 상태만 반영하기 때문에 최근에 변경된 내용은 저장되지 않을 수도 있습니다.
// Java program to illustrate
// Fail-Safe Iterator which
// does not create separate copy
import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;
public class FailSafeItr {
public static void main(String[] args)
{
// Creating a ConcurrentHashMap
ConcurrentHashMap<String, Integer> map
= new ConcurrentHashMap<String, Integer>();
map.put("ONE", 1);
map.put("TWO", 2);
map.put("THREE", 3);
map.put("FOUR", 4);
// Getting an Iterator from map
Iterator it = map.keySet().iterator();
while (it.hasNext()) {
String key = (String)it.next();
System.out.println(key + " : " + map.get(key));
// This will reflect in iterator.
// Hence, it has not created separate copy
map.put("SEVEN", 7);
}
}
}
- 컬렉션의 remove()와 순환자(Iterator) remove()의 차이점은 무엇인가요?
- 순환(iteration) 중에 요소를 삭제하려면 어떤 것을 사용해야 하나요?
컬렉션 인터페이스 remove(Object obj) 는 컬렉션에 있는 객체를 제거합니다. 리스트 인터페이스 remove(int index)는 특별 인덱스에 있는 객체를 제거합니다. 위 2개의 메서드는 순환 중(iterating)이 아닌 컬렉션에 있는 객체를 제거합니다.
순환에서는 다릅니다. 만약 모든 요소를 순회하다가 하나의 요소를 제거한다고 가정해 보겠습니다, 이 때 Iterator의 remove()를 사용합니다. 이 때는 순환자(Iterator)의 관점에서 요소를 제거합니다.
순환(iteration) 도중 컬렉션이나 리스트의 remove() 를 사용하면, CoccurentModificationException을 던집니다. 따라서 컬렉션 요소를 제거할 때는 순환자(Iterator) remove() 를 사용해야 합니다.
참고
https://stackoverflow.com/questions/33351946/why-iterators-are-called-fail-safe-or-fail-fast
https://www.geeksforgeeks.org/fail-fast-fail-safe-iterators-java/
'학습' 카테고리의 다른 글
Hash 충돌 회피 알고리즘 (0) | 2020.07.24 |
---|---|
Iterator & Enumeration & ListIterator (0) | 2020.07.23 |
ArrayList & Vector 차이점 (0) | 2020.07.23 |
Compile vs Interpretation (0) | 2020.07.23 |
다형성 / Up-casting & Down-casting (0) | 2020.07.20 |