이번 포스팅에서는, 스프링 트랜잭션이 제공하는 다양한 기능들에 대해 정리해보자.
(이전 DB 1편 포스팅을 참고하자)
트랜잭션 적용 확인
트랜잭션이 적용되고 있는지 테스트코드로 한번 확인해보자.
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
BasicService basicService;
@TestConfiguration
static class TxApplyBasicConfig {
@Bean
BasicService basicService() {
return new BasicService();
}
}
@Test
void proxyCheck() {
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);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
@Transactional을 메서드나 클래스에 붙이면 해당 객체는 트랜잭션 AOP 적용 대상이 되며,
결과적으로 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다.
또한 주입 받을 때도 실제 객체 대신에 프록시 객체가 주입된다.
현재 BasicService의 tx() 메서드에 @Transactional이 적용되어있기에, BasicService의 빈이 등록될 때는 실제 객체가 아닌
프록시 객체가 등록될 것이다.
그렇기에 proxyCheck 테스트를 돌린 결과 프록시 객체임을 확인할 수 있다.
프록시는 실제 서비스인 BasicService를 상속해서 만들어지기 때문에 다형성을 활용할 수 있다.
그렇기에 실제 BasicService가 아닌 프록시를 주입할 수 있다.
이제 다시 코드를 봐보자.
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
먼저 tx()를 호출하면 어떻게 될까?
우선 프록시의 tx()가 호출될 것이다.
tx()는 @Transactional이 붙어있으므로, 트랜잭션의 대상이다.
그렇기에 트랜잭션을 시작하고 실제 service의 tx()를 호출한다.
그 이후 실제 service의 tx()의 호출이 끝나고 프록시로 제어가 돌아오면 프록시는 트랜잭션을 커밋하거나 롤백 처리하고 종료할 것이다.
nonTx()는 어떨까?
우선 서비스가 프록시로 먼저 등록되어있기에, 프록시의 nonTx()가 호출될 것이다.
nonTx는 @Transactional이 아니므로, 트랜잭션 적용 대상이 아니다.
그렇기에 트랜잭션을 시작하지 않고, 실제 service의 nonTx()를 호출하고 종료할 것이다.
트랜잭션 적용 위치
스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다!!!
그렇다면 해당 코드를 보고 답변을 해보자.
@Slf4j
@Transactional(readOnly = true)
static class LevelService {
@Transactional(readOnly = false)
public void write() {
log.info("call write");
}
public void read() {
log.info("call read");
}
}
write와 read에는 각각 어떤 트랜잭션이 걸리게 될까?
클래스보다는 메서드가 더 구체적이므로, write에는 메서드에 걸려있는 @Transactional(readOnly = false)가 걸리게 된다.
read에는 클래스에 걸린 @Transactional(readOnly = true)가 걸리게 된다.
트랜잭션 AOP 주의 사항 - 프록시 내부 호출
트랜잭션 AOP는 아까의 설명처럼 프록시 방식의 AOP를 사용한다.
프록시 객체가 먼저 요청을 받아서 트랜잭션을 처리하고, 실제 객체를 호출한다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다.
우리가 AOP를 적용하면, 스프링 빈에 대상 객체가 아닌 프록시가 등록되기 때문에, 우리가 대상 객체를 직접 호출하는 문제는
일반적으로 생기지 않는다.
하지만,
대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall() {
callService.external();
}
static class CallService {
public void external() {
log.info("call external");
internal();
}
@Transactional
public void internal() {
log.info("call internal");
}
}
}
먼저 internalCall() 테스트를 봐보자.
해당 코드에는 트랜잭션이 있다.
프록시가 트랜잭션을 적용하고, 실제 객체 인스턴스의 internal()을 호출할 것이다.
이제 문제가 되는 externalCall() 테스트를 보자.
public void external() {
log.info("call external");
internal();
}
@Transactional
public void internal() {
log.info("call internal");
}
external()은 트랜잭션이 없이 시작한다.
그런데 내부에서 @Transactional이 있는 internal()을 호출하는 것을 확인할 수 있다.
internal()내에서는 당연히 트랜잭션이 적용되지 않을까?? 라는 생각이 든다.
하지만 실제로는 트랜잭션이 수행되지 않는다. 왜 그럴까?
실제 흐름을 봐보자.
callService라는 객체는 트랜잭션이 적용된 internal() 함수를 가지고 있기에 프록시를 생성한다.
그렇기에 callService의 프록시가 호출되었다.
먼저 external은 트랜잭션이 적용되어 있지 않기 때문에 프록시가 아닌 실제 객체 인스턴스의 external()을 호출한다.
그리고 나서 internal()을 호출한다.
그러면 internal()을 호출하는 대상은 프록시 객체일까 , 실제 객체 인스턴스일까?
자바에서 메서드 앞에 별도 참조가 없으면 default로 this가 붙는다. 즉, 자기 자신의 인스턴스를 가리킨다.
그렇기에 현재 internal()을 호출하는 대상은 실제 대상 객체의 인스턴스이다.
즉, 프록시를 거치지 않았기 때문에 트랜잭션이 적용되지 않는다.
그렇다면 해당 문제를 어떻게 해결할 수 있을까?
우선 문제부터 정의해보자.
문제는, 같은 클래스 안에 메서드 내부 호출을 사용하면 프록시를 적용할 수 없다라는 것이다.
그렇다면 같은 클래스가 아닌, 클래스 분리를 통하면 어떻게 될까?
static class CallService {
private final InternalService internalService;
public void external() {
log.info("call internal");
internalService.internal();
}
}
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
}
}
CallService 객체에는 트랜잭션 프록시가 적용되지 않지만,
InternalService 객체는 internal()이 @Transactional이 걸려있기 때문에 트랜잭션 프록시가 적용된다.
'스프링 강의 필기' 카테고리의 다른 글
db 2편 - 스프링 트랜잭션 전파 (1) | 2023.11.27 |
---|---|
DB 강의 1편 - 트랜잭션 문제 해결(2) (1) | 2023.11.09 |
DB 강의 1편 - 트랜잭션 문제 해결(1) (0) | 2023.11.08 |