- Published on
Spring Transaction 예외와 커밋, 롤백
- Authors
- Name
- ywj9811
예외와 트랜잭션 커밋, 롤백 기본
프로젝트를 진행하던 도중 예외가 발생했는데, 내부에서 예외를 처리하지 못하고 트랜잭션 범위(@Transactional
가 적용된 AOP) 밖으로 예외를 던지면 어떻게 될까?
예외 발생시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
- 언체크 예외인
RuntimeException
,Error
와 그 하위 예외가 발생하면 롤백 - 체크 예외인
Exception
과 그 하위 예외들은 커밋
이 동작을 코드로 확인해보자.
@SpringBootTest
public class RollbackTest {
@Autowired
RollbackService rollbackService;
@Test
void runtimeException() {
Assertions.assertThatThrownBy(() -> rollbackService.runtimeException())
.isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
Assertions.assertThatThrownBy(() -> rollbackService.checkedException())
.isInstanceOf(MyException.class);
}
@Test
void rollbackFor() {
Assertions.assertThatThrownBy(() -> rollbackService.rollbackFor())
.isInstanceOf(MyException.class);
}
@TestConfiguration
static class RollbackTestConfig {
@Bean
RollbackService rollbackService() {
return new RollbackService();
}
}
@Slf4j
static class RollbackService {
//런타임 예외 발생 : 롤백
@Transactional
public void runtimeException() {
log.info("call runtimeException");
throw new RuntimeException();
}
//체크 예외 발생 : 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}
//체크 예외 rollbackFor 지정 : 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call rollbackFor");
throw new MyException();
}
}
static class MyException extends Exception {}
}
위의 코드를 통해 테스트를 진행할 것이다.
이때 롤백 되는지 커밋 되는지 정보 로그를 찍기 위해 application.properties
에 다음과 같은 기능을 넣어주도록 하자.
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
지금은 JPA를 사용하기 때문에 JpaTransactionManager
가 실행되고, 여기의 로그를 출력하는 것이다.
테스트 코드를 이제 하나하나 실행시키도록 하자.
예외와 트랜잭션 커밋, 롤백 활용
지금까지 스프링에서 체크 예외는 커밋하고, 언체크 예외는 롤백하는 모습을 확인했다.
그렇다면 왜 체크 예외는 커밋하고 언체크 예외는 롤백하는 것일까?
스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 언체크 예외는 복구 불가능한 예외로 가정하기 때문이다.
따라서 상황에 따라서 rollbackFor 옵션을 활용하는 것이다.
그렇다면 비즈니스 의미가 있는 비즈니스 예외란 무슨 뜻일까?
그에 대해 예제를 통해서 살펴보도록 하자.
- 비즈니스 요구사항 주문을 하는데 상황에 따라 다음과 같이 조치한다.
- 정상 : 주문시 결제를 성곡하면 주문 데이터를 저장하고 결제 상태를 완료로 처리
- 시스템 예외 : 주문시 내부에 복구가 불가능한 예외가 발생하면 전체 데이터를 롤백
- 비즈니스 예외 : 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기로 처리
- 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내
이때 잔고가 부족하면 NotEnoughMoneyException
이라는 체크 예외가 발생한다고 가정하자.
이 예외는 시스템에 문제가 있어서 발생한 시스템 예외가 아닌 비즈니스 상황에서 고객의 사정으로 발생한 예외인 것이다.
이런 예외는 매우 중요하고 반드시 처리해야 하는 경우가 많으므로 체크 예외를 고려할 수 있는 것이다.
이제 코드를 살펴보자.
NotEnoughMoneyException
public class NotEnoughMoneyException extends Exception {
public NotEnoughMoneyException(String message) {
super(message);
}
}
결제 잔고가 부족하면 발생하는 예외로 Exception
을 상속 받아서 체크 예외가 된다.
Order
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
private Long id;
private String userName; //정상, 예외, 잔고 부족 이렇게 넘길겨
private String payStatus; //대기, 완료
}
JPA를 사용하는 Order 엔티티이다.
현재는 간단하게 구현하기 위해서 @Getter
, @Setter
를 사용하였지만 사실 엔티티에 @Setter
를 사용하는 것은 좋지 않다.
@Table(name = "orders")
는 자동으로 테이블을 생성할 때 이름이 order가 되어 오류가 발생하는 것을 막기 위해 따로 이름을 지정해준 것이다.
테이블의 이름이 order가 되는 경우 예약어와 중복되어 오류가 발생할 수 있다.
- id는 Pk키로 사용하기 위해
- userName은 정상, 예외, 잔고 부족 이런 식으로 처리 프로세스를 위해
- payStatus는 결제 결과를 위해
OrderRepository
public interface OrderRepository extends JpaRepository<Order, Long> {}
스프링 데이터 JPA를 사용한다.
따라서 기본으로 제공되는 함수만 사용하기 때문에 인터페이스만 생성했다.
OrderService
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
//JPA는 트랜잭션 커밋 시점에 Order 데이터를 DB에 반영한다.
@Transactional
public void order(Order order) throws NotEnoughMoneyException {
log.info("order 호출");
orderRepository.save(order);
log.info("결제 프로세스 진입");
if (order.getUserName().equals("예외")) {
log.info("시스템 예외 발생");
throw new RuntimeException("시스템 예외");
} else if (order.getUserName().equals("잔고부족")) {
log.info("잔고 부족 비즈니스 예외 발생");
order.setPayStatus("대기");
throw new NotEnoughMoneyException("잔고가 부족합니다.");
} else {
log.info("정상 승인");
order.setPayStatus("완료");
}
log.info("결제 프로세스 완료");
}
}
이렇게 간단하게 코드를 작성하고 테스트 코드를 통해 작동을 확인하도록 하자.
OrderServiceTest
@Slf4j
@SpringBootTest
class OrderServiceTest {
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
void complete() throws NotEnoughMoneyException {
//given
Order order = new Order();
order.setUserName("정상");
//when
try {
orderService.order(order);
} catch (NotEnoughMoneyException e) {
log.info("고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내");
}
//then
Order findOrder = orderRepository.findById(order.getId()).get();
assertThat(findOrder.getPayStatus()).isEqualTo("완료");
}
@Test
void runtimeException() {
//given
Order order = new Order();
order.setUserName("예외");
//when
assertThatThrownBy(() -> orderService.order(order))
.isInstanceOf(RuntimeException.class);
//then
Optional<Order> orderOptional = orderRepository.findById(order.getId());
assertThat(orderOptional.isEmpty()).isTrue();
}
@Test
void bizException() {
//given
Order order = new Order();
order.setUserName("잔고부족");
//when
try {
orderService.order(order);
} catch (NotEnoughMoneyException e) {
log.info("고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내");
}
//then
Order findOrder = orderRepository.findById(order.getId()).get();
assertThat(findOrder.getPayStatus()).isEqualTo("대기");
}
}
각각의 테스트를 실행하고 그 결과를 살펴보도록 하자.
이렇게 간단한 예제를 통해서 비즈니스 예외 상황과 시스템 예외 상황을 가정하여 각각의 상황에서 롤백되거나 커밋되는 모습을 확인할 수 있었다.
물론 원하는 경우 rollbackFor를 통해서 비즈니스 상황에 따라서 롤백을 선택하며 사용하면 되는 것이다.