체크예외를 커밋하는 이유는
스프링 설계 자체가 비즈니스 예외는 체크예외, 언체크예외는 복구 불가능 한 예외라고 가정하고 설계 됨.
물론 꼭 따를 필요는 없음.
그냥 rollbackFor 이런 거 써도 되고.
비즈니스 예외라는 것은,
예를 들어 잔고부족 같은 거.
보통 이런 건 우리가 따로 비즈니스 적으로 예를 들어 잔고가 부족합니다! 이런 메시지를 클라이언트에게 전해주게 하던지 처리 해야 하니까.
주문데이터를 일단 저장하고, 결제 상태를 대기로 만들고, 고객에게 잔고 부족을 알리는 등.
이렇게 시스템 자체는 문제가 없지만 비즈니스 상황에 대해 있는 문제를 비즈니스 예외.
이런 비즈니스 예외는 굉장히 중요하고 반드시 처리해야 하는 경우가 많아서 체크 예외를 고려.
그래서 보통 예외 구분은
시스템 장애 예외
비즈니스 예외
이 두가지로 구분하고
시스템 장애는 언체크,
비즈니스 예외는 체크 예외로 해서 필요하면 처리할 수 있도록.
지금 만들어 볼 예외는,
고객이 주문 했는데 잔고가 부족할 경우,
주문 DB에는 데이터가 남지만, 상태는 '대기'로 놓는 상태. 즉, 주문DB에 저장하는 것은 맞으나(커밋) 상태는 대기로.
public class NotEnoughMoneyException extends Exception{
public NotEnoughMoneyException(String message) {
super(message);
}
}
일단 예외 코드.
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
private Long id;
private String username; //정상, 예외, 잔고부족
private String payStatus; //대기, 완료
}
엔티티 만듦.
저 @Table이 어떤 테이블인지 정할 수 있음. 보통 엔티티 이름에 따라가지만, Order by 이런 것 처럼 보통 DB SQL의 예약어인 경우가 많아 저렇게 orders라고 많이 씀.
에러 날 시 payStatus를 바꿔야 하니까 @Setter 씀. 남발하는 것은 좋지 않으나 필요하면 써야지.
원래 저런 거 뭐 Enum이나 그런 거 쓰겠지만 연습이니까 대충 저렇게 한다고 함.
또 username도 저렇게 하는 거 이상하기는 한데,
그냥 단순하게 하려고 저렇게 한다고 함.
저걸로 어떤 상태로 테스트 할지 정하려고
public interface OrderRepository extends JpaRepository<Order, Long> {
}
SpringDataJPA 사용으로 자동으로 리포지토리 생성.
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
@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("결제 프로세스 완료.");
}
}
서비스 로직이라고 해 봤자.
그냥 들어오는 order의 username에 따라(이름 username으로 쓰는 것도 이상하긴 한데. )
예외 발생, 그리고 해야 할 일(payStatus 바꾸는 거) 정도.
@Slf4j
@SpringBootTest
class OrderServiceTest {
@Autowired
OrderRepository orderRepository;
@Autowired
OrderService orderService;
@Test
void complete() throws NotEnoughMoneyException {
Order order = new Order();
order.setUsername("정상");
orderService.order(order);
Order findedOrder = orderRepository.findById(order.getId()).get();
assertThat(findedOrder.getPayStatus()).isEqualTo("완료");
}
@Test
void runtimeException() throws NotEnoughMoneyException {
Order order = new Order();
order.setUsername("예외");
assertThatThrownBy(()->orderService.order(order))
.isInstanceOf(RuntimeException.class);
Optional<Order> findedOrderOptional = orderRepository.findById(order.getId());
assertThat(findedOrderOptional.isEmpty()).isTrue();
}
@Test
void bizException() {
Order order = new Order();
order.setUsername("잔고부족");
try {
orderService.order(order);
} catch (NotEnoughMoneyException e) {
log.info("고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내");
}
Order findedOrder = orderRepository.findById(order.getId()).get();
assertThat(findedOrder.getPayStatus()).isEqualTo("대기");
}
}
정상 로직
그냥 Service 로직에 따라 제대로 Commit 되고,
order의 payStatus가 '완료'가 되었는지.
두번째 런타임오류, 시스템 오류를 상정한.
이거는 그냥 하면 오류가 나와야 함. 그리고 당연히 언체크 예외이기에 롤백되어 DB에 저장이 안됨.
그래서 리포지토리에서 가져와봐도 비어있음.
마지막 비즈니스 에러
주문하고 에러를 catch 해봄.
보통 컨트롤러가 서비스를 호출해서 쓰니, 테스트는 컨트롤러의 메소드 같은 느낌.
그래서 뭐 catch에서 비즈니스 예외 시 처리할 로직을 쓰는거임.
여튼 Service의 로직에 따라 order는 체크예외이기에 예외더라도 정상적으로 Commit이 되고,
그 찾은 Order의 payStatus는 '대기'
뭐 그리고, Service에서 비즈니스 예외는 그냥 예외를 날리도록 해놓긴 했는데,
그냥 return enum.잔고부족
이런 식으로 해도 됨.
이거는 프로젝트에 따라 선택임.
아 그리고 우리가 Orders 테이블도 만들지 않았는데 만들어 진 이유는,
이렇게 별도의 DB설정 없이(그럼 메모리 상에 DB를 만듦) 테스트 환경에서 테스트를 실행하면 메모리에 테이블을 만들어 줌.
실제로 로그 보면 create table orders 하면서 찍혀 있음.
그리고 뭐 나도 스프링 표준이 체크예외를 commit이라고 해서 그렇게 쓸 것 같기는 한데 대부분,
이것도 선택이라고 함.
가끔 legacy 프로젝트를 할 때 체크 예외를 rollbackFor 해야 하는 경우도 있다고 함.
'스프링 > 6. 스프링 DB-2' 카테고리의 다른 글
49. 스프링 트랜잭션 전파 2 (0) | 2023.10.16 |
---|---|
48. 스프링 트랜잭션 전파 (0) | 2023.10.16 |
46. 트랜잭션과 예외 (0) | 2023.10.16 |
44. 트랜잭션 AOP 주의사항 -초기화 시점 (0) | 2023.10.15 |
43. 트랜잭션 AOP 주의사항 2 -프록시 내부호출 (0) | 2023.10.15 |