1. 알고자 하는 것
- AOP 개념을 다시 한번 공부하며 프록시와 내부메서드 호출에 따른 트랜잭션 적용 결과를 테스트 해보고자 한다.
- 다음과 같은 메서드(외부)와 해당 메서드 내에서 호출하는 메서드(내부)에 각각 @Transactional 어노테이션을 적용하였을 때 각 메서드에 대한 트랜잭션 적용 결과를 확인한다.
1. outerMethod (@Transactional) & innerMethod (@Transactional)
2. outerMethod (@Transactional) & innerMethod
3. outerMethod & innerMethod (@Transactional)
@Slf4j
@Service
public class SimpleService {
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
innerMethod();
log.info("==== outerMethod end ====");
}
public void innerMethod() {
log.info("==== innerMethod start ====");
log.info("==== innerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
log.info("==== innerMethod end ====");
}
}
- 테스트코드는 다음과 같다.
- 단순히 outerMethod를 호출하고, 로그를 통해 각 메서드의 트랜잭션 적용 여부를 확인한다.
@SpringBootTest
class SimpleServiceTest {
@Autowired
private SimpleService simpleService;
@Test
void transaction_test() {
simpleService.outerMethod();
}
}
2. 알게된 것
outerMethod (@Transactional) & innerMethod (@Transactional)
@Slf4j
@Service
public class SimpleService {
@Transactional
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
innerMethod();
log.info("==== outerMethod end ====");
}
@Transactional
public void innerMethod() {
log.info("==== innerMethod start ====");
log.info("==== innerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
log.info("==== innerMethod end ====");
}
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====
- outer, inner 메서드 모두 트랜잭션이 적용된다.
- 스프링에서 트랜잭션 전파(Transaction Propagation) 타입의 기본값은 REQUIRED 이다.
- REQUIRED 타입은 진행중인 트랜잭션 내부에 새로운 트랜잭션이 들어온다면 기존 트랜잭션에 참여하는 전파 방식이다.
- 이러한 이유로, outerMethod의 트랜잭션에 innerMethod의 트랜잭션이 참여하는 구조가 된다.
outerMethod (@Transactional) & innerMethod
@Slf4j
@Service
public class SimpleService {
@Transactional
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
innerMethod();
log.info("==== outerMethod end ====");
}
public void innerMethod() {
log.info("==== innerMethod start ====");
log.info("==== innerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
log.info("==== innerMethod end ====");
}
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====
- outer, inner 메서드 모두 트랜잭션이 적용된다.
- outerMethod는 @Transactional 어노테이션을 통해 트랜잭션이 시작되어 있는 상태이다.
- outerMethod가 종료되기 전에(트랜잭션이 닫히기 전에) innerMethod가 호출되었다.
- 따라서, innerMethod는 outerMethod의 트랜잭션을 사용한다.
outerMethod & innerMethod (@Transactional)
@Slf4j
@Service
public class SimpleService {
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
innerMethod();
log.info("==== outerMethod end ====");
}
@Transactional
public void innerMethod() {
log.info("==== innerMethod start ====");
log.info("==== innerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
log.info("==== innerMethod end ====");
}
}
==== outerMethod start ====
==== outerMethod transaction Active : false
==== innerMethod start ====
==== innerMethod transaction Active : false
==== innerMethod end ====
==== outerMethod end ====
- outer, inner 메서드 모두 트랜잭션이 적용되지 않는다.
- 흔히 표현하는 '프록시 객체 내부 메서드 호출 시 트랜잭션 적용이 안되는' 문제이다.
- 스프링은 @Transactional이 붙은 메서드를 가지는 / @Transactional이 붙은 클래스에 대해 프록시 객체를 Bean으로 등록한다.
- CGLIB라는 바이트 조작 기술을 통해 해당 클래스를 상속 받은 프록시 객체로 @Transactional이 붙은 메서드를 오버라이딩 해 트랜잭션 관련 코드를 삽입한다.
- 테스트 코드에서 호출된 simpleService.outerMethod()는 사실 프록시 객체의 outerMethod를 호출한 것이다.
- 이 때, 프록시 객체 내부에서 실제 simpleService의 outerMethod()를 호출한다. (target.outerMethod())
- outerMethod 내에서 innerMethod를 호출하므로 다음과 같이 동작한다.
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
this.innerMethod();
log.info("==== outerMethod end ====");
}
- 즉, 트랜잭션 관련 코드가 존재하지 않는 실제 객체의 innerMethod가 호출되어 트랜잭션이 적용되지 않는다.
- 이를 해결하기 위해서 @Transactional을 외부 메서드에 붙이는 것이 권장된다.
- 내부메서드에 @Transactional을 붙여야 한다면 별도의 클래스를 생성하여 해당 메서드를 사용해야한다.
@Slf4j
@Service
public class SimpleInternalService {
@Transactional
public void innerMethod() {
log.info("==== innerMethod start ====");
log.info("==== innerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
log.info("==== innerMethod end ====");
}
}
@Slf4j
@Service
public class SimpleService {
private final SimpleInternalService simpleInternalService;
public SimpleService(SimpleInternalService simpleInternalService) {
this.simpleInternalService = simpleInternalService;
}
public void outerMethod() {
log.info("==== outerMethod start ====");
log.info("==== outerMethod transaction Active : {}",
TransactionSynchronizationManager.isActualTransactionActive());
simpleInternalService.innerMethod();
log.info("==== outerMethod end ====");
}
}
==== outerMethod start ====
==== outerMethod transaction Active : false
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====
3. 정리
- 프록시의 동작원리에 따라, 외부메서드 - 내부메서드 관계에 대해서 @Transactional을 외부메서드에 붙여야 트랜잭션 적용이 올바르게 수행된다.
- 내부메서드에만 트랜잭션을 적용해야 하는 상황이라면, 별도의 클래스로 내부메서드를 빼내 해당 클래스의 프록시가 올바르게 트랜잭션 관련 코드를 적용할 수 있도록 처리한다.
'Spring' 카테고리의 다른 글
Spring JdbcTemplate에서는 SQLException를 어떻게 처리했을까? (0) | 2023.06.23 |
---|---|
Jsoup 라이브러리를 통한 정적 페이지 크롤링 (0) | 2023.03.18 |
Proxy로 생성되는 Service와 의존관계를 갖는 @Repository, JpaRepository는 Proxy일까? (2) | 2022.09.09 |
JWT를 사용한 로그인 및 Refresh Token을 활용한 로그인 상태 유지 (9) | 2022.07.12 |
username만 매칭되면 user 세션 생성 되는 문제 해결 - AuthenticationProvider를 통한 password 기반 인증 (0) | 2022.05.30 |