온라인 도서관 프로젝트에서, 현재로서는 책 커버이미지(BookCover)는 book당 1개밖에 갖지 못한다.
그런데, test에서 실패하는데 그 이유를 보니 findByBookId로 찾은 요소가 db에서 1개 이상의 요소를 찾았다고 한다.
@Test
void modify() {
//given
MultipartFile multipartFile = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test data".getBytes());
NewBookForm newBookForm = new NewBookForm("test", "testAuthor", "12345", multipartFile);
Book savedBook = bookService.save(newBookForm);
//when
ModifyBookForm modifyBookForm = new ModifyBookForm(savedBook.getBookId(), "modify", "modifyAuthor", "modify12345", multipartFile);
bookService.modify(modifyBookForm);
//then
Book findBook = bookService.findBookById(savedBook.getBookId()).get();
assertThat(findBook.getBookName()).isEqualTo("modify");
//cleanUp
bookService.deleteBook(savedBook.getBookId()); // 문제 발생!
}
같은 트랜잭션 안에서 삭제와 저장을 동시에
문제는 책을 수정한 다음 바로 삭제할 때 일어났다. 테스트 코드에서 cleanUp을 위해서 책을 저장 후, 수정 후, 저장한 책을 삭제하는 로직이었다. @Transcational이 걸려있지만, 로컬에 파일을 저장하는 로직이 있기 때문에 직접 cleanUp 코드를 작성해두었었다.
BookService에서 modifyBook 메서드를 보자.
public void modify(ModifyBookForm form) {
findBookById(form.getId()).ifPresentOrElse(book -> {
if (isDuplicated(book.getIsbn(), form.getIsbn())) {
throw new DuplicateIsbnException();
}
book.modify(form);
modifyBookCover(form, book);
}, () -> {
throw new NotFoundBookException();
});
}
서비스 계층은 전체적으로 @Transactional이 걸려 있고, 테스트 코드 역시 @Transaction으로 묶여 있다. 한 작업을 시작할 때 트랜잭션이 걸려 있어서, remove 후에 save를 하는 방식의 저장은 문제가 될 수 있다.
제대로 삭제되기 전에, 새로운 BookCover를 저장하는 것이다.
그러면 다음번에 deleteBook을 할 때 역시 같은 트랜잭션 안이므로, (테스트 코드) 아직 BookCover는 삭제가 되지 않고, 새로운 BookCover만 저장된 상태이다. 이때, deleteBook 내부에서 호출하는 remobeBookCover가 BookCoverRepository.findByBookId를 호출하면서 두 개의 BookCover를 찾아서 문제가 발생한다.
public void deleteBook(Long bookId) {
Book removedBook = bookRepository.findById(bookId)
.orElseThrow(NotFoundBookException::new);
removeBookCover(removedBook);
bookRepository.remove(bookId);
}
private void removeBookCover(Book book) {
BookCover bookCover = bookCoverRepository.findByBookId(book.getBookId())
.orElseThrow(NotFoundBookCoverException::new);
uploadFileRepository.remove(bookCover.getUploadFileId());
}
private void saveBookCover(Book book, MultipartFile multipartFile) {
UploadFile image = uploadFileRepository.save(multipartFile);
BookCover newBookCover = new BookCover(book.getBookId(), image.getUploadFileId());
bookCoverRepository.save(newBookCover);
}
private void modifyBookCover(ModifyBookForm form, Book book) {
if (form.getCoverImage() != null) {
removeBookCover(book);
saveBookCover(book, form.getCoverImage());
}
}
삭제 – 저장이 아닌 수정으로
방법은, 삭제 후 새로 저장이 아닌 파일은 실제로 삭제하고, 새 파일의 id만 BookCover에 수정해주는 방식이다. 이렇게 하면, 한 트랜잭션 안에서도 문제 없이 수정이 가능하다.
private void modifyBookCover(ModifyBookForm form, Book book) {
if (form.getCoverImage() != null) {
// 1. 새 이미지를 저장한다
// 2. 기존 커버 객체를 가져온다
// 3. 기존 커버 객체에서 uploadFileId를 변수에 임시 저장해둔다
// 4. 기존 커버 객체의 uploadFileId를 새 이미지의 uploadFileId로 변경한다
// 5. 기존 이미지를 삭제한다.
UploadFile updateImage = uploadFileService.save(form.getCoverImage());
bookCoverRepository.findById(book.getBookId())
.ifPresent(bookCover -> {
Long oldUploadFileId = bookCover.getUploadFileId();
bookCover.setUploadFileId(updateImage.getUploadFileId());
uploadFileService.remove(oldUploadFileId);
});
}
}