java.util.ConcurrentModificationException 원인과 해결
Map 에서 특정 키에 대한 값을 제거하는 로직을 구현하는 경우가 있었다.
그 과정에서 java.util.ConcurrentModificationException 에러를 만나게 되었다. 그래서 원인과 해결한 과정을 정리해보려고 한다.
문제 상황
Map<Long, String> map = new HashMap<>();
map.put(1L, "item1");
map.put(2L, "item2");
Set<Long> keySet = map.keySet();
for (Long key : keySet) {
if (key == 1L) map.remove(key);
}
map.entrySet().stream().forEach(System.out::println);
Exception in thread "main" java.util.ConcurrentModificationException
위의 코드가 오류가 발생한 코드이다.
원인 및 분석 과정
java.util.ConcurrentModificationException 은 Collection 에서 순회 중 수정이 일어났을 때 발생한다.
fast-fail iterator 가 Collection 을 순회하면서 초기와 달라졌다고 판단되면 에러를 바로 던진다. fail-fast 방식은 빠르고 리스크없이 오류가 있을 법한 상황을 남겨두지 않는 것을 목표로 한다.
하지만, 위의 코드는 map 의 keySet 이라는 컬렉션A 를 돌면서 map 이라는 컬레션 B 를 처리하고 있다. 서로 다른 컬렉션을 조회하고 수정하는 데 이런 에러가 발생했다는 사실이 이해가 가지 않았다.
그렇다면 한 가지 가정을 세워볼 수 있다.
keySet 이 map 의 key 값을 따로 가지고 있는 것이 아니라, map 의 레퍼런스 정보를 통해 key 값을 가져오고 있는 것은 아닐까?
가설을 확인하기 위해 keySet 의 컬렉션의 내용을 변경시키면 본 컬렉션인 map 컬렉션에 반영이 되는 지 보기로 했다.
Map<Long, String> map = new HashMap<>();
map.put(1L, "item1");
map.put(2L, "item2");
Set<Long> keySet = map.keySet();
keySet.remove(1L);
map.entrySet().stream().forEach(System.out::println);
map 에 1=item1, 2=item2 데이터를 넣고 map 의 keySet 을 가져와 1 을 제거했다. 그리고 map 을 전체 순회하여 내용을 확인했다.
2=item2
결과는 map 에서 key 1 에 대한 내용이 사라졌다.
Map.keySet() 은 단순히 Map 의 키값을 복사해와서 새로운 컬렉션을 만드는 것이 아니라 Map 에 대한 레퍼런스 정보를 가져와서 키를 보여준다는 것을 알 수 있다.
그래서, keySet 을 돌면서 Map 을 삭제하면 keySet 에도 삭제가 되어 java.util.ConcurrentModificationException 에러가 발생했던 것이다.
오라클 문서에서도 아래와 같이 설명이 되어있었다.
- Returns a Set view of the keys contained in this map. The set is backed by the map, so changes to the map are reflected in the set, and vice-versa. If the map is modified while an iteration over the set is in progress (except through the iterator's own remove operation), the results of the iteration are undefined. The set supports element removal, which removes the corresponding mapping from the map, via the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations. It does not support the add or addAll operations.
아래과 같이 간단하게 번역할 수 있다.
Map.keySet() 은 Map 의 키에 대한 Set 형태의 view 를 제공한다. Map.keySet() 이 삭제에 대한 변화는 Map 에도 반영이 되고, 그 반대도 마찬가지이다.
해결 방안
에러를 해결하기 위해서는 iterator.remove() 함수로 직접 삭제하는 방법이다. 이렇게 삭제하면 에러를 던지지 않는다.
Map<Long, String> map = new HashMap<>();
map.put(1L, "item1");
map.put(2L, "item2");
Set<Long> keySet = map.keySet();
Iterator<Long> iterator = keySet.iterator();
while (iterator.hasNext()) {
Long key = iterator.next();
if (key == 1L) iterator.remove();
}
map.entrySet().stream().forEach(System.out::println);
2=item2
원본 map 에서 직접 삭제하는 것이 아닌 또 다른 map 을 만들어 처리하는 것도 방법이 될 수 있다.
Map<Long, String> map = new HashMap<>();
map.put(1L, "item1");
map.put(2L, "item2");
Set<Long> keySet = map.keySet();
// solution1
Map<Long, String> copiedMap = new HashMap<>(map);
for (Long key : keySet) {
if (key == 1L) copiedMap.remove(key);
}
// solution2
Map<Long, String> copiedMap = new HashMap<>();
for (Long key : keySet) {
if (key == 1L) continue;
copiedMap.put(key, map.get(key));
}
copiedMap.entrySet().stream().forEach(System.out::println);
2=item2
ref