@Transactional 에는 rollbackFor 옵션이 있다.
해당 옵션의 주석을 확인해 보았더니
By default, a transaction will be rolled back on RuntimeException and Error but not on checked exceptions (business exceptions). See org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable) for a detailed explanation.
Transaction은 Unchecked Exception(RuntimeException 하위 클래스)과 Error에만 roll back을 수행하고,
Checked Exception에는 roll back 되지 않는다고 나와있다.
좀 더 자세하게 알아보도록 하자
🤔 Unckecked Exception과 Checked Exception의 차이가 뭘까?
자바에서 예외는 크게 3가지로 나눌 수 있다.
- Error
- Checked Exception
- Unckecked Exception
프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를 프로그램 오류라 하고, 프로그램 오류는 에러(error)와 예외(exception) 두 가지로 구분할 수 있다.
🐞 에러(Error) 란?
컴파일 시 문법적인 오류와 런타임 시 널포인트 참조, 메모리 부족, 스택오버플로우 같이 발생하면 복구할 수 없는 심각한 오류로 프로세스에 심각한 문제를 야기시켜 프로세스를 종료시킬 수 있다.
🌲 예외(Exception) 란?
예외는 프로그램 실행 중에 개발자의 실수로 예기치 않은 상황이 발생하여 수행 중인 프로그램이 영향을 받는 것이다.
초기에 정해진 배열의 크기보다 더 많은 양의 데이터를 넣고자 할 때 주로 발생하는 ArrayIndexOutOfBoundsException 나 실제 값이 아닌 null을 가지고 있는 객체나 변수를 호출할 때 발생하는 NullPointerException 등을 예로 들 수 있다.
예외는 발생하더라도 수습할 수 있는 비교적 덜 심각한 오류이며 try-catch문을 통한 Exception handling으로 해당 상황을 미리 방지할 수 있다.
예외는 2가지로 나눌 수 있다.
- Checked Exception
- Unchecked Exception
위의 자바 에러 클래스의 계층 구조를 보았을 때 RuntimeException의 하위 클래스들을 Unchecked Exception이라 하고 RuntimeException의 하위 클래스가 아닌 Exception 클래스의 하위 클래스들을 Checked Exception이라 한다.
🌱 체크 예외(Checked Exception)
체크 예외는 RuntimeException의 하위 클래스가 아닌 Exception 클래스의 하위 클래스들이다.
체크 예외는 반드시 에러 처리를 해야 하는 특징 (try/catch or throw)을 가지고 있다.
- 존재하지 않는 파일의 이름을 입력(FileNotFoundException)
- 실수로 클래스의 이름을 잘못 적음(ClassNotFoundException)
🌱 언체크 예외(Unchecked Exception)
언체크 예외는 RuntimeException의 하위 클래스들이다.
언체크 예외는 실행 중에(runtime) 발생할 수 있는 예외를 의미하며 체크 예외와는 달리 에러 처리를 강제하지 않는다.
- 배열의 범위를 벗어난(ArrayIndexOutOfBoundsException)
- 값이 null이 참조변수를 참조(NullPointerException)
🧐 그냥 @Transaction 어노테이션을 사용했을 때에는 어떻게 되는 거야?
내가 개발하던 프로젝트의 상품 좋아요 생성 API로 테스트를 진행했다.
- Product
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
private Integer price;
@Builder
public Product(Long id, String name, Integer price) {
this.id = id;
this.name = name;
this.price = price;
}
}
- ProductLike
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "product_id"})})
public class ProductLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
@Builder
public ProductLike(Long id, User user, Product product) {
this.id = id;
this.user = user;
this.product = product;
}
}
- ProductRepository, ProductLikeRepository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
public interface ProductLikeRepository extends JpaRepository<ProductLike, Long> {
}
ProductController에 로그인 한 유저와 상품 id를 받아 상품 좋아요를 생성하는 API를 추가했다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@PostMapping("/{productId}/like")
public SuccessResponse<Void> createProductLike(@LoginUser User user,
@PathVariable Long productId) throws Exception {
productService.createProductLike(user, productId);
return SuccessResponse.create();
}
}
ProductService에
- product id가 1L일 경우 Checked Exception
- product id가 2L일 경우 Unchecked Exception
- product id가 3L일 경우 Error
로 분기 처리해 보았다.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final ProductLikeRepository productLikeRepository;
@Transactional
public void createProductLike(User user, Long productId) throws Exception {
Product product = productRepository.findById(productId)
.orElseThrow(() -> NOT_FOUND_PRODUCT);
ProductLike productLike = ProductLike.builder()
.user(user)
.product(product)
.build();
productLikeRepository.save(productLike);
product.plusLikeCount();
if (productId == 1L) {
// Exception (Checked Exception)
throw new ClassNotFoundException();
}
if (productId == 2L) {
// RuntimeException (Unchecked Exception)
throw new NullPointerException();
}
if (productId == 3L) {
/// Error
throw new IllegalAccessError();
}
}
포스트맨에서 차례대로 productId가 1, 2, 3인 경우로 API를 호출해 보았을 때
product ID가 2L, 3L인 경우 roll back 되어 ProductLike가 추가되지 않았지만
product ID가 1L인 경우(Exception인 경우) error가 발생해 임의로 정의해 놓은 ClassNotFoundException을 던졌지만
roll back이 되지 않고 productLike 값이 DB에 저장되어 있었다.
🥸 나는 모든 Exception에서 roll back을 하고 싶어요
어떤 경우의 에러에서도 roll back을 하고 싶은 경우 맨 위에서 언급했던 rollbackFor 옵션을 붙이면 된다.
Exception.class는 모든 예외의 상단에 위치해 있기 때문에 Exception.class를 roll back 조건으로 지정하면 아까는 roll back 되지 않았던 Checked Exception에서도 roll back이 된 것을 확인할 수 있었다.
@Transactional(rollbackFor = Exception.class)
public void createProductLike(User user, Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> NOT_FOUND_PRODUCT);
ProductLike productLike = ProductLike.builder()
.user(user)
.product(product)
.build();
productLikeRepository.save(productLike);
product.plusLikeCount();
}
❗️특정 에러에만 roll back을 하고 싶으면?
나의 경우 모든 경우에서 roll back을 하고 싶었기 때문에 rollbackFor을 Exception.class로 지정해 주었지만
특정 예외에 대해 롤백하고 싶다면, 해당 예외 클래스를 rollbackFor에 적어주면 된다.
https://devlog-wjdrbs96.tistory.com/351
https://gyoogle.dev/blog/computer-language/Java/Error%20&%20Exception.html
https://da-nyee.github.io/posts/spring-transactional-rollbackfor-exceptionclass/
'Back-end' 카테고리의 다른 글
[Spring Boot] yml 파일에 작성한 정보로 어떻게 OAuth 설정을 할까? (0) | 2023.05.22 |
---|---|
[Java] HashTable VS HashMap(Linked List / Red-Black Tree) (0) | 2023.05.04 |
[git] git stash drop 복구 (0) | 2023.03.13 |
JPA Entity @Builder 등 어노테이션 주의점 (0) | 2023.03.06 |
[Gradle] Spring boot 3.0.0 이상 Querydsl build.gradle + 멀티 모듈 프로젝트 (0) | 2023.02.27 |