‘스프링 부트 쇼핑몰 프로젝트 with jpa’ 책의 코드를 따라가던 중 상품 등록 과정에서 문제가 생겼다.
상품의 이미지 등록이 실패하면 상품 정보는 DB에 저장되고 이미지는 저장되지 않았다.
이미지가 저장되지 않았던 원인은 파일을 저장할 폴더가 없기 때문이었다.
하지만 @Transactional 이 제대로 작동한다면 상품정보와 상품이미지 모두 데이터베이스에 들어가지 않았어야 한다고 생각했고, 원인을 찾기 위해 @Transactional의 동작방식과 원인에 대해 알아봤다.
원인
@Transactional 에서 Checked Exception은 롤백하지 않는.
이미지 파일을 저장할 때 FileNotFoundException
이 발생했고, 이는 Checked Exception이기 때문에 롤백하지 않았던 것이다.
Spring에서 @Transactional 동작 방식
Spring에서 @Transactional
은 AOP를 이용해 구현한다.
@Transactional
을 선언한 메서드가 실행되기 전 Transaction begin 코드를 삽입하고, 메서드가 실행된 후 Transaction commit 코드를 삽입한다.
Spring Boot에서는 default로 CGLIB를 이용해 프록시 객체를 생성하고 코드를 삽입한다.
application.properties 설정
트랜잭션 관련 로그를 보기위해 application.properties
파일에 트랜잭션 패키지의 로그 레벨을 아래와 같이 설정한다.
logging.level.org.springframework.transaction=trace
위와 같이 트랜잭션이 작동하지만 Item Image를 저장할 때 예외가 생겼음에도 롤백하지 않는걸 알 수 있음
Checked Exception인 경우 롤백 되지않는다.
...JpaTransactionManager : Creating new transaction with name [com.shop.service.ItemService.saveItem]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
...JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@54440f18]
...TransactionInterceptor : Getting transaction for [com.shop.service.ItemService.saveItem]
...JpaTransactionManager : Participating in existing transaction
...TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
...TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
...JpaTransactionManager : Participating in existing transaction
...TransactionInterceptor : Getting transaction for [com.shop.service.ItemImgService.saveItemImg]
...TransactionInterceptor : Completing transaction for [com.shop.service.ItemImgService.saveItemImg] after exception: java.io.FileNotFoundException: C:\shop\item\33e0aaf5-7f86-4537-94e6-4337195b82e9.jpg (지정된 경로를 찾을 수 없습니다)
...TransactionInterceptor : Completing transaction for [com.shop.service.ItemService.saveItem] after exception: java.io.FileNotFoundException: C:\shop\item\33e0aaf5-7f86-4537-94e6-4337195b82e9.jpg (지정된 경로를 찾을 수 없습니다)
...JpaTransactionManager : Initiating transaction commit
...JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1608174189<open>)]
Hibernate: insert into item (created_by,item_detail,item_nm,item_sell_status,modified_by,price,reg_time,stock_number,update_time,item_id) values (?,?,?,?,?,?,?,?,?,?)
FileNotFoundException
이 터졌지만 예외를 잡은 후 최종 커밋이 이루어지고, item insert 쿼리가 나가는 것을 볼 수 있다.
이는 FileNotFoundException
이 IOException
을 상속받는 Checked Excception 이기 때문이다.
코드 뜯어보기
TransactionAspectSupport.java 670줄을 보면 아래와 같이 “Completing transaction for…” 로그가 찍히는 부분을 볼 수 있다. rollbackOn(ex)
부분을 보면 RuntimeException
과 Error
인 경우 롤백을 하는것으로 나와있다. 때문에 위의 FileNotFoundException
은 예외가 발생했지만 롤백되지 않는다.
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
//여기!!!
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
해결 방법
1) Checked Exception이 아닌 RuntimeException으로 반환한다.
public String uploadFile(String uploadPath, String originalFileName, byte[] fileData) {
UUID uuid = UUID.randomUUID();
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String savedFileName = uuid.toString() + extension;
String fileUploadFullUrl = uploadPath + "/" + savedFileName;
try {
FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
fos.write(fileData);
fos.close();
} catch (Exception e) {
throw new RuntimeException("FileNotFound");
}
return savedFileName;
}
2) rollbackFor 추가
@Transactional
의 rollbackFor를 Exception.class
로 설정하면 모든 예외 발생에 롤백처리를 할 수 있다.
@Transactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = {Exception.class})
public Long saveItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList) throws Exception{
//상품 등록
Item item = itemFormDto.createItem();
itemRepository.save(item);
//이미지 등록
for(int i=0;i<itemImgFileList.size();i++){
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
if(i == 0)
itemImg.setRepimgYn("Y");
else
itemImg.setRepimgYn("N");
//이미지 저장 메서드
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
}
return item.getId();
}
Checked Exception은 이미 알고있는 예외이므로 롤백하지 않는다는 너무 당연한 이야기를 처음 깨달았다…!
참고
https://leejaengjaeng.tistory.com/14
https://velog.io/@kdhyo/JavaTransactional-Annotation-알고-쓰자-26her30h
https://velog.io/@ddongh1122/Spring-Transactional-클래스-내부-호출-미작동-이슈
https://joyykim.tistory.com/24
https://techblog.woowahan.com/2606/
https://wildeveloperetrain.tistory.com/218
'Spring' 카테고리의 다른 글
프레임워크를 사용하는 이유 (0) | 2023.10.06 |
---|---|
Spring Data JPA Projections (0) | 2023.04.24 |