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문 */
for (Element e : elements) { // elements는 배열 또는 Iterable을 구현한 클래스
// e로 무언가를 한다.
}

Iterable 인터페이스는 이러한 Iterator 구현체를 반환하는 것을 강제하고, Collection 클래스는 Iterable 인터페이스를 구현하였다.

ArrayList, Vector, HashMap 등의 컨테이너들이 fail-fast 방식으로 동작한다.
순회 도중 컨테이너 내부의 원소들이 변경되면 ConcurrentModificationException를 던진다. 새롭게 추가되거나 삭제된 원소들로 인해 순회 결과가 원하는 값이 나오지 않을 수 있기 때문이다. 때문에, fail-fast하게 동작한다고 말할 수 있다.

##

ArrayList의 fail-fast

대표적인 컨테이너 클래스인 ArrayList를 살펴보자.

ArrayListmodCount라는 변수를 통해 리스트 내 변화 횟수를 저장하고 관리한다. Iterator 생성 시, 당시 modCount 값을 따로 저장하고 순회하면서 변경이 감지되면 ConcurrentModificationException를 던진다.

다음은 ArrayListIterator 내부 구현체 중 일부이다.

int expectedModCount = modCount; // Iterator 생성 당시 modCount값 저장

public E next() {
checkForComodification(); // modCount 체크
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); // 예외 발생
}

Iterator에서 원소 삭제

그렇다고 Iterator 순회 시, 원소의 변경이 아예 불가능한 것은 아니다. Iteratorremove메서드를 이용하여 순회 도중 원소를 삭제할 수 있다.

/* 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(); ) {
Element e = en.nextElement()
// e로 무언가를 한다.
}
  • 일부에서는 Iterator는 fail-fast방식이고, Enumeration은 그렇지 않다.” 라고 말하지만 정확한 표현은 아닌 듯 하다.
    • Iterator를 구현했지만 fail-fast하게 동작하지 않는 클래스도 존재한다 (ex. ConcurrentHashMapKeySetViewKeyIterator)
    • 강제된 규약은 아니지만, 대체로 따르는 것 같다. 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를 던지지도 않는다.

예제

VectorIteratorEnumeration

  • 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

HashMapConcurrentHashMap

  • HashMapIterator 동작

    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)
  • ConcurrentHashMapIterator 동작

    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