728x90
배경
프로젝트를 진행하면서 피드의 ImageUrl을 수정하는 로직이 있었는데 여기서 java.util.ConcurrentModificationException 예외가 발생했다. ConcurrentModificationException는 무엇이고 어떻게 해결할 수 있는지 알아보자
private void removeFeedImages(List<String> removeImageUrls, Feed feed) {
if (removeImageUrls != null) {
if (feed.isImageUrlsSizeOne()) {
log.debug("FeedService.updateFeed exception occur " +
"removeImageUrls : {}, saveFeedImageUrls : {}",
removeImageUrls,
feed.getImageUrls().toString());
throw new BusinessLogicException(ExceptionCode.UNABLE_TO_DELETE_FEED_IMAGE);
}
removeImageUrls.forEach(imageUrl -> {
fileUploadService.remove(imageUrl);
feed.removeImageUrl(imageUrl);
});
}
}
java.util.ConcurrentModificationException
at java.base/java.util.ArrayList.forEach(ArrayList.java:1543)
at com.frog.travelwithme.domain.feed.service.FeedService.removeFeedImages(FeedService.java:150)
at com.frog.travelwithme.domain.feed.service.FeedService.deleteFeed(FeedService.java:82)
at com.frog.travelwithme.domain.feed.service.FeedService$$FastClassBySpringCGLIB$$dda9705f.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
...
원인
ConcurrentModificationException는 List, Map과 같은 컬렉션을 수정하는 도중에 다른 스레드에서 동시에 컬렉션을 수정하려고 발생하는 예외이다.
아래 코드에서 예외가 발생했던 것인데, foreach메서드는 Iterator를 사용해 내부적으로 반복자가 사용되기 때문에 중간에 다른 스레드에서 컬렉션을 수정하려고 할 때 ConcurrentModificationException 예외가 발생했던 것이다.
removeImageUrls.forEach(imageUrl -> {
fileUploadService.remove(imageUrl);
feed.removeImageUrl(imageUrl);
}
해결
그래서 나는 내부 반복자를 사용하지 않도록 직접 for문을 돌려 데이터를 하나씩 선언해 작업을 수행하도록 했다.
이렇게 하면 반복문이 컬렉션의 요소를 순서대로 가져오면서 수정 작업을 수행하기 때문에 내부 반복자를 사용하지 않아서 ConcurrentModificationException이 발생하지 않는다.
private void removeFeedImages(List<String> removeImageUrls, Feed feed) {
if (removeImageUrls != null) {
if (feed.isImageUrlsSizeOne()) {
log.debug("FeedService.updateFeed exception occur " +
"removeImageUrls : {}, saveFeedImageUrls : {}",
removeImageUrls,
feed.getImageUrls().toString());
throw new BusinessLogicException(ExceptionCode.UNABLE_TO_DELETE_FEED_IMAGE);
}
for (String imageUrl : removeImageUrls) {
fileUploadService.remove(imageUrl);
feed.removeImageUrl(imageUrl);
}
}
}
정리
- foreEach() 메서드는 내부적으로 반복자를 사용해서 컬렉션을 순회한다. 동시성 문제를 방지하기 위해 내부적으로 불변성을 유지하는 방식으로 동작하기 때문에 forEach에 컬렉션을 수정하는 로직이 들어가 있으면 ConcurrentModificationException 예외가 발생할 수 있다.
- for-each 문법은 직접 컬렉션의 요소를 가져오기 때문에 반복자를 사용하지 않는다. 그래서 동시 수정 문제가 발생하지 않고 ConcurrentModificationException 예외도 발생하지 않는다.
- 단순 조회할 때는 forEach() 메서드를 사용하고 수정 작업이 필요하면 for-each 문으로 작업하는 것이 ConcurrentModificationException 예외를 방지할 수 있다.
728x90