DS's TechBlog
[Spring Data JPA] 이미지 파일과 DB 간의 일관성 유지하기 With @TransactionalEventListener - 2 본문
[Spring Data JPA] 이미지 파일과 DB 간의 일관성 유지하기 With @TransactionalEventListener - 2
dsjo 2025. 1. 5. 23:13이미지를 파일 시스템에 저장하고 관련 정보를 DB에 저장할 때, 이미지 파일과 DB 간의 일관성을 유지하는 방법 2편입니다. (https://dsjo.tistory.com/12) 2편은 @TransactionEventListener 를 중심으로 작성하였습니다.
아직 남은 문제
@Override
@Transactional
public void updateBoardImage(Long activityId, Long boardId, MultipartFile boardImageFile) {
Board board = boardService.getBoardWithImageAndUser(activityId, boardId);
User loggedInUser = securityService.getLoggedInUser();
User creator = board.getUser();
boardService.validateAuthorityOfBoardManagement(loggedInUser, creator);
String originalFile = boardImageFile.getOriginalFilename();
String saveFile = UUID.randomUUID() + originalFile.substring(originalFile.lastIndexOf('.'));
ImageUtil.uploadImage(boardImageFile, uploadPath, saveFile);
BoardImage boardImage = board.getBoardImage();
String removeFile = boardImage.getSaveFile();
// 이미지 테이블 수정
boardImage.setOriginalFile(originalFile);
boardImage.setSaveFile(saveFile);
ImageUtil.removeImage(uploadPath, removeFile);
}
위의 코드로 이미지 파일과 DB 간의 일관성을 유지하려고 했는데요, 아직 아래와 같은 문제가 남았습니다.
- ImageUtil.uploadImage(...) 이후에 예외가 발생한다면?
- @Transactional 키워드로 인해서 DB는 Rollback 됩니다. 하지만, 이미지 파일은 Transaction과 관계가 없으므로 그대로 저장된 상태를 유지하게 됩니다. 이로 인해 불필요한 이미지가 파일 시스템에 남는 문제가 생길 수 있습니다. - 메서드가 끝나고 Transaction commit 단계에서 에러가 발생한다면?
- ImageUtil.removeImage(...)에 의해 이미지가 파일 시스템에서 삭제됩니다. 그 후, 메서드가 끝나게 되면서 트랜잭션 커밋이 실행되는데, 이때 문제가 발생하면 DB는 Rollback 되지만, 지워진 이미지는 복구할 수 없기에 일관성의 문제가 발생할 수 있습니다.
@TransactionEventListener 사용해 보자!
현재 문제는 Transaction의 범위에 File System이 포함되지 않는 것인데요, @TransactionEventListener를 사용해서 해결하려고 합니다. 이는 트랜잭션의 단계에 따라서 이벤트를 처리할 수 있는 리스너를 정의하기 위한 어노테이션인데요, 다음과 같은 이벤트를 처리할 수 있습니다.
- TransactionCommittedEvent: 트랜잭션이 커밋되었을 때 발생.
- TransactionRolledBackEvent: 트랜잭션이 롤백되었을 때 발생.
- TransactionStartedEvent: 트랜잭션이 시작되었을 때 발생.
방법을 소개드리겠습니다. 먼저, 이벤트 리스너에서 사용할 이벤트 객체를 아래와 같이 정의합니다.
@Getter
public class ImageSaveRollbackEvent {
private final String uploadPath;
private final String saveFile;
public ImageSaveRollbackEvent(String uploadPath, String saveFile) {
this.uploadPath = uploadPath;
this.saveFile = saveFile;
}
}
@Getter
public class ImageRemoveEvent {
private final String uploadPath;
private final String removeFile;
public ImageRemoveEvent(String uploadPath, String removeFile) {
this.uploadPath = uploadPath;
this.removeFile = removeFile;
}
}
ImageSaveRollbackEvent는 트랜잭션이 실패한다면, 파일 시스템에 저장한 이미지 삭제를 하기 위한 클래스입니다.
ImageRemoveEvent는 트랜잭션이 성공한다면, 파일 시스템에 저장한 이미지 삭제를 하기 위한 클래스입니다.
그리고, 이벤트를 처리할 이벤트 리스너 클래스를 작성합니다.
@Component
@Slf4j
public class ImageEventListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(ImageRemoveEvent event) { // 트랜잭션이 성공한다면, 파일 시스템에 저장한 이미지 삭제 (기존 이미지 삭제 할 때 사용)
// 트랜잭션 커밋 후 파일 삭제
ImageUtil.removeImage(event.getUploadPath(), event.getRemoveFile());
log.info("Successfully removed old image file: {}", event.getRemoveFile());
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(ImageSaveRollbackEvent event) { // 트랜잭션이 실패한다면, 파일 시스템에 저장한 이미지 삭제 (새로운 이미지 저장 할 때 사용)
ImageUtil.removeImage(event.getUploadPath(), event.getSaveFile());
log.info("Transaction rolled back, removed newly uploaded file: {}", event.getSaveFile());
}
}
TransactionPhase.AFTER_COMMIT 를 이용하여 트랜잭션 커밋 이후에 이벤트를 처리합니다.
(새로운 이미지를 저장하는 트랜잭션이 성공했을 경우에 기존 이미지를 삭제하는 이벤트 처리)
TransactionPhase.AFTER_ROLLBACK 을 이용하여 트랜잭션이 롤백 되었을 때 이벤트를 처리합니다.
(새로운 이미지를 저장하였는데, 이후에 트랜잭션이 실패하여 롤백 되었을 경우 저장한 이미지 삭제하는 이벤트 처리)
마지막으로 이를 적용한 코드입니다.
@Service
@Slf4j
public class BoardAppServiceImpl implements BoardAppService {
private final ApplicationEventPublisher applicationEventPublisher;
// ... 생략
@Override
@Transactional
public void updateBoardImage(Long activityId, Long boardId, MultipartFile boardImageFile) {
Board board = boardService.getBoardWithUser(activityId, boardId);
User loggedInUser = securityService.getLoggedInUser();
User creator = board.getUser();
// BoardImage 업데이트 권한 유효성 검사
boardService.validateAuthorityOfBoardManagement(loggedInUser, creator);
// 새 이미지 파일 저장
String originalFile = boardImageFile.getOriginalFilename();
String saveFile = UUID.randomUUID() + "." + ImageUtil.getExtension(originalFile);
ImageUtil.uploadImage(boardImageFile, uploadPath, saveFile);
// ROLLBACK 발생시 저장한 이미지 파일 삭제
applicationEventPublisher.publishEvent(new ImageSaveRollbackEvent(uploadPath, saveFile));
BoardImage boardImage = board.getBoardImage();
String removeFile = boardImage.getSaveFile();
// 이미지 테이블 수정
boardImage.setOriginalFile(originalFile);
boardImage.setSaveFile(saveFile);
// 모든작업이 Commit 될 시에 이전 이미지 파일 삭제
applicationEventPublisher.publishEvent(new ImageRemoveEvent(uploadPath, removeFile));
}
}
note) ApplicationEventPublisher로 이벤트를 발행할 수 있습니다.
ImageUtil.uploadImage(...) 이후에 예외가 발생하여 Rollback이 발생하면 새로 저장한 이미지가 삭제되고, 메서드가 끝나고 Transcation이 commit된 후에 이전 이미지가 삭제됩니다. 이로써 이미지 파일과 DB 간의 일관성을 유지할 수 있게 됩니다.
이번 포스팅에선 이미지 파일과 DB 간의 일관성 유지하기 위해서, @TransactionalEventListener를 사용해보았습니다. 이외에도 트랜잭션과 관련하여 이벤트를 처리할 때 유용하게 사용할 수 있을 것 같습니다.
읽어주셔서 감사합니다.
'Java & Spring' 카테고리의 다른 글
[Spring Data JPA] 이미지 파일과 DB 간의 일관성 유지하기 with SQL Order Based on Foreign Key Constraints - 1 (1) | 2024.12.27 |
---|---|
[Spring] AWS S3 + Spring Boot 3.2 설정 및 적용하기 (1) | 2024.06.09 |
[Spring Security] authorizeHttpRequests 우선순위 적용하기 (3) | 2024.05.17 |
[Spring] Swagger default 200 response 삭제하기 (0) | 2024.05.03 |
[Spring] Swagger 공통 응답 코드 처리 및 Enum으로 정의한 응답 코드 사용하기 (0) | 2024.05.03 |