List 사용 - 동시성 문제 (Synchronized vs Concurrent)
List 사용시 갑자기 생겨나는 null
발생한 상황과 문제
스프링 스케쥴러를 통하여 하루 한 번 db에서 사라진 이미지 파일 url 값을 감지해 s3 버킷에서 삭제하는 기능을 만들고 있던 도중 생겨난 문제이다.
S3에서 모든 파일 object 리스트를 받아와서 db에 현재 존재하는 file 이름 값과 대조하여 s3에 남은 고아 object들을 stream.forEach를 통해 진행하던 도중 처음 이미지가 많이 없을 때는 문제가 없었으나 이미지가 많아지고 나니 null값이 뜨며 NullPointerException이 터졌음.
발생 환경
- SpringBoot version - 2.7.2
- Java Complier version - 11
- h2 db
- IDE - Intelli J
해결 및 시도 방법
1. S3에서 Null로 들어온건가? 혹시나 싶어 filter를 통해 걸러줌.
List<DeleteObjectsRequest.KeyVersion> keys = s3Service.getImageKeys().stream().filter(key -> key != null).collect(Collectors.toList()); // null 처리
하지만, 결과가 같았음.
2. Null을 정리 해줬는데도 Null이 뜨고, 삭제된 이후에 null이 생기는 것으로 봐서, 자바 Collection 공부할 때 지나가면서 읽었던 동시성 이슈인가? 싶어 구글링하고 대체할 방법을 찾음.
- 사실, 그냥 단순히 삭제하는 것이 아닌 다른 Collection 객체를 만들어 담으면 문제가 생기지 않으나 동시성 오류(정확한 원인인지는 모르겠으나)를 해결하기 위해 해결 방법을 찾아봄.
세부사항 변화
기존
기존에는 ArrayList를 사용함
public List<DeleteObjectsRequest.KeyVersion> getImageKeys() {
ListObjectsV2Result result = s3Client.listObjectsV2(bucket);
List<S3ObjectSummary> objects = result.getObjectSummaries();
List<DeleteObjectsRequest.KeyVersion> imageNameList = new ArrayList<>();
for (S3ObjectSummary os : objects) {
imageNameList.add(new DeleteObjectsRequest.KeyVersion(os.getKey()));
}
return imageNameList;
}
수정
java.util.concurrent 패키지에서 제공하는 병렬 컬렉션 중 CopyOnWriteArrayList를 사용하여 코드 작성
public List<DeleteObjectsRequest.KeyVersion> getImageKeys() {
ListObjectsV2Result result = s3Client.listObjectsV2(bucket);
List<S3ObjectSummary> objects = result.getObjectSummaries();
List<DeleteObjectsRequest.KeyVersion> imageNameList = new CopyOnWriteArrayList<>();
for (S3ObjectSummary os : objects) {
imageNameList.add(new DeleteObjectsRequest.KeyVersion(os.getKey()));
}
return imageNameList;
}
결과
문제 해결
확실히 위험성이 제거되었는지 확인하기 위해 수 차례 테스트 실시 및 이후에도 테스트 중.
회고
. 이 글에서는 동시성 이슈에 대한 기억을 쉽게 떠올린 것처럼 보이나 코드를 확인해보면서 기억해내는데 시간이 좀 걸렸다. 그만큼 문제 해결을 위해서는 유연한 사고와 폭 넓은 지식과 경험이 중요하다는 것을 느낀다. 만약 과거에 Java 책을 읽으며 Collection 에 대해서 지식이 없었다면 이 문제를 해결하지 못하거나 위에서 적었듯 새로운 컬렉션을 만들어 다른 방식으로 해결했겠지만 근본적인 문제 해결을 하지 못하였을 것이다. 문제를 회피하는 것은 미래에 있을 큰 위기를 방치하는 것이니 공부를 꾸준히 하며 주어진 문제를 해결하는 연습을 해야겠다.
이를 해결하기 위해서는 Synchronized 컬렉션과 Concurrent 병렬 컬렉션을 사용할 수 있다. Synchronized 컬렉션은 자바 공부할 당시 성능저하가 일어날 수 있다고 공부했었으며 구글링을 하다보니 김영한님 강의에서 HashMap은 실무에서 쓸 때 ConcurrentHashMap을 써야한다고 하신 말씀이 기억나서 찾아보고 사용하게 되었다. 구글링을 해보니 ArrayList 는 CopyOnWriteArrayList가 존재하여 ConCurrent Collection을 사용하였다.