2024. 9. 21. 21:58ㆍBackend 취업준비/Spring
1. 프록시와 내부 호출의 문제
@Transactional이 적용된 메서드(코드 예시의 internal())를 @Transactional이 적용되지 않은 메서드(코드 예시의 external()) 안에서 사용하게 되면, external()메서드를 사용할 때 우리의 기대와 다르게 internal() 에서 트랜잭션이 전혀 적용되지 않는다. 이는 @Transactional 이 작동하는 프록시 객체의 원리에 있다.
코드 예시
@Slf4j
static class CallService {
public void external() {
log.info("callExternal");
printTxInfo();
internal();
}
@Transactional
public void internal() {
log.info("callInternal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
- callService.external() 가 실행된다.
- callService 의 트랜잭션 프록시가 호출된다. external() 메서드에는 @Transactional 이 없다. 프록시 인스턴스는 실제 인스턴스에 할 일을 단순 위임한다.
- 따라서 트랜잭션 적용되지 않고, 실제 callService 객체 인스턴스의 external() 을 호출한다.
- external() 은 내부에서 internal() 메서드를 호출한다.
- 메서드 앞에 별도의 참조가 없으면 this 가 생력되어 있고, 이는 자기 자신의 인스턴스를 가리킨다.
- 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데, 여기서 this 는 자기 자신을 가리키
므로, 실제 대상 객체( target )의 인스턴스를 뜻한다. - 실제 인스턴스에는 트랜잭션을 위한 로직이 없다. 프록시 인스턴스를 통해서 호출 되어야 트랜잭션이 적용됨
이를 실용적으로 해결하려면 내부 호출 방식을 채택하지 않고 클래스를 나누어 외부에서 호출한다.
2. private 메서드에는 트랜잭션 적용 X
- 스프링의 트랜잭션 AOP 기능은 private 메서드에는 트랜잭션이 적용되지 않도록 설정되어 있다.
- public, protected , default 는 외부에서 호출이 가능하다. 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다.
- 따라서 private 메서드에서는 트랜잭션을 적용이 안된다. 클래스 레벨이 아니라 따로 메서드에 @Transactional을 달아 주어도 적용되지 않는다.
과거 스프링 부트 버전(스프링 부트 3.0 이전)에서는 protected , default 의 경우에서도 @Transactional이 적용이 안되었으며, 해당 접근제어자를 가진 메서드 레벨에 @Transactional이 붙어있어도 컴파일 오류가 나지 않아서 많은 주의가 요구 된다. 스프링 부트 3.0 이후 부터는 private 메서드 레벨에 @Transactional 이 붙어있으면 컴파일 오류가 난다. (클래스 레벨에 붙어있을 때는 오류 안남)
3. @PostConstruct와 함께 사용시 의도대로 작동하지 않는다
스프링 빈의 이벤트 라이프사이클은 다음과 같다.
- 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백(@PostConstruct) -> 사용 -> 소멸전 콜백 -> 스프링 종료
빈을 초기화 하면서 트랜잭션을 적용하고 싶다고 다음과 같이 설정하면 트랜잭션이 적용되지 않는다. 이유는 다음과 같다
- @PostConstruct는 해당 빈 자체만 생성되었다고 가정하고 호출된다.
- 이는 해당 빈에 관련된 AOP등을 포함한, 전체 스프링 애플리케이션 컨텍스트가 초기화 된 것을 의미하지는 않는다.
- 트랜잭션을 처리하는 AOP등은 스프링의 후 처리기(post processer)가 완전히 동작을 끝내서, 스프링 애플리케이션 컨텍스트의 초기화가 완료되어야 적용된다.
@SpringBootTest
@Slf4j
public class IntiTxTest {
@Autowired
Hello hello;
@Test
void go() {
log.info("hello class ={}", hello.getClass());
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
}
}
스프링 빈의 라이프사이클과 관련된 내용을 더 자세히 정리하면 다음과 같다
- 스프링 컨테이너 생성
- 스프링 빈 생성
- 의존관계 주입
- 빈 처리기 (BeanPostProcessor) - 전처리, postProcessBeforeInitialization()
- 초기화 콜백 (@PostConstruct)
빈이 완전히 생성되고 의존성 주입이 완료된 후, 초기화 메서드가 호출됩니다. @PostConstruct 애노테이션이 붙은 메서드 호출됨. - 빈 처리기 (BeanPostProcessor) - 후처리, postProcessAfterInitialization()
초기화가 끝난 후 다시 BeanPostProcessor가 동작하여 프록시 객체 생성 등의 후처리가 진행. 이 과정에서 AOP 프록시가 생성. - 빈 등록 완료
모든 후처리 작업이 끝난 후, 빈이 스프링 컨테이너에 등록. - 스프링 애플리케이션 컨텍스트 초기화 완료 및 어플리케이션 실행
- 사용
- 소멸 전 콜백 (@PreDestroy, DisposableBean의 destroy)
애플리케이션이 종료되기 전에 빈의 소멸 콜백 메서드가 호출. @PreDestroy 애노테이션이 붙은 메서드 호출 - 스프링 종료
스프링 컨테이너가 종료되고, 모든 빈이 소멸
번 외로 해당 테스트를 실행할 때 로그에는 hello에 프록시 객체로 찍히게 되는데 그 이유는
해당 메서드가 실행되는 시점이 사용 시점에 해당하기 때문이다
BeanPostProcessor의 두 메서드
AOP 프록시 객체의 생성 방식에 조금 더 자세히 하자면
- @TransactionalAOP 프록시 객체 과 같은 AOP 기능을 사용하면 프록시 객체가 생성. 이 프록시는 일반적으로 CGLIB 방식으로 생성.
- 프록시 객체를 만드는 주체는 AnnotationAwareAspectJAutoProxyCreator인데, 이는 AOP 적용 대상이 되는 빈을 프록시로 감싸기 위해 사용되는 클래스.
- AnnotationAwareAspectJAutoProxyCreator는 BeanPostProcessor 인터페이스를 구현한 클래스입니다. 따라서 스프링의 AOP 기능은 BeanPostProcessor를 활용하여 프록시 객체를 생성하고 관리
스프링 고급편을 안듣고 DB2로 왔더니 좀 헤맸다. 얼른 공부해야할듯
출처
김영한의 스프링 DB 2편 - 데이터 접근 활용 기술
'Backend 취업준비 > Spring' 카테고리의 다른 글
스프링MVC, DispatcherServlet, ArgumentResolver, HttpMessageConverter (0) | 2024.08.15 |
---|---|
AOP를 활용한 api 중복 요청 방지 (따닥 방지) (0) | 2024.07.22 |
@WebMvcTest를 사용한 Controller 단위 테스트 (1) | 2024.07.15 |
@Transactional, 영속성 컨텍스트 (2) | 2024.03.23 |
엔티티와 @RequestBody를 쓰는 DTO에서 기본 생성자, getter/setter의 필요성 (리플랙션) (0) | 2024.03.22 |