remagine
배열(list) 제거 중 발생한 ConcurrentModificationException 본문
최근 부분취소 관련하여 로직이 수정된 부분이 있어, 이 부분을 거래대사에 녹여내는 작업이 있었습니다.
불일치 거래 내역 중 플러스 거래 이면 불일치에서 제외한다
for(ByddCmpProc plusData : plusDataList){
for(ByddCmpProc diffData : diffDataList){
if(StringUtils.equals(plusData.getCommcAprvNo(),diffData.getCommcAprvNo())){
diffDataList.remove(diffData);
}
}
}
상기 코드가 추가 되었고, 간단한 반영이라고 생각했습니다.
실제로 배치를 실행해 보니 에러로그가 찍혔습니다.
[2019-04-29 10:14:52.374|ERROR|o.s.batch.core.step.AbstractStep] java.util.ConcurrentModificationException
생전 처음보는 ConcurrentModificationException 이 발생했습니다.
Concurrent? 동시 발생의 , 경쟁 상대의
Modification? 가감, 변경
동시,경쟁적으로 가감,변경이 발생한 예외라는 거네요 무슨 말일까요
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) ~[na:1.7.0_80]
at java.util.ArrayList$Itr.next(ArrayList.java:831) ~[na:1.7.0_80]
에러로그를 살펴보니 checkForComodification 메소드에서 예외가 발생해 살펴보았습니다.
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount라는 값이 expectedModCount와 다르면 발생하네요.
next 메소드도 살펴보았습니다.
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
next 호출 시 먼저 checkForComodification 를 호출하고 있고
remove 시 modCount가 변경되어 예외가 발생하고 있었습니다.
remove 메소드는 내부적으로 fastRemove를 호출하고 있고
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
}
fastRemove는 modCount++로 modCount값을 증가시키고 있네요
정리하자면 Collection 구현체를 Iterator로 순환시 remove 하게되면 Collection이 변경되었다고 판단하여 예외를 던집니다.
이런 자바의 정책을 fail-fast라고 부릅니다.
fail-fast 방식 : 순차적 접근이 모두 끝나기 전에 콜렉션 객체에 변경이 일어날 경우 순차적 접근이 실패되면서 ConcurrentModificationException 예외를 return
반대로 fail-safe라는 방식도 있는데 이는 Enumertaion의 정책입니다. ( https://javaconceptoftheday.com/differences-between-enumeration-vs-iterator-in-java/)
아무생각 없이 쓰고 있던 Iterator는 사실 순환중 변경을 금지하고 있었네요.
제일 중요한 해결책으로는
자바 1.7 이하에서는
실제 List가 아닌 Iterator를 삭제
Iterator<ByddCmpProc> iter = diffDataList.iterator();
for(ByddCmpProc plusData : plusDataList){
while(iter.hasNext()){
ByddCmpProc diffData = iter.next();
if(StringUtils.equals(plusData.getCommcAprvNo(),diffData.getCommcAprvNo())){
iter.remove();
}
}
}
자바 1.8 이상에서는 람다를 사용하여 필터링
diffDataList.removeif(d -> {
return plusDataList.stream().anyMatch(x -> (StringUtils.equals(p.getCommcAprvNo(),x.getCommcAprvNo())))
});
참고로 iterator를 사용하지 않고 for문으로 변경후 삭제는 제대로 동작하지 않습니다.
for( int i = 0; i < plusDataList.size(); i++){
ByddCmpProc plusData = plusDataList.get(i);
for( int j = 0; j < diffDataList.size(); j++){
ByddCmpProc diffData = diffDataList.get(j);
if(StringUtils.equals(plusData.getCommcAprvNo(),diffData.getCommcAprvNo())){
diffDataList.remove(diffData);
}
}
}
리스트에서 i번째 원소가 삭제되면, i+1번째 원소가 그자리에 오게되서 i+1번째 원소의 체크를 누락하고 넘어가게 됩니다.
생각지 못한 부분에서 배운점이 많아 공유합니다.
'JAVA' 카테고리의 다른 글
알아두면 매우 편리한 자바 상식 10선 (0) | 2017.02.17 |
---|