주요글: 도커 시작하기
반응형
콜렉션 프레임워크에서 fail-fast iterator에 대한 멀티쓰레드 무결성 해결 방법에 대해 알아본다.

자바 콜렉션 프레임워크와 콜렉션 뷰

자바 콜렉션 프레임워크에서 콜렉션 뷰 메소드인 iterator() 또는 listIterator() 메소드로 반환되는 Iterator 객체는 fail-fast 방식이다. 웹 서비스와 같은 콜렉션의 변경이 빈번한 경우에 적용 가능한 멀티쓰레드 무결성 해결방법인 스냅샷 기법과 synchronization 방법을 소개할 것이다.

콜렉션 프레임워크

자바 2 플랫폼에서는 객체를 저장하고 정리하기 위한 방법으로 자바 콜렉션 프레임워크를 사용한다. 콜렉션 프레임워크는 java.util 패키지에 포함되어 있으며, 콜렉션 계열, 맵 계열, 콜렉션 뷰, 콜렉션 유틸리티 등을 포함한다. 콜렉션 계열에는 Collection, Set, SortedSet, List 등의 인터페이스와 ArrayList, LinkedList, HashSet, TreeSet 등의 클래스 등이 포함되어 있다. 맵 계열에는 Map, SortedMap 등의 인터페이스와 HashMap, TreeMap 등의 클래스가 포함되어 있다. 콜렉션 뷰에는 Iterator, ListIterator 인터페이스 등이 포함되어 있으며, 콜렉션 유틸리티에는 Collections 클래스 등이 포함되어 있다.

자바 2 이전에 같은 목적으로 사용되어 오던 Vector, Hashtable 등의 클래스도 Collection, Map 인터페이스를 상속함으로써 자바 콜렉션 프레임워크에 통합되었고, Enumeration 인터페이스 대신 향상된 Iterator 인터페이스를 사용할 것을 권장하고 있다.

콜렉션 뷰

콜렉션 프레임워크의 클래스들은 객체를 저장하고 정리하기 위해 사용된다. 콜렉션 클래스들 내에 저장된 객체들을 차례로 접근하기 위한 방법을 콜렉션 뷰라고 한다. 자바 2 이전 버전에서 사용되던 Vector, Hashtable의 뷰 객체는 Enumeration 객체이며, 자바 2의 콜렉션 프레임워크에서 콜렉션 뷰는 Iterator와 ListIterator 객체이다.

Vector 클래스의 객체인 v 객체의 모든 요소(저장된 객체)들을 Enumeration 뷰를 이용하여 프린트하려면 다음과 같다.

for (Enumeration e = v.elements() ; e.hasMoreElements() ;) {
    System.out.println(e.nextElement());
}

같은 Vector 클래스의 객체 v 객체를 Iterator 뷰를 이용하여 모든 요소를 프린트하려면 다음과 같다.

for (Iterator it = v.iterator() ; it.hasNext() ;) {
    System.out.println(it.next());
}

Iterator 객체가 Enumeration 객체와 다른 점은 Iterator 객체는 콜렉션에 대하여 remove() 메소드를 제공한다는 점이다. 또한 메소드의 이름이 훨씬 명시적이다. 그리고, 자바 2 버전에서는 Enumeration 보다는 잘 정의된 콜렉션 뷰 객체인 Iterator를 사용할 것을 권하고 있다.

Fail-fast

그러나, Enumeration 객체와 Iterator 객체의 뷰 방식에는 또 다른 차이점이 있다. 자바 2 이전 버전에서 사용되던 Vector, Hashtable의 뷰 객체인 Enumeration은 fail-fast 방식이 아니었으나, 자바 2의 콜렉션 프레임워크에서 콜렉션 뷰인 Iterator, ListIterator 객체는 fail-fast 방식이라는 점이다.

콜렉션 뷰는 콜렉션 객체에 저장된 객체들에 대한 순차적 접근을 제공한다. 그러나, 뷰 객체인 Enumeration 또는 Iterator 객체를 얻고 나서 순차적 접근이 끝나기 전에 뷰 객체를 얻은 하부 콜렉션 객체에 변경이 일어날 경우, 순차적 접근에 실패하게 된다. 여기서 변경이라는 것은 콜렉션에 객체가 추가되거나 제거되는 것과 같이 콜렉션 구조의 변경이 일어나는 경우를 말한다.

이런 상황은 멀티쓰레드 구조와 이벤트 구동 모델에서 일어날 수 있으며, 개발자가 혼자 테스트할 경우 발견하기 어려운 문제이다. 따라서 정확한 이해와 예상이 필요하며, 이에 대한 대처 방안을 마련해야 한다.

하부 콜렉션 객체에 변경이 일어나 순차적 접근에 실패하면 Enumeration 객체는 실패를 무시하고 순차적 접근을 끝까지 제공한다. Iterator 객체는 하부 콜렉션 객체에 변경이 일어나 순차적 접근에 실패하면 ConcurrentModificationException 예외를 발생한다. 이처럼 순차적 접근에 실패하면 예외를 발생하도록 되어 있는 방식을 fail-fast라고 한다.

Iterator는 fail-fast 방식으로 하부 콜렉션에 변경이 발생했을 경우, 신속하고 결함이 없는 상태를 만들기 위해 Iterator의 탐색을 실패한 것으로 하여 예외를 발생하며, 이렇게 함으로써 안전하지 않을지도 모르는 행위를 수행하는 위험을 막는다. 왜냐하면 이러한 위험은 실행 중 불특정한 시간에 멋대로 결정되지 않은 행위를 할 가능성이 있기 때문에 안전하지 않다고 할 수 있기 때문이다.

스냅샷

이처럼 콜렉션 뷰 객체인 Iterator와 ListIterator 객체가 fail-fast 방식인 이유는 스냅샷에 대한 보장을 포함하고 있지 않기 때문이다. 즉, 콜렉션의 뷰인 Iterator 객체를 얻었을 때 그 순간의 상태를 따로 생성하지 않기 때문에, Iterator 객체에서 순차적으로 하부 콜렉션 객체를 접근할 때, 콜렉션 객체의 변경에 영향을 받게 된다.

따라서, 콜렉션 뷰인 Iterator를 통하여 순차적 접근을 행하는 도중에 하부 콜렉션 객체에 변경이 일어날 가능성이 있는 경우 스냅샷을 명시적으로 생성하는 것이 좋다. ArrayList를 이용한 스냅샷을 생성하는 간단한 방법은 다음과 같다.

public Iterator snapshotIterator(Collection collection) {
    return new ArrayList(collection).iterator();
}

이렇게 스냅샷을 이용하면 스냅샷이 생성된 순간의 상태에 대하여 Iterator를 통한 콜렉션의 뷰를 생성하게 된다. 하부 콜렉션의 변경이 일어나도 스냅샷에는 반영되지 않으며 Iterator는 실패없이 실행된다.

콜렉션 계열 중에 ArrayList를 사용하는 이유는 다른 콜렉션 계열 클래스들보다 생성과 순차적 접근이 빠르기 때문이다. ArrayList가 삽입과 삭제에 불리한 단점이 있으나 이 경우에는 삽입과 삭제가 없으므로 고려되지 않는다.

동기화

콜렉션 프레임워크의 클래스들은 동기화를 보장하고 있지 않다. 즉 멀티쓰레드에서 콜렉션 구현 객체에 동시에 추가, 삭제를 위한 접근이 행해진다면, 그러한 행위가 발생하는 부분에 동기화를 명시적으로 해주어야 한다.

다음은 콜렉션 구현 객체에 동기화를 명시적으로 제공하는 간단한 방법을 보여준다. 먼저 콜렉션 구현 객체를 Collections 유틸리티 클래스를 이용하여 동기화된 콜렉션 구현 객체로 만든다.

Collection c = Collections.synchronizedCollection(myCollection);

이 과정을 다음처럼 콜렉션 구현 객체를 생성할 때 실행하면 효과적이다.

Set c = Collections.synchronizedSet(new HashSet(...));

SortedSet c = Collections.synchronizedSortedSet(new TreeSet(...));

List c = Collections.synchronizedList(new ArrayList(...));

Map c = Collections.synchronizedMap(new HashMap());

SortedMap c = Collections.synchronizedSortedMap(new HashSortedMap());

이렇게 생성된 콜렉션 구현 객체는 쓰레드에 안전하므로 스냅샷을 만들지 않고 synchronized 블럭을 이용하여 콜렉션 뷰의 실패를 막을 수 있다.

synchronized(c) {
    for (Iterator it = c.iterator() ; it.hasNext() ;) {
        System.out.println(it.next());
    }
}

고려할 점

어떤 방법을 사용할 것인가는 콜렉션에 대한 접근이 이루어지는 상황에 대한 예측과 접근 회수를 근거로 결정하게 된다. 어떤 경우에도 동기화가 가장 확실한 방법을 제공한다.

그러나, 웹 서비스와 같이 접근회수가 아주 많은 경우 실행 속도와 성능 향상이라는 문제를 무시할 수 없다. 콜렉션 구현 객체에 대하여 읽기 전용 접근과 변경을 위한 접근이 얼마나 자주 발생하는지, 또한 콜렉션 뷰에 대한 생성과 접근이 얼마나 빈번한지, 그리고 콜렉션 객체에 저장되는 객체의 수는 평균 얼마나 되는지 등을 고려하여야 한다.

만약 읽기 전용 접근이 빈번한 경우 성능 향상을 위하여 동기화보다는 스냅샷과 같은 다른 방법이 유용하다. 또한 변경을 위한 접근이 드물 경우 immutable 패턴을 적용하는 방법도 유용하다. (immutable 패턴에 대한 내용은 자바캔의 강좌 게시판에 박경선님이 올리실 글을 참고하기 바란다.) 스냅샷 생성이 빈번한 경우 프록시 패턴을 적용하는 것이 좋다.

이러한 기법의 적용은 항상 무결성에 대한 보장이 필요하며, 성능 향상을 고려하여 결정하는 것이 좋다. 웹 서비스 등과 같이 실행 속도가 중요한 경우, 예측되는 상황에 따른 적절한 프로그래밍 패턴을 적용하는 것이 좋다.

결론

콜렉션 프레임워크에서 Fail-fast Iterator에 대한 멀티쓰레드 무결성 해결 방법을 알아보았다. 멀티쓰레드 무결성은 개발시 테스트하기 힘든 항목 중 하나이며, 무결성이 보장되지 않으면 불특정한 시간에 예측하지 못한 결과를 일으키므로 원인을 추적하기 어렵다. 따라서 콜렉션 프레임워크에서 iteration에 대한 멀티쓰레드 무결성을 위하여 동기화와 스냅샷을 적용하는 것이 필요하다.

관련링크:

본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts