이펙티브 자바 3편을 보면 for each에 대한 설명이 나와 있습니다. 거기에서도 언급했다 시피, for each문을 도는 동안에 이터를 돌고 있는 자료구조에 변형이 오면 안 됩니다. (item 58) 만약에 변형이 오면 어떻게 될까요? 사실 저는 null Exception이 뜨거나 (Linked인 경우에), 아니면 정상적으로 수행이 될 거라고 생각했습니다만.. 예상과는 다른 것이 출력되었습니다.
그 이유는 modCount 필드와 관련이 있습니다.
아래 코드를 보겠습니다.
0부터 4까지 차례대로 LinkedList에 넣습니다. 그리고, List를 순회합니다. 이는 향상된 for loop를 써서 할 수 있습니다. 그런데, 이 코드에서는 단순히 '순회'만 하지 않습니다. list에 있는 첫 번째 원소를 도는 중간에 remove를 하는 transform도 수행합니다. 물론, 하면 안 되긴 합니다만, 어떻게 예외 코드를 따라가는지 보실 겸 겸사겸사 보도록 합시다. 프로그램을 실행해 보겠습니다.
그러면, checkForComodification에서 Exception이 떨어집니다. 그리고, 트레이스 정보가 나오는데요. 이 정보가 없었다면 꽤 끔찍했을 겁니다. 이건 또 무슨 예외일까요? 일단 888번째 예외를 주목할 필요가 있습니다.
modCount하고, expectedModCount가 다르면 던져지는 예외입니다. 그러면 이 둘에 대해서 알아둘 필요가 있습니다. 먼저, 우변에 있는 expectedModCount에 대해 알아보겠습니다.
이것은 ListItr에 있는 필드입니다. 7번째 줄에, for each문을 도는 구문이 있습니다. 내부적으로 새로운 ListItr을 생성합니다.
그러면, 이것은, expectedModCount를 초기화 합니다. 이 값은 modCount 값으로 초기화가 됩니다.
어딘가에 정의가 되어 있는 modCount의 설명을 보겠습니다.
설명을 보시면, 이 필드가 무시가 되는 경우도 있다는 것을 알 수 있습니다. 그렇지 않다면, 클래스의 어딘가에, modCount 변수와 expected 변수를 비교하는 로직이 있을 겁니다. 아니. 최소한 modCount를 쓰는 부분이 있을 겁니다. 어떤 클래스에서 modCount가 무시되고, 어떤 클래스에서 그렇지 않은지 보시는 것도 중요하긴 합니다만, 이 글에서 중요하게 다룰 내용은 아닙니다.
그러면, expectedModCount와 modCount가 언제 바뀌는가를 볼 필요가 있습니다. 이게 제일 중요한 부분이기도 하고요. 전자는, iter가 새로 생성이 될 때 modCount 값으로 초기화가 됩니다.
문제의 888번째 줄을 보면, 그 어디에서도 expected 값이 변하지 않았음을 알 수 있습니다. 그렇다면, 어디에서 modCount가 변했다는 이야기입니다. modCount는 공식 문서에 보면 구조가 변형된 횟수와 관련이 있습니다. 예를 들자면, 원소가 하나 추가되었다던지, 제거되었다던지, 아니면 clear가 되었다던지. 이는 modCount++을 해 보면 쉽게 찾을 수 있습니다. 물론 앞에 공백을 주면 좋습니다.
여기서 문제는, modCount가 변한 상황에서, expected가 변하지 않았다면, 불일치가 나타납니다. 예를 들어서, 영희가 기존에 사탕을 20개쯤 가지고 있었습니다. 철수가 그것을 알고 있었습니다. 그런데, 철수는 그 이후에 영희가 사탕 10개를 먹어버린 것을 몰랐습니다. 그러면, 철수는 영희가 사탕을 20개 가지고 있을 거라고 생각할 겁니다. 그런데, 실제로 영희는 10개만 가지고 있어요.
철수와 영희 사이에 정보의 불일치가 일어난 겁니다.
다시 문제의 for each문 상황으로 돌아와 보겠습니다. 영희는 사탕 2개를 가지고 있었습니다. 그리고 새로 생성된 철수는 사탕 2개가 있다는 것을 알고 있습니다. 그런데, li.remove(0)을 호출합니다.
사실 여기까지는 별로 큰 문제가 되지 않습니다. 문제는, 철수가 2개가 있다는 것을 알고 있는데, 실제로 영희가 하나를 제거했다는 점입니다. 업데이트가 된 것을 철수는 모르고 있습니다. 그렇기 때문에, 불일치 exception이 뜹니다.
그러면, expectedModCount가 증가하는 경우는 없을까요? 찾기를 해 보면, 아래와 같이 결과가 나옵니다.
ListItr의 remove와 add에 expected가 증가한다고 되어 있습니다.
이 중에서 remove를 보면 932번째 줄에 expected가 증가합니다. 그러면 ModCount는 어디서 증가할까요? unlink는 Linked List가 변형이 되는 연산입니다. 이 메서드에서 증가합니다. 중요한 것은, iter 내에서 remove 연산이 수행되었다는 겁니다.
그러면, 이렇게 작성하는 것은 별 문제 없을까요? 단지, 우리는, iterator를 받아서, 현재 이터레이터가 가리키고 있는 것을 제거했을 뿐입니다. 만약에, 제거가 되기 전에, next 값이 제거되는 위치의 다음 위치를 가리키고 있었다면, iter.next()도 별 문제 없이 동작할 겁니다. 다시 말해, 위치를 잃어버리지 않았다면 별 문제가 되지 않습니다.
그리고, 내부적으로 iter.remove를 할 때, expected와, modCount 2개가 동시에 증가하기 때문에, checkForcomodification 예외가 발생하지 않습니다.
정리하면, for each 문 중간에, 자료구조를 변형을 시키는 add, remove와 같은 것을 쓰면 안 된다는 겁니다. 이펙티브 자바에서도, 이 내용을 언급하고 있는데, 실제로 어떤 예외가 발생하는지 추적해 가는 것은 의미가 있었던 듯 싶습니다.
'레퍼런스 > 분석' 카테고리의 다른 글
java UnmodifiableCollection : 무엇이 다른가요? (0) | 2020.11.08 |
---|---|
Java의 String.valueOf 메서드의 동작과 lombok의 ToString 어노테이션의 스택 오버플로우 (0) | 2020.11.05 |
왜 mutable한 객체를 java map의 키 값으로 삼으면 조심해야 할까요? (0) | 2020.10.09 |
java toLowerCase toUpperCase 를 수행하면 길이가 항상 같을까요? (4) | 2020.09.27 |
파이썬의 list.pop(0)을 쓰면 안 되나요? (0) | 2020.09.25 |
최근댓글