문제 요약
이벤트 등록/수정 화면에서 2개의 이미지를 관리한다.
- 썸네일 이미지 (thumbnail)
- 상세 페이지 메인 콘텐츠 이미지 (main)
정상 동작
- 최초 업로드 시: thumbnailFileSeq / mainFileSeq가 각각 서로 다른 값으로 정상 저장
버그 현상
- 둘 중 하나를 삭제하고 저장한 순간부터
- thumbnailFileSeq와 mainFileSeq가 같은 값으로 동기화되기 시작
- 그 이후에는
- 둘 중 하나만 수정해도 두 개가 같이 바뀌는 버그 발생
아키텍처 개요
프론트엔드 저장 흐름
저장 버튼 클릭 시, 2개의 업로드 컴포넌트 배열을 순회하며 업로드를 수행한다.
for (const [index, key]of fileKeyArray) {const result =await fileComponents[index].upload();if (result?.data?.fileList?.length >0) {
params[key] = result.data.fileList[0].fileSeq;
params.fileInfoList = params.fileInfoList.concat(result.data.fileList);
}
}
업로드 컴포넌트는 최종 파일 배열을 만들고 multipart/form-data로 업로드한다.
upload:async () => {const data = form.serializeObject()[attrName];const finalList =getFinalData(data);if (finalList.length <1) {return {code:"NO_CHANGE" };
}const formData =toFormData({fileList: finalList });returnawait api.uploadFiles(formData);
}
서버 업로드 처리 방식
서버는 fileList의 각 항목을 보고 flag로 처리한다.
- I : insert (신규 업로드)
- D : delete (논리 삭제)
- 그 외 / 없음 : “그냥 응답 리스트에 담아줌” (여기서 냄새가 난다)
if ("I".equals(flag)) {
insertFile(fileInfo);
response.add(toResponse(fileInfo));
}elseif ("D".equals(flag)) {
deleteFile(fileInfo);// delYn=Y, useYn=N
}else {// ⚠️ 변경이 없어도 응답 리스트에 들어갈 수 있음
response.add(toResponse(fileInfo));
}
원인 분석
핵심 원인 1) “변경 없음”도 업로드 루프를 타면서 응답에 섞임
삭제/수정이 발생하면, 프론트에서는 두 컴포넌트를 무조건 루프한다.
- 실제로는 “썸네일만 변경” 또는 “메인만 삭제” 같은 상황이 대부분인데
- 업데이트 대상 필터링이 없음 → 둘 다 upload() 호출됨
그런데 upload()는 변경이 없을 경우 NO_CHANGE를 리턴해야 정상인데,
데이터 구성 과정에서 flag 없는 기존 파일 정보가 최종 리스트에 포함되는 케이스가 생겼다.
즉, 결과적으로:
- 한쪽은 D(삭제)
- 다른쪽은 flag 없음(변경 없음)
- 그런데 서버는 flag 없음도 응답 리스트에 담아버림
핵심 원인 2) 서버 update 로직이 fileInfoList “첫 번째”를 메인 seq로 박아버림
이게 “동기화”가 시작되는 트리거였다.
publicintupdateEvent(Request req) {if (req.getFileInfoList() !=null && !req.getFileInfoList().isEmpty()) {// ❌ 첫 번째를 메인 이미지로 가정
req.setMainFileSeq(req.getFileInfoList().get(0).getFileSeq());
}return mapper.updateEvent(req);
}
삭제 직후에는 fileInfoList의 순서/내용이 흔들리면서
- 썸네일의 fileSeq가 mainFileSeq로 들어가거나
- 메인 삭제 후 남아있던 썸네일 fileSeq가 양쪽에 들어가며
- thumbnailFileSeq == mainFileSeq 상태가 만들어짐
그 이후 “한쪽만 바꿔도 둘 다 바뀌는” 상태로 고착된다.
재현 시나리오
- 썸네일/메인 모두 업로드 → 정상
- 메인 이미지만 삭제하고 저장
- 서버 update 시 fileInfoList.get(0)이 썸네일을 가리킴
- mainFileSeq가 썸네일 seq로 덮임
- 다음부터는 어느 한쪽 수정 시도 시, 서버/프론트 로직 때문에 둘 다 같은 seq로 처리
해결 방향
사실 코드로 봤을 때 서로 다른 두개 파일을 업로드 하는 것을 고려해서 설계되지 않았던 것 같았다. 그래서 썸네일, 메인 이미지로 수정되는 순간 기존 로직들이 꼬이는 부분이 많았다. 이미 파일 업로드 부분에 대한 로직은 공통으로 쓰이는 영역이 많아서, 기존 로직을 건들지 않으면서 해결할 수 있는 방안으로 수정을 했다.
- 파일이 무엇인지(썸네일/메인) 구분자가 반드시 있어야 한다
- 변경 없는 데이터는 update 대상에서 제거해야 한다
- 서버는 “첫 번째 원소” 같은 위치 기반 가정을 하면 안 된다
1) 파일 구분자(imageCode) 추가
fileInfoList에 다음 중 하나를 포함시키도록 설계:
- imageCode = THUMBNAIL
- imageCode = MAIN
그리고 서버에서는 imageCode 기준으로 seq를 set 한다.
2) “실제 업로드된 파일만” 업데이트 대상으로 처리
fileFullPath가 null이면 “업로드로 생성된 결과가 아님” → skip
수정된 서버 로직
대략 아래와 같은 플로우이다.
public int updateEntity(UpdateRequest req) {
List<FileRef> files = req.getFiles();
if (files == null || files.isEmpty()) {
return repository.update(req);
}
for (FileRef f : files) {
// ✅ 변경(업로드)이 실제로 있었던 파일만 처리
if (!f.isUploaded()) continue; // 예: fileFullPath != null 로 판단해도 됨 > 이 판단 조건 자체도 100% 마음에 들지는 않았다.
// ✅ 파일 메타 갱신(선택): useYn, audit 등
fileRepo.markInUse(f.getFileSeq(), f.getFileDetailSeq(), req.getUpdaterId());
// ✅ role 기준으로 해당 슬롯에만 반영 (순서/index 의존 X)
if ("THUMBNAIL".equals(f.getRole())) {
req.setThumbnailFileSeq(f.getFileSeq());
} else if ("MAIN".equals(f.getRole())) {
req.setMainFileSeq(f.getFileSeq());
}
}
return repository.update(req);
}
class FileRef {
Long fileSeq;
Long fileDetailSeq;
String role; // "THUMBNAIL" | "MAIN" ...
boolean uploaded; // 이번 요청에서 실제 업로드로 생성된 결과인지
boolean isUploaded() { return uploaded; }
}
배운 점
1) “index 기반으로 파일을 구분”하면 언젠가 터진다
- 삭제/추가/정렬/필터가 들어가는 순간 index는 신뢰할 수 없다
- 특히 파일 업로드같이 여러 컴포넌트가 생기는 부분을 loop로 처리할 때 위험이 커진다
2) 서버에서 “리스트 첫 번째를 대표값으로” 가정하면 데이터가 깨진다
- 리스트의 순서는 UI/상태/삭제 로직에 따라 항상 바뀔 수 있다
- “대표값”은 반드시 명시적인 키(imageCode) 로 결정해야 한다
3) 변경 없는 데이터는 payload에서 제거하거나, 서버에서 확실히 필터링해야 한다
- “flag 없음” 상태로 서버에 들어오면
- 응답 list 오염
- update 조건 오판
- 결과적으로 다른 필드까지 덮어쓰기
추가 개선 아이디어
버그는 수정됐지만 전반적인 플로우 자체가 마음에 들지는 았았다. 프론트엔드에서 dirty check 를 하는 부분과 백엔드에서 해당 플래그를 바탕으로 동작하는 부분이 매끄럽게 맞아 떨어지지 않았고 그 부분을 수정하려니 맞물려있는 동작들이 많아서 수정할 엄두가 안났다.
만약 이후 리팩토링해야 한다면, 아래 방향으로 리팩토링하지 않을까 싶다.
- 프론트에서 애초에 dirty check를 해서 변경된 컴포넌트만 upload 호출
- 서버 upload 응답은 “변경된 파일만” 내려주고, 변경 없음은 아예 리스트에 포함하지 않도록 정책
- loop 으로 판단하고 파일 처리하는 로직 검토
'Dev Log' 카테고리의 다른 글
| 엑셀 업로드부터 DB 저장까지의 전체 프로세스 (0) | 2026.03.08 |
|---|---|
| 무한스크롤을 적용하며 다시 보게 된 페이징 구조 (0) | 2026.03.01 |
| Java 컬렉션 프레임워크 정리: List, Set, Map, HashMap (0) | 2026.02.15 |
| 사용자 추천 기능은 어떻게 테이블을 설계해야 할까 (0) | 2026.02.08 |
| 프론트엔드에서 처리하는가, 백엔드에서 처리하는가 (0) | 2026.02.01 |