Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

DS's TechBlog

[Spring Data JPA] 이미지 파일과 DB 간의 일관성 유지하기 With @TransactionalEventListener - 2 본문

Java & Spring

[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 간의 일관성을 유지하려고 했는데요, 아직 아래와 같은 문제가 남았습니다.

  1. ImageUtil.uploadImage(...) 이후에 예외가 발생한다면? 
    - @Transactional 키워드로 인해서 DB는 Rollback 됩니다. 하지만, 이미지 파일은 Transaction과 관계가 없으므로 그대로 저장된 상태를 유지하게 됩니다. 이로 인해 불필요한 이미지가 파일 시스템에 남는 문제가 생길 수 있습니다.
  2. 메서드가 끝나고 Transaction commit 단계에서 에러가 발생한다면?
    - ImageUtil.removeImage(...)에 의해 이미지가 파일 시스템에서 삭제됩니다. 그 후, 메서드가 끝나게 되면서 트랜잭션 커밋이 실행되는데, 이때 문제가 발생하면 DB는 Rollback 되지만, 지워진 이미지는 복구할 수 없기에 일관성의 문제가 발생할 수 있습니다.

 

@TransactionEventListener  사용해 보자!

현재 문제는 Transaction의 범위에 File System이 포함되지 않는 것인데요, @TransactionEventListener를 사용해서 해결하려고 합니다. 이는 트랜잭션의 단계에 따라서 이벤트를 처리할 수 있는 리스너를 정의하기 위한 어노테이션인데요, 다음과 같은 이벤트를 처리할 수 있습니다.

  1. TransactionCommittedEvent: 트랜잭션이 커밋되었을 때 발생.
  2. TransactionRolledBackEvent: 트랜잭션이 롤백되었을 때 발생.
  3. 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를 사용해보았습니다. 이외에도 트랜잭션과 관련하여 이벤트를 처리할 때 유용하게 사용할 수 있을 것 같습니다.

읽어주셔서 감사합니다.