스프링부트 자바 기초 되새기기
트랜잭션
스프링에서 트랜잭션은 데이터의 일관성과 무결성을 지키기 위한 핵심 개념이다.
하나의 작업 단위를 여러 단계로 나눠 처리할 때, 도중에 오류가 발생하면 이전 작업까지 모두 되돌려야 한다.
이 과정을 스프링에서는 선언적으로 관리할 수 있다.
트랜잭션을 직접 관리하려면 코드가 복잡해지지만, 스프링에서는 `@Transactional` 하나로 이를 간단히 제어할 수 있다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
paymentService.pay(order.getAmount());
}
}
`@Transactional`이 붙은 메서드는 하나의 트랜잭션으로 실행된다.
중간에 예외가 발생하면 전체 작업이 롤백된다.
즉, 결제 실패 시 주문 데이터도 저장되지 않는다.
스프링의 트랜잭션 관리자는 내부적으로 AOP를 활용한다.
프록시(proxy)를 생성해 메서드 실행 전후에 트랜잭션을 시작하고, 성공 시 커밋, 실패 시 롤백을 수행한다.
개발자는 비즈니스 로직만 작성하면 되고, 트랜잭션의 경계 처리는 스프링이 대신 처리한다.
예외 처리 흐름
스프링부트에서는 예외가 발생했을 때 일관된 방식으로 응답을 반환할 수 있다.
`@RestControllerAdvice`와 `@ExceptionHandler`를 활용하면 전역적인 예외 처리가 가능하다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest().body("잘못된 요청입니다: " + e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception e) {
return ResponseEntity.internalServerError().body("서버 오류가 발생했습니다.");
}
}
위 코드처럼 특정 예외 유형마다 다른 응답을 정의할 수 있다.
이 덕분에 컨트롤러 코드에서는 예외를 직접 처리하지 않아도 된다.
또한 비즈니스 로직이 실패했을 때 단순히 예외를 던지면, 전역 핸들러가 알아서 처리한다.
트랜잭션과 예외 처리는 함께 동작한다.
기본적으로 스프링은 런타임 예외(RuntimeException)가 발생하면 자동으로 롤백한다.
체크 예외(Checked Exception)는 명시적으로 롤백 설정을 추가해야 한다.
@Transactional(rollbackFor = Exception.class)
public void processOrder(Order order) throws Exception {
orderRepository.save(order);
throw new Exception("테스트용 예외");
}
이렇게 `rollbackFor` 속성을 추가하면 체크 예외도 롤백 대상이 된다.
상황에 따라 커스텀 예외를 정의해 도메인별 오류를 구분하면 훨씬 관리하기 쉽다.
마치며
트랜잭션은 데이터의 안전장치이고, 예외 처리는 시스템의 방어선이다.
이 두 가지를 올바르게 설계하면 서비스의 안정성이 크게 높아진다.
스프링부트는 `@Transactional`과 `@RestControllerAdvice`를 통해 복잡한 제어 흐름을 단순하게 만들어준다.
결국 스프링의 철학은 일관됨을 느꼈다.
개발자는 비즈니스 로직에 집중하고, 나머지는 프레임워크가 책임진다.