Java

Collection Test, Fail Fast, Fail Safe...

체리필터 2021. 7. 2. 09:21
728x90
반응형

오늘은 기본적인 Collection 들에 대해 간단한 테스트를 해 볼 예정이다.

그동안 아무런 생각없이 사용해 왔는데 어떻게 다른지에 대해 테스트 코드를 통해 알아보자.

우선 간단한 ArrayList를 while 문으로 돌려보자.

    @Test
    public void loopTest() {
        Collection<String> collection = new ArrayList<>();
        collection.add("1");
        collection.add("2");
        collection.add("3");
        collection.add("4");
        collection.add("5");

        Iterator iterator = collection.iterator();

        int i = 0;
        while (iterator.hasNext()) {
            log.debug("value is {}", iterator.next());
        }
    }

우리가 흔히 볼 수 있는 ArrayList Collection을 가지고 다음 요소가 있으면 다음 요소를 출력하는 모습이다. 실행 결과는 다음과 같다.

08:59:54.318 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
08:59:54.322 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
08:59:54.322 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
08:59:54.322 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
08:59:54.322 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5

그런데 저 while 문 안의 next를 두 번 호출하면 어떤 모습일까? 실행 하면 아래와 같은 오류가 발생한다.

09:01:55.529 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:01:55.535 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:01:55.535 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
09:01:55.535 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
09:01:55.535 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5

java.util.NoSuchElementException
	at java.util.ArrayList$Itr.next(ArrayList.java:864)
	at com.example.demo.collection.CollectionTest.loopTest2(CollectionTest.java:53)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

next를 두 번 호출하였기 때문에 loop문이 한 번 돌 때 1, 2 그리고 다음에 3, 4 다음에 5, 6 식으로 찍히게 되는데 ArrayList에 6이 없으므로 그런 Element가 없다면서 NoSuchElementException을 발생 시킨다.

이제 next가 어떻게 동작하는지 알게 되었다.

그렇다면 log를 여러 군데에서 찍고 싶다면 어떻게 해야 할까? 다음 처럼 변수에 받아 사용하면 된다.

        while (iterator.hasNext()) {
            String element = (String) iterator.next();

            log.debug("value is {}", element);
            log.debug("value is {}", element);
        }

위와 같이 수정한 상태에서 실행을 하면 아래와 같이 나오게 된다.

09:04:53.927 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5
09:04:53.930 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5

모든 값들이 두 번씩 찍히는 것을 볼 수 있다.

자 이제 다음에는 loop를 돌면서 특정 Element를 삭제 하고 싶을 경우에는 어떻게 할까? 이를 위해서 우리가 알아야 하는 개념은 fail-fast, fail-safe 이다. 이와 관련한 개념은 아래 링크에서 확인해 볼 수 있다.

https://simuing.tistory.com/entry/JAVA-Fail-Safe-Iterator-vs-Fail-Fast-Iterator

 

JAVA_ Fail-Safe Iterator vs Fail-Fast Iterator

Fail-Safe Iterator vs Fail-Fast Iterator Fail-Fast systems은 가능한 빨리 실패를 노출하고 전체 작업을 중지하여 작업을 중단합니다. 반면 Fail-Safe systems은 장애 발생시 작업을 중단하..

simuing.tistory.com

이를 확인해 보기 위해 코드를 다음과 같이 수정해 보자.

    @Test
    public void loopTest() {
        Collection<String> collection = new ArrayList<>();
        collection.add("1");
        collection.add("2");
        collection.add("3");
        collection.add("4");
        collection.add("5");

        Iterator iterator = collection.iterator();

        int i = 0;
        while (iterator.hasNext()) {
            String element = (String) iterator.next();
            i++;
            if (i == 3) {
                iterator.remove();
//                collection.remove(element);
            }

            log.debug("value is {}", element);
        }

        log.debug("collection is {}", collection);
    }

결과값은 어떻게 나올까? 아래와 같이 나온다.

09:08:11.369 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:08:11.373 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:08:11.373 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
09:08:11.373 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
09:08:11.373 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5
09:08:11.373 [main] DEBUG com.example.demo.collection.CollectionTest - collection is [1, 2, 4, 5]

 

반응형

위 참고 링크에서 확인할 수 있는 것 처럼 iterator는 Collection의 복제본을 사용하기 때문에 loop를 돌면서 element를 삭제해도 Exception이 발생하지 않게 된다.

그리고 중간 중간 loop 안에서 로그를 찍어도 이상없이 모든 원소가 다 찍히게 된다.

그리고 loop를 빠져 나와 마지막에 collection을 찍어보면 3이 빠져 있는 것을 볼 수 있다.

도대체 iterator가 어떻게 만들어 졌기 때문일까? iterator를 따라 들어가 보면 Collection.java의 iterator가 나오는데 Collection 자체가 interface라서 우리는 ArrayList에 구현된 것을 따라 들어가 봐야 한다.

ArrayList에 구현된 iterator는 다음과 같다.

    /**
     * Returns an iterator over the elements in this list in proper sequence.
     *
     * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
     *
     * @return an iterator over the elements in this list in proper sequence
     */
    public Iterator<E> iterator() {
        return new Itr();
    }

그렇다. 새로운 Itr 객체를 생성해서 리턴하게 되어 있다. 그리고 Itr 객체는 ArrayList 안에 만들어진 클래스이다.

Itr 클래스는 Iterator를 구현하고 있습니다.

따라서 원본인 Collection이 삭제 된다 하더라도 복제본인 iterator는 loop를 돌면서 원소가 삭제 되어도 Exception이 발생하지 않은 것이다. 즉 fail-safe로 동작하게 된 것이다.

그렇다면 반대로 collection을 삭제하게 되면 어떻게 될까? 소스를 다음과 같이 고쳐 보자.

            if (i == 3) {
//                iterator.remove();
                collection.remove(element);
            }

그러고 나서 실행을 해 보면 아래와 같이 Exception이 발생하는 것을 볼 수 있다.

09:16:15.982 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:16:15.986 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:16:15.986 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at com.example.demo.collection.CollectionTest.loopTest(CollectionTest.java:27)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

원본인 3번째 원소가 삭제 된 후 loop를 돌아서 다음 원소에 접근하려 하면 카운트 값이 달라 ConcurrentModificationException 이 발생하게 된다. 즉 위 참조 링크에서 아래와 같이 설명하는 바와 같다.

따라서 이는 fail-fast 로 동작한다는 것을 알게 되었다.

이러한 동시성 이슈를 해결하기 위해 나온 것이 java.util.concurrent 패키지 아래 있는 것들이다. 위의 소스에서 Collection을 선언하는 부분을 아래와 같이 고친 후 다시 실행해 보면 다음과 같이 나온다.

concurrent package 안에 있는 CopyOnWriteArrayList를 사용해 선언

09:25:00.428 [main] DEBUG com.example.demo.collection.CollectionTest - value is 1
09:25:00.432 [main] DEBUG com.example.demo.collection.CollectionTest - value is 2
09:25:00.432 [main] DEBUG com.example.demo.collection.CollectionTest - value is 3
09:25:00.432 [main] DEBUG com.example.demo.collection.CollectionTest - value is 4
09:25:00.432 [main] DEBUG com.example.demo.collection.CollectionTest - value is 5
09:25:00.432 [main] DEBUG com.example.demo.collection.CollectionTest - collection is [1, 2, 4, 5]

 

마치 iterator 처럼 동작하는 것을 볼 수 있다. 따라서 concurrent package 안에 있는 것들은 fail-safe 하게 돌아 간다는 것을 확인할 수 있다.

 

참고로 Collection을 loop 하는 방법은 아래를 참고할 수 있다.

https://knowm.org/iterating-through-a-collection-in-java/

 

Iterating through a Collection in Java – Knowm.org

There are three common ways to iterate through a Collection in Java using either while(), for() or for-each(). While each technique will produce more or less the same results, the for-each construct is the most elegant and easy to read and write. It doesn

knowm.org

 

728x90
반응형

'Java' 카테고리의 다른 글

Public Interface의 품질에 영향을 미치는 요소...  (0) 2021.07.01
Reflection 사용2  (0) 2021.06.29
Reflection 사용1  (0) 2021.06.25
Reflection  (0) 2021.06.25
Java Random 함수의 동작 원리  (0) 2021.05.14