fail-fast
fail-fast 란?
간단히 말하면 처리 과정에서 실패할 가능성이 보인다면 즉시 중지하고 상위 인터페이스에 보고하여 조기에 오류를 처리하는 동작 방식이다.
위키피디아에서는 다음과 같이 설명한다.
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.
wikipedia - fail-fast
반대되는 개념으로 fail-safe라고 표현하는 경우는 있지만 드문 것 같다. 실패할 가능성이 보이는 조건이 나타나도 무시하고 계속 진행한다.
Iterator
와 fail-fast
자바에서 제공하는 여러 컨테이너 클래스들은 Iterator
클래스를 이용한 순회를 제공한다. 덕분에 foreach문(Enhanced for statement)을 사용하여 깔끔하고 간편하게 원소를 순회할 수 있다./* Iterator를 이용한 원소 순회 */
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
// e로 무언가를 한다.
}
/* foreach문 */ |
Iterable
인터페이스는 이러한 Iterator
구현체를 반환하는 것을 강제하고, Collection
클래스는 Iterable
인터페이스를 구현하였다.
ArrayList
, Vector
, HashMap
등의 컨테이너들이 fail-fast 방식으로 동작한다.
순회 도중 컨테이너 내부의 원소들이 변경되면 ConcurrentModificationException
를 던진다. 새롭게 추가되거나 삭제된 원소들로 인해 순회 결과가 원하는 값이 나오지 않을 수 있기 때문이다. 때문에, fail-fast하게 동작한다고 말할 수 있다.
##
ArrayList
의 fail-fast
대표적인 컨테이너 클래스인 ArrayList
를 살펴보자.
ArrayList
는 modCount
라는 변수를 통해 리스트 내 변화 횟수를 저장하고 관리한다. Iterator
생성 시, 당시 modCount
값을 따로 저장하고 순회하면서 변경이 감지되면 ConcurrentModificationException
를 던진다.
다음은 ArrayList
의 Iterator
내부 구현체 중 일부이다.
int expectedModCount = modCount; // Iterator 생성 당시 modCount값 저장 |
Iterator
에서 원소 삭제
그렇다고 Iterator
순회 시, 원소의 변경이 아예 불가능한 것은 아니다. Iterator
의 remove
메서드를 이용하여 순회 도중 원소를 삭제할 수 있다./* Iterator 순회 중 원소 삭제 */
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
i.remove(); // 삭제
}
단, 순회 당시 Iterator
가 가르키고 있는 원소가 있어야한다(반드시 next
메서드 다음에 호출되어야 함). 이렇게 Iterator
의 관리 하에 안전하게 원소를 삭제할 수 있다.
Iterator
vs Enumeration
바로 자바2 이전에는 Iterator
와 같은 역할은 하던 Enumeration
가 존재했다. 자바2부터 Iterator
이 만들어졌는데, 이때부터 fail-fast 방식으로 구현된 컬렉션 뷰 객체가 등장했다.
for (Enumeration<Element> en = c.elements(); en.hasMoreElements(); ) { |
- 일부에서는 “
Iterator
는 fail-fast방식이고,Enumeration
은 그렇지 않다.” 라고 말하지만 정확한 표현은 아닌 듯 하다.Iterator
를 구현했지만 fail-fast하게 동작하지 않는 클래스도 존재한다 (ex.ConcurrentHashMap
의KeySetView
의KeyIterator
)- 강제된 규약은 아니지만, 대체로 따르는 것 같다.
Iterator
인터페이스 자체의 특성이라기 보다는 그걸 구현한 컬렉션 뷰 클래스의 특성이라고 보는게 맞을 듯 하다. Enumeration
도 마찬가지
공식 문서에서는 remove
메서드가 추가된 점, 더 메서드명이 더 명료한 점 때문에 Iterator
사용을 권장한다.
다음은 Vector
클래스에서 반환되는 Enumeration
객체 내부 구현 일부이다./* Vector에서 반환되는 Enumeration 객체 내부 구현 일부 */
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
보다시피 fail-fast하게 동작하지 않고 ConcurrentModificationException
를 던지지도 않는다.
예제
Vector
의 Iterator
와 Enumeration
Iterator
동작Vector<String> vector = new Vector<>();
vector.add("aa");
vector.add("bb");
vector.add("cc");
for (Iterator<String> i = vector.iterator(); i.hasNext(); ) {
String s = i.next(); // ConcurrentModificationException 발생
System.out.println(s);
if (s.equals("bb")) {
vector.add("dd"); // 내용 변경, 다음 순회에서 예외 발생
}
}결과
aa
bb
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
at java.util.Vector$Itr.next(Vector.java:1137)
at workspace.Main.main(Main.java:xx)Enumeration
동작Vector<String> vector = new Vector<>();
vector.add("aa");
vector.add("bb");
vector.add("cc");
for (Enumeration<String> e = vector.elements(); e.hasMoreElements(); ) {
String s = e.nextElement();
if (s.equals("bb")) {
vector.add("dd"); // 내용 변경, 순회 크기 늘어남
}
}
for (String s : vector) {
System.out.println(s);
}결과
aa
bb
cc
dd
HashMap
과 ConcurrentHashMap
HashMap
의Iterator
동작Map<String, String> hashMap = new HashMap<>();
hashMap.put("a", "AA");
hashMap.put("b", "BB");
hashMap.put("c", "CC");
for (String key : hashMap.keySet()) {
System.out.println(key + ", " + hashMap.get(key));
hashMap.remove("a"); // ConcurrentModificationException 발생
// hashMap.put("d", "DD"); // 마찬가지로 ConcurrentModificationException 발생
}결과
a, AA
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
at workspace.Main.main(Main.java:xx)ConcurrentHashMap
의Iterator
동작ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("a", "AA");
concurrentHashMap.put("b", "BB");
concurrentHashMap.put("c", "CC");
for (String key : concurrentHashMap.keySet()) {
System.out.println(key + ", " + concurrentHashMap.get(key));
if (key.equals("a")) {
concurrentHashMap.remove("b"); // 원소 삭제
concurrentHashMap.put("d", "DD"); // 원소 추가
}
}
System.out.println("---");
for (String key : concurrentHashMap.keySet()) {
System.out.println(key + ", " + concurrentHashMap.get(key));
}결과
a, AA
b, null
c, CC
d, DD
---
a, AA
c, CC
d, DD
reference
- Effective Java 3/E item 58
- Fail Fast Vs Fail Safe Iterator In Java : Java Developer Interview Questions
- Fail-fast Iterator에 대한 멀티쓰레드 무결성 해결방법
- Fail-Safe Iterator vs Fail-Fast Iterator
- Oracle java8 document