코드잇이라는 프론트엔드 부트캠프에서 협업으로 팀 프로젝트를 진행하여 restAPI 서버를 구현하면서 겪었던 이슈들과 해결과정들을 간략하게 기록하는 글입니다.
우선 이슈 상황을 먼저 설명드리자면 ,
- AWS 암호키와 jwt 키를 보안하는데 local과 github push 시 외부에 노출되는 이슈
- 자바 collections 반복문 로직 안에서의 concurrent 에러
- 프론트 운영서버와의 CORS 관련 문제
위와 같은 세가지 상황을 기록하겠습니다.
하단의 상황은 이미 블로그 글로 정리하였기 때문에 제외하였습니다.
- Multipart 형태의 http 요청 데이터를 받을 때 다른 dto와 같이 받는 상황에서 데이터를 바인딩하지 못하는 이슈 발생
- 배포 과정에서 에러가 나서 서버가 다운되거나 배포 도중 애플리케이션이 작동하지 않는 이슈 상황
☀️ Collection 반복문에서 수정 또는 삭제시 concurrent 에러 발생
프로젝트 등록 API 부분에서 다수의 이미지를 S3에 업로드하고 반환받은 url을 DB에 넣어서 구현하는 부분이 있는데 , 이미지 파일의 개수대로 collection의 저장하고 반복문을 돌려 각각의 이미지 정보를 수정 또는 삭제하는 로직을 구현하였는데 이 부분에서 ConcurrentModificationException 예외가 발생하였습니다. 부끄럽지만 Java 프로젝트를 하면서 해당 예외를 처음 접하게 되었고 어떤 문제가 원인이지를 파악하는 데 시간이 꽤 오래 걸린 이슈라서 기록해 놓으려고 합니다.
ConcurrentModificationException 이 무엇인가?
자바에서 발생하는 예외(Exception) 중 하나로 ,Collection의 내부 요소를 동시 수정 시 주로 발생합니다.
일반적으로 Collection은 동기화되지 않은 (Non-Synchronized) 상태로 사용됩니다.
따라서 여러 스레드가 동시에 동일한 컬렉션을 수정하면 예측 불가능한 결과가 발생할 수 있습니다.
ConcurrentModificationException은 이런 문제를 방지하고자 Java Collection을 수정하는 동시성 작업을
제어하기 위해 도입 되었습니다.
위와 같이 ConcurrentModificationException의 정의를 보시면 스레드의 동시성 때문이구나라고만 예상할 수 있지만 주로 Java의 for-each 문에서의 ConcurrentModificationException 이 발생하는 경우는 멀티스레드 프로그래밍과는 직접적인 연관이 없습니다. 그 이유는 for-each문 자체가 멀티스레드를 생성하거나 다중 스레드를 사용하는 문법이 아니기 때문입니다.
그렇다면 무엇때문에 for-each문에서 ConcurrentModificationException 이 발생하는 지 2가지 원인을 알아보겠습니다.
- 반복 도중 Collection을 수정하는 경우
- 이 경우에는 for-each 문을 사용하여 list를 순회할 때 list의 요소가 수정되거나 삭제될 때 ConcurrentModificationException이 발생하는데 이는 반복 중에 컬렉션을 수정하면서 for-each문이 예상치 못한 동작을 하게 되기 때문입니다.
- 다른 스레드에 의해 컬렉션을 수정하는 경우
- 이 경우에는 새로운 스레드를 생성하고 , 이 스레드에서 list의 새로운 요소를 추가하거나 수정하고 동시에 다른 스레드에서 해당 list를 순회해서 사용하려고 하면 ConcurrentModificationException이 발생합니다.
for-each문의 순회 도중에 요소 추가 , 삭제 시 문제 되는 이유
for-each 문은 내부적으로 컬렉션의 Iterator를 사용하여 요소를 순회하기 때문입니다.
lterator 은 컬렉션의 요소를 순회하는 데 사용되는 인터페이스로서 , for-each 문은 이 lterator를 활용하여 순회 작업을 진행합니다.
1. for-each 문이 실행될 때 , 컬렉션의 lterator가 생성됩니다.
- 이 lterator는 순회 도중에 컬렉션의 변경 여부를 감지할 수 있도록 설계되어 있습니다.
2. for-each 문이 lterator를 사용하여 컬렉션의 요소를 하나씩 가져옵니다.
3. 만약 for-each문에서 직접 remove() 메서드를 호출하여 현재 요소를 삭제하면 ,
lterator는 컬렉션의 내부 상태가 변경(추가 및 삭제)되었다는 것을 감지합니다.
4. lterator는 요소를 삭제한 후에도 다음 순회를 진행하기 위해 다음 요소를 가져오려고 할 때 , 컬렉션의 구조가 변경되었음을 감지할 수 있습니다.
5. lterator는 컬렉션의 변경 여부를 확인한 결과 변경이 있었다는 것을 파악하고 , 이 상태에서 추가적인 순회를 중단시키고 ConcurrentModificationException을 발생시킵니다.
저의 경우에는 위와 같은 상황에서의 예외의 원인이였습니다!
이제 원인을 알았으니 해결방안을 알아보겠습니다.
ConcurrentModificationException 해결 방안
- 일반 for문과 인덱스 사용하기
- 이 방법은 일반 for문과 인덱스를 사용하여 list를 순회하고 순회 중에 요소를 변경 또는 삭제하는 방법입니다.
- 이렇게 일반 for 문과 인덱스를 사용하여 컬렉션을 순회하면 구조가 변경되는지에 대한 감지가 lterator를 사용하는 경우보다 미세하게 이루어지기 때문입니다.
- 따라서 일반 for 문과 인덱스를 사용하는 경우 , 개발자가 직접 컬렉션을 제어하고 요소를 수정하기 때문에 ConcurrentModificationException을 피할 수 있습니다.
- 단 이 경우에도 멀티스레드 환경에서는 여전히 동시성 문제에 주의해야 하며 반복문 도중 해당 인덱스의 요소의 위치가 변경될 수 있습니다.
- lterator를 직접 활용하기
- Collection 인터페이스에 removeIf 메소드 사용하기
- 저의 경우에는 이미지 배열에서 특정 조건의 이미지가 삭제하면 되면 상황이기 때문에 코드가 가장 깔끔하고 간단하게 나타낼 수 있다고 생각해 이 방법을 선택하였습니다.
private List<ProjectImage> orderImagesByIndex(List<ProjectImageDto> projectImageDtoList, Project currentProject) throws IOException {
//index - 기본 - db 인덱스 , 변경 - 0
List<ProjectImage> projectImageList = new ArrayList<>();
List<ProjectImage> removeProjectImgList = currentProject.getProjectImages();
List<Integer> toRemove = new ArrayList<>();
int newImageIdx = 0;
for (ProjectImageDto projectImageDto : projectImageDtoList) { //변경 후 넘어온 이미지 순서대로 loop
int originImageIdx = projectImageDto.index();
newImageIdx++; // loop 마다 +1
ProjectImage projectImage = new ProjectImage();
if (originImageIdx == 0) { //새로운 이미지 업로드
String url = this.uploadImageToS3(projectImageDto);
projectImage = new ProjectImage(url, newImageIdx);
} else { //순서만 변경 or 변경 x
String url = projectPort.findByIdx(originImageIdx).getUrl();
projectImage = new ProjectImage(url, newImageIdx);
removeProjectImgList.removeIf(image -> image.getIndex() == originImageIdx); // 재사용 이미지 인덱스 제거 배열에서 제외
}
projectImageList.add(projectImage);
}
//deleteImageToS3(removeProjectImgList); //사용 안 하는 이미지 S3 삭제
return projectImageList;
}
'TroubleShooting' 카테고리의 다른 글
| [FeedB 프로젝트] SpringBoot 프로젝트에서 민감 정보 보안(.env 파일) (0) | 2024.07.04 |
|---|---|
| [FeedB 프로젝트] json 형태의 값과 이미지 파일(MultipartFile)을 한 API에서 요청 받기 (0) | 2024.06.29 |