개발 공부/Java

이펙티브 자바 아이템 21 - 인터페이스는 구현하는 쪽을 생각해 설계하라 - 완벽 공략

개발인생 2022. 11. 29. 13:55
반응형

아이템 21 - 인터페이스는 구현하는 쪽을 생각해 설계하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

ConcurrentModificationException

ConcurrentModificationException 은 컬렉션을 다룰 때 주로 발생하는 예외이다.

컬렉션을 순회하다 어떤 엘리먼트를 삭제하고 싶을 때 주로 발생할 수 있다.

ConcurrentModificationException 는 컬렉션에 국한된 예외는 아니다.

ConcurrentModificationException 은 현재 객체의 상태가 바뀌면 안되는 것을 수정 할 때 발생하는 예외이다.

어떤 스레드에서 컬렉션을 순회하는 도중에 다른 스레드가 해당 컬렉션을 변경하려고 한다면 순회한 결과가 예측이 불가능해진다.

그래서 이런 상황이 감지가되면 ConcurrentModificationException 예외를 던지도록한다.

컬렉션 순회를 완료하고나서 예외를 던지는 것이 아니라

변경이 감지되면 바로 예외를 던지게하는 것을 fail-fast 라고 하고 이러한 기능을 제공하는 이터레이터를 fail-fast 이터레이터 라고한다.

이런 변경에 대한 문제는 멀티 스레드 환경에서만 발생하는 것은 아니다.

ConcurrentModificationException 은 싱글 스레드에서도 발생할 수 있는 예외이다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers1 = List.of(1, 2, 3, 4, 5);

        List<Integer> numbers2 = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);
    }
}

위의 코드에서 List.of 를 이용해 만든 list 와 new ArrayList 를 사용해 만든 list 는 서로 다르다.

List.of 를 이용해 만든 컬렉션은 수정할 수 없다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        // 예외 발생 - 수정하는 오퍼레이터가 없다.
        for (Integer number : numbers) {
            if (number == 3) {
                numbers.remove(number);
            }
        }
    }
}

수정을 하려고하면 수정할 수 있는 오퍼레이션을 지원하지 않는다는 에러가 발생한다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);
    }
}

리스트를 변경하고 싶다면 new ArrayList 를 사용해 컬렉션을 생성해야한다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

       // 이터레이터로 콜렉션을 순회하는 중에 Collection의 remove를 사용한다면...
        for (Integer number : numbers) {
            if (number == 3) {
                numbers.remove(number);
            }
        }
    }
}

이터레이터를 사용해 컬렉션을 순회하는 중에 remove 메서드를 통해 컬렉션을 변경하려고하면 ConcurrentModificationException 이 발생한다.

컬렉션을 순회하면서 변경을 하기 위해서는 다른 방법을 사용해야한다.

위의 상황은 이터레이터가 fail-fast 이터레이터였고, 해당 이터레이터로 순회하는 중에 컬렉션을 변경 하려고 했기 때문에 발생한 상황이다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

       // 이터레이터의 remove 사용하기
        for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
            Integer integer = iterator.next();
            if(integer == 3) {
                iterator.remove();
            }
        }
    }
}

Iterator 를 직접 사용하면서 Iterator 를 통해 컬렉션을 변경하면

해당 컬렉션의 변경을 Iterator 가 주도하기 때문에 안전하게 실행이 된다.

Iterator 통해 remove 를 하는 것과 Collection 을 통해 remove 를 하는 차이이다.

다시말해 Iterator 가 순회하는 중에 Collection 을 통해 remove 를 하면 ConcurrentModificationException 이 발생하고,

Iterator 통해 remove 를 하면 ConcurrentModificationException 예외가 발생하지 않는다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

//        // 인덱스 사용하기
        for (int i = 0; i < numbers.size() ; i++) {
            if (numbers.get(i) == 3) {
                numbers.remove(numbers.get(i));
            }
        }
    }
}

다른 방법으로는 인덱스 를 사용하는 것이다.

Iterator 자체를 사용하지 않기 때문에 ConcurrentModificationException 예외가 발생하지 않는다.

public class FailFast {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

       // removeIf 사용하기
        numbers.removeIf(number -> number == 3);
    }
}

다른 방법으로는 removeIf 를 사용하는 것이다.

removeIf 는 내부적으로 Iteratorremove 를 사용하는 것과 같다.

반응형