remagine

배열(list) 제거 중 발생한 ConcurrentModificationException 본문

JAVA

배열(list) 제거 중 발생한 ConcurrentModificationException

remagine 2019. 5. 9. 11:28

최근 부분취소 관련하여 로직이 수정된 부분이 있어, 이 부분을 거래대사에 녹여내는 작업이 있었습니다.

 

불일치 거래 내역 중 플러스 거래 이면 불일치에서 제외한다 

 

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
Comments