https://velog.io/@mooh2jj/트랜잭션-이해DB-세션-자동커밋-트랜잭션-시작
를 시작으로 트랜잭션 이야기를 하고 있다. 스프링에서 트랜잭션을 사용할 때 프록시를 사용한다. 이 기술은 AOP기능이기도 하다.
AOP란 관심사를 모아서 모듈화에 앞단에서 처리하는 스프링 기술이다.
스프링에서는 트랜잭션 처리
를 어떻게 하는지 자세히 알아보자.
스프링에서는 트랜잭션 처리
는 너무나 간단하게 이루어질 수 있다.
바로, @Transactional
어노테이션만으로 처리가 가능하기 때문이다.
@Transactionaldms 스프링 AOP 기반으로, 스프링 AOP는 Proxy 기반으로 동작한다.
@Transacational이 포함된 메소드가 호출할 때, 프록시 객체를 생성함으로써 트랜잭션 생성 및 commit 또는 rollback 후 트랜잭션 닫는 부수적인 작업을 프록시 객체에게 위임한다.
위 그림처럼 Spring AOP는 사용자의 특정 호출 시점에 IoC 컨테이너에 의해 AOP를 할 수 있는 Proxy Bean을 생성해준다
.
동적으로 생성된 Proxy Bean은 타깃의 메서드가 호출되는 시점에 부가기능을 추가할 메서드를 자체적으로 판단해 가로채어 부가기능을 주입한다. 이를 호출시점에 동적으로 위빙을 한다해 런타임 위빙(Runtime Weaving)이라고 한다.
Spring AOP는 런타임 위빙 방식을 기반으로 하고 있고, Spring 에서는 상황에 따라 JDK Proxy와 CGLib방식을 통해 Proxy Bean을 생성해준다.
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
BasicService basicService;
@Test
void proxyCheck() {
//BasicService$$EnhancerBySpringCGLIB...
log.info("aop class={}", basicService.getClass());
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
...
선언적 트랜잭션 방식에서 스프링 트랜잭션은 AOP를 기반으로 동작
한다. 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다.
@Transactional 애노테이션이 특정 클래스나 메서드에 하나라도 있으면
트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록한다.
우선순위
스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다. 이것만 기억하면 스프링에서 발생하는 대부분의 우선순위를 쉽게 기억할 수 있다.
예를 들어서 메서드와 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 메서드가 더 높은 우선순위를 가진다.
자동적용
1.클래스의 메서드 (우선순위가 가장 높다.)
2.클래스의 타입
3.인터페이스의 메서드
4.인터페이스의 타입 (우선순위가 가장 낮다.)
AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기때문에 권장하지 않는다.
참고) https://www.youtube.com/watch?v=pq9QB-wGsNs
@Transactional 을 사용하면 스프링의 트랜잭션 AOP가 적용된다.
트랜잭션 AOP는 기본적으로 프록시 방식의 AOP
를 사용한다.
앞서 배운 것 처럼 @Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다.
하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다. 이렇게 되면 @Transactional이 있어도 트랜잭션이 적용되지 않는다. 실무에서 반드시 한번은 만나서 고생하는 문제라고 한다.
내부 호출
https://velog.io/@dodo4723/스프링-DB-2-정리-9.-스프링-트랜잭션-이해-22.8.31 참고
@Slf4j
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있다.
@Transactional
public class Hello {
public method1();
method2():
protected method3();
private method4();
}
이렇게 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있다. 그러면 트랜잭션을 의도하지 않는 곳 까지 트랜잭션이 과도하게 적용된다.
트랜잭션은 주로 비즈니스 로직의 시작점에
걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다.
@PostConstruct
@Transactional
public void initV1() {
log.info("Hello init @PostConstruct");
}
초기화 코드 (예: @PostConstruct)와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.
왜냐하면 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다.
가장 확실한 대안은 ApplicationReadyEvent 이벤트를 사용하는 것이다.
@EventListener(value = ApplicationReadyEvent.class)
예외 발생시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
체크 예외인 Exception 과 그 하위 예외가 발생하면 트랜잭션을 커밋한다.
기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다.
@Transactional(rollbackFor = Exception.class)
체크 예외인 Exception 이 발생해도 커밋 대신 롤백된다.
스프링 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용
하고, 런타임(언체크) 예외는 복구 불가능한 예외로 가정한다.
비즈니스 예외?
주문을 하는데 상황에 따라 다음과 같이 조치한다.
정상: 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료로 처리한다.
시스템 예외
: 주문시 내부에 복구 불가능한 예외가 발생하면 전체 데이터를 롤백한다.비즈니스 예외
: 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기 로 처리한다. 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내한다.
이때 결제 잔고가 부족하면NotEnoughMoneyException 이라는 체크 예외가 발생한다고 가정하자. 이 예외는 시스템은 정상 동작했지만, 비즈니스 상황에서 문제가 되기 때문에 발생한 예외
인 것이다.
잔고 부족은 결제 상태를 대기 상태로 두고, 체크 예외가 발생하지만, order 데이터는 커밋되기를 기대한다.
NotEnoughMoneyException
은 시스템에 문제가 발생한 것이 아니라, 비즈니스 문제 상황을 예외를 통해 알려준다. 마치 예외가 리턴 값 처럼 사용된다. 따라서 이 경우, 롤백하면 생성한 Order 자체가 사라지기 때문에 커밋해야한다.