문제 원인
Kotlin에서 mutableMapOf 함수를 통해 생성되는 MutableMap 객체는 내부적으로 Java의 LinkedHashMap을 사용한다. 이 LinkedHashMap은 내부에서 LinkedHashIterator이란 것을 사용하는데, 이 객체는 원소를 순환할 때 LinkedHashMap이 실제로 조작된 횟수(modCount)와 순환이 시작될 때 확인된 조작된 횟수(expectedModCount)의 값을 비교해 만약 두 값이 일치하지 않으면 ConcurrentModificationException을 발생시킨다.
abstract class LinkedHashIterator {
...
int expectedModCount;
...
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
// 실제 조작된 횟수와 기대 조작 횟수(Iteration을 처음 시작할 때 조작 횟수)가 일치하지 않는 경우
throw new ConcurrentModificationException();
...
}
...
}
예를 들어 다음과 같은 코드를 살펴보자.
fun main() {
val map = mutableMapOf(
1 to 1,
2 to 2,
3 to 3
)
map.forEach { // Iteration 시작
map.remove(it.key) // expectedModCount
}
}
이 코드에서 map은 3개의 원소가 입력됐기 때문에 Iteration을 시작할 때 modCount가 3이 되며, expectedModCount도 3이 된다. 하지만, 내부에서 remove를 한 번 호출하는 순간 modCount가 4가 된다. 이때, expectedModCount는 3이 되기 때문에 ConcurrentModificationException이 발생한다.
문제 해결 방법
Iteration이 진행될 동안 MutableMap 변화시키지 않기
이 문제를 해결하는 근본적인 방법은 Iteration이 진행될 때 MutableMap 객체에 변경을 가하지 않는 것이다. 이를 위해서는 별도의 리스트를 선언한 후, 해당 리스트에 제거해야할 원소를 입력하고, Iteration이 마쳐지면 제거를 호출해야 한다. 예를 들어 다음과 같이 작성하면 된다.
fun main() {
val map = mutableMapOf(
1 to 1,
2 to 2,
3 to 3
)
val removeTargets = mutableListOf<Int>() // 제거할 원소가 입력될 리스트
map.forEach {
removeTargets.add(it.key) // Iteration이 진행되는 동안 map을 변화시키지 않음
}
removeTargets.forEach {
map.remove(it)
}
}
그러면 코드를 실행해도 아무런 오류가 발생하지 않는 것을 볼 수 있다.
ConcurrentHashMap 사용하기
다른 방법은 ConcurrentHashMap을 사용하는 것이다. ConcurrentHashMap은 동시성을 지원하는 컬렉션이며, 다수의 스레드에서 접근해서 값을 읽고 변경해도 안전하도록 설계되었기 때문에 Iteration이 진행되는 와중에 map에 대한 변경 요청이 와도 안전하다. 따라서 코드를 다음과 같이 만들어도 코드가 문제 없이 실행되는 것을 볼 수 있다.
fun main() {
val map = ConcurrentHashMap<Int, Int>().apply {
put(1, 1)
put(2, 2)
put(3, 3)
}
map.forEach {
map.remove(it.key)
println(map)
}
}
실행 결과는 다음과 같다.
정리
- MutableMap 사용시 ConcurrentModificationException은, Iterator가 순환하는 동안 MutableMap에 변경이 일어나 생긴다.
- 해결을 위해서는 Iterator의 순환이 모두 끝난 후 조작을 하거나, ConcurrentHashMap을 사용하면 된다.