스프링 DB 2편 - 데이터 접근 활용 기술의 복습을 위한 글이며,
이 글에 나오는 모든 사진과 코드의 저작권은 김영한 강사님께 있습니다.
1. 스프링 AOP 주의 사항 - 초기화 시점
InitTest
@PostConstruct 와 @Transactional 을 함께 사용하면 초기화 코드가 먼저 실행되고, 그 다음에 트랜잭션 AOP가 적용되기 때문에 초기화 시점에 아래 메소드에서는 트랜잭션이 적용되지 않는다.
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}

ApplicationReadyEvent를 사용하면 트랜잭션 AOP를 포함한 스프링 컨테이너가 생성되고 나서 이벤트가 붙은 메소드를 호출해준다. 따라서 트랜잭션이 적용된다.
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}

코드 전문이다.
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
void go() {
// 초기화 코드는 스프링이 초기화 시점이 호출한다.
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
}
2. 트랜잭션 옵션
2.1 value, transactionManager
: 사용할 트랜잭션 매니저를 지정해주는 옵션이다.
사용하는 트랜잭션이 둘 이상이라면 다음과 같이 트랜잭션 매니저의 이름을 구분해서 사용하면 된다.
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
@Transactional("orderTxManager")
public void order() {...}
}
2.2 rollbackFor
: 어떤 예외가 발생할 때 롤백할지 지정할 수 있다.
@Transactional(rollbackFor = Exception.class)
2.3 noRollbackFor
: rollbackFor의 반대 버전이다. 어떤 예외가 발생했을 때 롤백을 금지한다.
2.4 isolation
: 트랜잭션 격리 수준을 지정할 수 있다.
기본값은 DB에서 설정한 Default 값이다. 격리 수준은 Spring DB #3. 트랜잭션(Transaction) 글을 참고하자.
2.5 timeout
: 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정한다.
기본값은 트랜잭션 시스템의 타임아웃을 사용한다.
2.6 readOnly
: readOnly=true옵션을 사용하면 읽기 전용 트랜잭션이 생성된다. 이 경우 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다.
3. 예외와 트랜잭션 커밋, 롤백
예외를 처리하지 못하고 트랜잭션 적용 범위(@Transactional) 밖으로 던지는 경우를 살펴보자.

이런 경우에는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
- 체크 예외인 경우 :
Exception과 하위 예외가 발생하면 트랜잭션을 커밋한다. - 언체크 예외인 경우 :
RuntimeException과Error의 하위 예외가 발생하면 트랜잭션을 롤백한다.
RollbackTest
RuntimeException가 발생했으므로 롤백한다.
// 런타임 예외 발생 : 롤백
@Transactional
public void runtimeException() {
log.info("call runtimeException");
throw new RuntimeException();
}

체크 예외를 상속받은 MyException이 발생했으므로 커밋된다.
// 체크 예외 발생 : 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}

Exception 예외가 발생했을 때 롤백하라고 지정해놨음으로 롤백된다.
@Transactional(rollbackFor = Exception.class)
// 체크 예외 rollbackFor 지정 : 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call checkedException");
throw new MyException();
}

다음은 코드 전문이다.
@SpringBootTest
public class RollbackTest {
@Autowired
RollbackService service;
@Test
void runtimeException() {
Assertions.assertThatThrownBy(() -> service.runtimeException())
.isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
Assertions.assertThatThrownBy(() -> service.checkedException())
.isInstanceOf(MyException.class);
}
@Test
void rollbackFro() {
Assertions.assertThatThrownBy(() -> service.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 checkedException");
throw new MyException();
}
}
static class MyException extends Exception {
}
}
4. 예외와 트랜잭션 커밋, 롤백 - 활용
스프링 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임(언체크) 예외는 복구 불가능한 예외로 가정한다.
- 체크 예외 : 비즈니스 의미가 있을 때 사용
- 언체크 예외 : 복구 불가능한 예외
4.1 비즈니스 예외란?
비즈니스 예외란 시스템에 문제가 있는 것이 아닌 비즈니스 상황에서 발생한 예외를 뜻한다. 예제를 통해서 살펴보자.
- 정상 : 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료 로 처리한다.
- 시스템 예외 : 주문시 내부에 복구 불가능한 예외가 발생하면 전체 데이터를 롤백한다.
- 비즈니스 예외 : 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기 로 처리한다. 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내한다.
고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내하는 것이 비즈니스 예외에 속한다. 코드로 살펴보자.
NotEnoughMoneyException
public class NotEnoughMoneyException extends Exception {
public NotEnoughMoneyException(String message) {
super(message);
}
}
Order
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id
@GeneratedValue
private Long id;
private String username; // 정상, 예외, 잔고부족
private String payStatus; // 대기, 완료
}
OrderRepository
public interface OrderRepository extends JpaRepository<Order, Long> {
}
OrderService
여러 상황을 만들기 위해 사용자 이름(username)에 따라 처리 프로세스를 다르게 했다.
@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
정상 프로세스 흐름이다.
@Test
void complete() throws NotEnoughMoneyException {
// given
Order order = new Order();
order.setUsername("정상");
// when
orderService.order(order);
// then
Order findOrder = orderRepository.findById(order.getId()).get();
assertThat(findOrder.getPayStatus()).isEqualTo("완료");
}

RuntimeException("시스템 예외") 가 발생한다. 런타임 예외로 롤백이 수행되었다.
@Test
void runtimeException() throws NotEnoughMoneyException {
// 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();
}

고객에게 잔고 부족을 알리는 테스트이다. NotEnoughMoneyException("잔고가 부족합니다") 예외가 발생한다. 이후 assertThat(findOrder.getPayStatus()).isEqualTo("대기")로 데이터가 대기 상태로 잘 저장되었는지 검증해야 한다.
@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("대기");
}

테스트 코드 전문이다.
@Slf4j
@SpringBootTest
public class OrderServiceTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void complete() throws NotEnoughMoneyException {
// given
Order order = new Order();
order.setUsername("정상");
// when
orderService.order(order);
// then
Order findOrder = orderRepository.findById(order.getId()).get();
assertThat(findOrder.getPayStatus()).isEqualTo("완료");
}
@Test
void runtimeException() throws NotEnoughMoneyException {
// 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("대기");
}
}
'DataBase > Spring - DB' 카테고리의 다른 글
| Spring DB 2 #9. 스프링 트랜잭션 이해 (1) (0) | 2023.03.02 |
|---|---|
| Spring DB 2 #8. 데이터 접근 기술 - 활용 (0) | 2023.03.02 |
| Spring DB 2 #7. 데이터 접근 기술 - QueryDSL (0) | 2023.03.01 |
| Spring DB #6. 예외 처리, 반복 해결 (0) | 2023.02.27 |
| Spring DB #5. 자바 예외 (0) | 2023.02.20 |