쌓고 쌓다
웹 에디터에서 업로드된 이미지 파일 관리(+연관관계) 방법 본문
현재 CKEditor5를 사용해서 게시글의 텍스트 중간중간에 이미지 파일을 출력하고 있다.
이러한 방식의 게시글을 위해서는 사용자가 이미지 파일 업로드시에
서버에서 업로드한 이미지파일을 저장하고 그 URL 경로를 통해서 게시글의 중간에 이미지 파일을 출력할 수 있다.
그런데 바로 이미지 파일을 바로 업로드 해버리면 다음과 같은 문제가 발생한다.
문제
게시글 작성(수정)중에 나가버리는 경우 업로드한 파일은 쓰레기가 된다.
그래서 다음과 같은 구현 방법을 고민해야한다.
- 게시글 작성(수정)중에 나가버리는 경우 업로드된 파일은 어떻게 관리할 것인가?
- 게시글 수정하여 지워버린 이미지 파일은 어떻게 관리할 것인가?
해결 방법
- 이미지 파일 업로드시에 게시글과의 연관관계는 우선 NULL로 맺어놓는다.
- 이미지 파일의 저장 정보는 UUID를 통해 관리해야하며 Unique함.
1. 게시글 작성(수정)중에 추가된 파일
- 게시글 작성완료시 전송되는 html의 img src를 파싱하여 UUID를 추출한다.
- DB를 통해 해당 UUID를 통해 SELECT하며, 게시글의 외래키를 해당 게시글 PK로 설정한다.
2. 게시글 작성(수정)중에 삭제된 파일
- 게시글 작성(수정) 완료시에 전송되는 html의 img src를 파싱하여 UUID를 추출한다.
- DB를 통해 해당 UUID를 가지는 파일을 삭제처리한다.
3. 추가, 삭제된 이미지 파일 구별 방법
그러면 게시글 생성 및 수정으로 전송된 html을 통해서 어떻게 추가, 삭제된 파일을 구별할까?
다음과 같은 게시글 수정 코드를 작성할 수 있다.
@Transactional
public Poster updatePoster(Long posterId, PosterWriteRequest posterWriteRequest) {
Poster poster = posterRepository.findById(posterId)
.orElseThrow(() -> new IllegalArgumentException());
poster.setTitle(posterWriteRequest.getTitle());
poster.setContent(posterWriteRequest.getContent());
List<String> existingUUIDs = posterImageRepository.findAllByPoster(poster).stream()
.map(p -> p.getStoreFileName())
.collect(Collectors.toList());
List<String> receivedUUIDs = UploadFileUtil.imageUUIDExtractor(posterWriteRequest.getContent());
// 게시글 수정시 삭제된 업로드 이미지 파일 삭제 방법
// 기존 게시글의 이미지 UUIDs 현재 게시글의 이미지 UUIDs 제거하면 삭제된 UUIDs가 남는다.
existingUUIDs.removeAll(receivedUUIDs);
for (String existingUUID : existingUUIDs) {
posterImageRepository.findByStoreFileName(existingUUID)
.ifPresent(posterImage -> {
posterImageRepository.delete(posterImage);
s3Uploader.deletePosterImg(posterImage);
});
}
// 게시글 수정시 추가된 업로드 이미지 파일 등록
for (String receivedUUID : receivedUUIDs) {
posterImageRepository.findByStoreFileName(receivedUUID)
.filter(posterImage -> posterImage.getPoster() == null)
.ifPresent(posterImage -> posterImage.setPoster(poster));
}
return poster;
}
위의 코드 로직은 다음과 같다.
- 기존에 해당 게시글에 저장된 이미지 파일들의 UUID를 받아옵니다. => existingUUIDs
- 게시글 수정으로 들어온 게시글의 이미지 파일들의 UUID를 받아옵니다. => receivedUUIDs
- 게시글 수정시 삭제된 업로드 이미지 파일 추출 방법
- 기존 게시글의 UUID 리스트에서 입력으로 들어온 UUID 리스트를 제거하면 삭제된 UUID만 남게 됩니다.
- 이 UUID를 통해 이미지 파일을 찾아 삭제 처리해주면 됩니다.
- 게시글 수정시 업로드된 이미지 파일 추출 방법
- 입력으로 들어온 UUID를 통해서 DB를 통해 업로드 이미지 파일을 조회해 게시글과 매핑된 연관관계 외래키가 NULL인 경우에 해당 게시글의 PK로 업데이트해줍니다.
HTML에서 파싱 방법
<img style="aspect-ratio:80/102;" src="https://example-aws-s3.s3.ap-northeast-2.amazonaws.com/poster/ae7ab5f1-d3ea-4a92-bdd0-b1918a84e422" width="80" height="102">
현재 내 프로젝트에서 이미지 파일 URL의 경로는 위와 같다.
맨 마지막에 "/ae7ab5f1-ae7ab5f1-d3ea-4a92-bdd0-b1918a84e422"로 마지막 "/"뒤에 UUID가 붙는 형식이다.
자신의 프로젝트에 맞게 img src를 정규표현식을 이용해 추출하여 UUID를 구하면된다.
현재 위의 img src를 추출하고 UUID를 뽑아내는 코드는 다음과 같이 작성했다.
public static List<String> imageUUIDExtractor(String html) {
String regex = "<img\\s+[^>]*src=\"([^\"]+)\"[^>]*>";
ArrayList<String> imageUUIDs = new ArrayList<>();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(html);
while (matcher.find()) {
String imageUrl = matcher.group(1);
imageUUIDs.add(imageUrl.substring(imageUrl.lastIndexOf("/") + 1));
}
return imageUUIDs;
}
자신의 프로젝트에서 이미지 파일의 URL 형식에 맞게 정규표현식을 작성하여 추출하면 된다.
다음과 같이 동작을 확인할 수 있다.
게시글 최초 작성시 test.jpg를 업로드하였을때
게시글 외래키는 NULL로 일시적으로 들어가 있다.
게시글 작성 완료시
전송된 html을 파싱하여 이미지 파일의 UUID를 구해 해당 이미지 파일의 게시글 PK로 외래키를 UPDATE 쿼리를 날릴 수 있다.
게시글 수정시 해당 이미지를 지워버리면
게시글 내용 html에 해당 Img 태그는 지워진채로 게시글 수정된 html을 서버로 전송하게 된다.
서버측에서는 html을 통해 삭제된 이미지 UUID를 구할 수 있으며 이것을 삭제 처리 로직을 밟게하면 된다.
그럼 게시글 최초 작성시 업로드한 이미지 파일은 중간에 나가버리면 어떻게 관리하나요?
배치를 통해서 주기적으로 NULL인 이미지 파일을 삭제하여 관리한다.
아직 배치 프로그램을 작성하지 않았지만 재밌을 것 같다.
+ TMI
깃허브나 다른 게시글 작성하는 웹 사이트에서 업로드된 파일은 이렇게 세세하게 연관관계를 관리하지 않고 그냥 저장해버리고 URL로만 남기는것 같다.
그래서 우리가 다른 유명 웹 사이트에서 게시글 작성 완료를하지 않고 업로드한 이미지파일을 영구적으로 사용할 수 있는것 같다.
역시 요즘 저장 공간 기술력이 좋아져서 용량은 문제되지 않기에 그냥 서버에 짱박아서 저장해버리고 관리하지 않는것 같다..
'프로그래밍 > spring' 카테고리의 다른 글
CORS error 발생! 그게 뭔데? (0) | 2024.08.14 |
---|---|
EC2와 도메인 연결하기 with Spring Security {baseUrl} (0) | 2024.08.11 |
CKEditor5 이미지 업로드 방법 및 CSRF 처리 (0) | 2024.07.30 |
위지윅 에디터와 XSS 대응 방법 with 타임리프 (0) | 2024.07.29 |
API, BindingResult, MessageSource를 이용한 에러 메시지 출력 방법 (0) | 2024.07.28 |