파일 스토리지 제어는 커밋 이후에 하자

db에서 삭제 실패 해도 파일 시스템에서는 삭제 성공?!

이 코드의 문제는 뭘까? Optional을 findById에서 반환받아서 uploadFile 객체를 반환받았을 경우에만 fileStore에서 파일을 삭제하려는 시도는 좋았다.

하지만, 모종의 이유로 mapper.remove(UploadFileId) 메서드가 트랜젝션에서 롤백될 때 문제가 발생한다. fileStore에서 파일을 삭제하는 코드는 롤백이 되지 않기 때문이다.

@Override
public void remove(Long uploadFileId) {
    mapper.findById(uploadFileId).ifPresent(uploadFile -> fileStore.deleteFile(uploadFile.getStoreFileName()));
    mapper.remove(uploadFileId);
}

시도1: remove의 반환 결과에 따라 수정해보자

Mybatis는 update 코드에 대해 영향받은 row 수를 반환하도록 할 수 있으니, 이렇게 개선하면 되지 않을까? 하는 생각이 들었고, 코드를 수정해보았다.

결과는, 실패했다. 이유는, Mybatis의 remove는 영향을 준 로우 수를 반환하지만, 트랜잭션 rollback은 이 이후 시점이다. 따라서, 이 코드는 의미가 없다.

@Override
public void remove(Long uploadFileId) {
    mapper.findById(uploadFileId).ifPresent(uploadFile -> {
        if (mapper.remove(uploadFileId) > 0) {
            fileStore.deleteFile(uploadFile.getStoreFileName());
        }
    });
}

시도2: affterCommit

최종적으로 성공한 방법이다. 그렇다면, fileStore.deleteFile을 커밋 이후에 하도록 하면 될 일이다.

TransactionSynchronizationManager.registerSynchronization으로 트랜잭션 동기화 콜백을 등록해준다. 현재 진행 중인 트랜잭션에 특정 작업(예: afterCommit, afterRollback 등)을 수행할 콜백을 등록하는 것을 의미한다.

@Override
public void remove(Long uploadFileId) {
    mapper.findById(uploadFileId).ifPresent(uploadFile -> {
                mapper.remove(uploadFileId);
                deleteFile(uploadFile);
            }
    );
}

private void deleteFile(UploadFile uploadFile) {
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            fileStore.deleteFile(uploadFile.getStoreFileName());
        }
    });
}

개선: fileStore.deleteFile 메서드 안에서 트랜잭션 동기화 콜백 등록하기

위 방법은 커밋 후에 삭제를 시도하기 때문에 적절하기는 하지만, uploadFileRepository가 코드가 늘어나는 단점이 있다. 따라서 fileStore.deleteFile에서 추가해주는 방법으로 개선해주었다.

@Override
public void deleteFile(String storeFileName) {
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            File file = new File(getFullPath(storeFileName));
            file.delete();
        }
    });
}
@Override
public void remove(Long uploadFileId) {
    mapper.findById(uploadFileId).ifPresent(uploadFile -> {
                mapper.remove(uploadFileId);
                fileStore.deleteFile(uploadFile.getStoreFileName());
            }
    );
}

댓글

개발자  김철준

백엔드 개발자 김철준의 블로그입니다.

주요 프로젝트