@Transactional AOP 로직과 분산락 AOP 을 중첩해서 사용하는 경우에 @Transactional 을 나중에 실행시키고 싶었다. 이를 적용하기 위해 검토했던 코드 기록이다. 해당 내용은 kurly techblog 에서 분산락 내용을 보고 적용하는 과정에서 든 고민이다.
참고자료 : 풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson
[1] 분산락은 락 점유/해제 블록 안에서 Transaction 를 묶어야 한다.
분산락을 사용해야 하는 이유는 무엇일까? 그 이유는 분산 환경의 동시성 제어를 위함이다. 분산락은 임계 영역에 다른 요청이 접근을 막아 여러 컴포넌트의 데이터 정합성을 보장하는 방식이다.
각 저장소는 독립적인 로컬 트랜잭션을 지원하지만 이종 데이터베이스(hetero database) 의 트랜잭션을 지원하지 않는다. 이를 분산락을 사용해 단일 요청만 접근을 허용해 동시성 제어를 보장한다.
만약 분산락과 함께 트랜잭션을 선언해 데이터 일관성을 유지하고 싶다면 분산락은 락 점유/해제 블록 안에 트랜잭션을 실행시켜야 한다. 만약 lock block 내부에 트랜잭션을 선언하지 않을 경우 후행 트랜잭션이 임계 영역에 접근이 가능하기 때문에 데이터 읽기 작업에서 일관성이 꺠지는 경우가 발생할 수 있어 동시성 제어를 할 수 없는 경우가 발생할 수 있다.
[2] custom Aspect 가 @Transactional 보다 실행이 늦은 이유는?
처음에 분산락을 적용하기 위해 @Transactional 와 분산락 AOP 적용을 위해 커스텀 어노테이션 @DistributedLock 을 선언했다.
하지만 예상과는 다르게 분산락 Aspect 을 적용하며 @Transactional 보다 늦게 실행되는 문제가 있었다.
이 이야기는 정합성 보장 및 동시성 제어를 할 수 없다는 말이기 때문에 해당 글에서는 AopForTransaction 이라는 컴포넌트를 사용해 @Transactional 을 분리하고자 했다.
처음에는 해당 방식을 동의하며 기존에 선언한 @Transactional 을 적용하고 AopForTranaction 을 도입해 적용했다.
그런데 만약 분산락에서 트랜잭션을 사용하지 않는다면??
불필요한 DB connection 과 같은 불필요한 리소스를 소모하기 때문에 위와 같은 방식은 적절하지 않는 방식이라는 피드백이 있었다.
그래서 @Transactional 의 순서를 제어하기 위해 공식 코드를 확인해보았다.
[3] @Transactional 의 우선 순위는 가장 낮다. (Ordered.LOWEST_PRECEDENCE)
@Transactional 우선 순위를 확인해보자.
spring docs 을 보면 @Transactional 의 우선 순위는 가장 낮다.
하지만 주의사항이 있다.
만약 다른 로직에서 @Order 어노테이션 선언 또는 Ordered 인터페이스 구현이 되어 있지 않다면
아무리 @Transactional 이 우선 순위가 낮더라도 먼저 실행된다.
spring docs :Using @Transactional
[4] @Transactional 의 우선 순위 코드로 확인해보기
(1) SpringBoot autoconfiguartion 찍먹하기
SpringBoot 3.x 부터는 spring.factories 대신 AutoConfiguration.imports를 사용하여 자동 설정을 더욱 명확하게 관리한다. SpringBoot 3.x 자동 설정은 아래를 참고하자.
- @SpringBootApplication 내부의 @EnableAutoConfiguration이 자동 설정을 활성화함.
- AutoConfiguration.imports에서 자동 설정 클래스 목록을 가져옴.
- AutoConfigurationImportSelector가 자동 설정 클래스를 로드함.
- @ConditionalOnClass와 @ConditionalOnMissingBean 등의 조건을 확인하여 자동 설정을 적용함.
- 필요할 경우 특정 자동 설정을 exclude하여 비활성화 가능.
[💁 추가적으로 AutoConfiguration 실행 순서를 아래를 통해서 확인해보자.]
- spring-boot-autoconfigure-3.4.1.jar!/META-INF/spring-autoconfigure-metadata.properties
# jdbc autoconfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration.AutoConfigureBefore=org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
# hibernate autoconfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.AutoConfigureBefore=org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
# transaction autoconfiguration
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration=
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$AspectJTransactionManagementConfiguration=
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$AspectJTransactionManagementConfiguration.ConditionalOnBean=org.springframework.transaction.aspectj.AbstractTransactionAspect
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration=
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration.ConditionalOnBean=org.springframework.transaction.TransactionManager
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration=
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration.ConditionalOnSingleCandidate=org.springframework.transaction.PlatformTransactionManager
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration.ConditionalOnClass=org.springframework.transaction.PlatformTransactionManager
(2) @EnableTransactionManager 가 없으면 TransactionAutoConfiguration 설정에서 @EnableTransactionManager 를 설정한다.
TransactionAutoConfiguration 을 살펴보자.
아래 spring-autoconfigure-metadata.properties 를 확인하면 아래와 같이 표시되어 있다. 해당 의미는 EnableTransactionManagementConfiguration 를 해당 클래스 빈으로 등록되어야 한다는 의미이며, 거의 대부분 @Configuration 이 추가되어 빈으로 등록된다.
이를 TransactionAutoConfiguration 에서 확인해보면
기본 설정으로 EnableTransactionManagementConfiguration 을 @Configuration 을 통해 등록하고 @EnableTransactionManager 설정을 추가한다.
(3) @EnableTransactionManager 는 TransactionManagementConfigurationSelector 을 통해 설정한다.
@EnableTransactionManager 는TransactionManagementConfigurationSelector 에 관한 설정 파일을 호출해 관련 설정을 활성화 한다.
이 때, TransactionManagementConfigurationSelector 내부 코드를 확인해보면 AdviceMode.PROXY 에 분기 처리되어 ProxyTransactionManagerConfiguration 의 설정을 가져와 사용하게 된다.
- AdviceMode.PROXY 는 JDK-Dynamic-Proxy 또는 CGLIB Proxy 를 활성화 하는 경우에 선언하는 상수이다. 해당 내용에서는 해당 주제를 다루지 않는다.
(4) ProxyTransactionManagementConfiguration 에서 TransactionInteceptor 선언 및 순서 설정
- BeanFactoryTransactionAttributeSourceAdvisor
- Spring AOP 기반 트랜잭션 적용을 위한 핵심 컴포넌트
- AOP에서 트랜잭션을 어드바이스(Advice)로 적용할 대상을 설정하는 역할
- TransactionInterceptor
- 트랜잭션을 시작, 커밋, 롤백하는 실제 작업을 처리하는 컴포넌트
아래 빨간 네모를 확인해보면 advisor 의 순서를 설정하기 위해 @EnableTransactionManager 의 order option 을 가져와 실행 순서를 조회해 주입하고 이를 @Transactional 어노테이션을 처리하는 과정의 실행 순서로 사용된다. 결국 이 부분이 @Transactional 이 선언되어 BeanFactoryTransactionAttributeSourceAdvisor 실행 순서를 제어하는 역할이다.
그래서 결론은 @Transactional 은 @Order 실행 또는 Ordered interface 구현했을 때의 우선 순위는 가장 하위!
[5] @Transactional 보다 먼저 실행하기 = @Aspect 에 @Order 선언해 순서 설정하기
@Transactional 보다 먼저 실행하기 위해서는 @Order 어노테이션 사용하면 간단히 해결된다.
앞서 이야기했듯이 @Order 의 우선 순위는 값이 낮을수록 우선 순위가 높다.
Ordered.LOWEST_PRECEDENCE 를 확인해보면 Integer.MAX_VALUE 로 선언되어 있다.
그러므로 @Transactional 보다 먼저 사용하기 위해서는 Integer.MAX_VALUE 보다 작은 값을 선언하면 해결될 것이다.
[6] 결론
스프링 부트는 자동 설정에 관해 잘 지원해줘 이전의 설정에 한세월이 결렸던 스프링을 사용할 때보다 편하다.
세부 구현에 대해 알지 못해도 추상화가 잘 되어 있어 프로퍼티 값만 변경하면 관련 설정을 자동으로 해준다.
하지만, 자동 설정의 편안함에 취해 사용하는데만 익숙해지면 조금만 다른 케이스가 발생해도 해결하지 못하지 못하는 경우가 많다.
비록 모든 스프링 코드를 공부하고 이해할 수는 없지만 스프링에서 핵심이 되는 코드들은 필요할 떄마다 읽어보는 연습이 필요한 것 같다.
간만에 내부 코드를 보면서 마치 미로 찾듯이 어려움은 있었지만 다음에 다른 스프링 코드를 볼 때는 수월하게 볼 수 있기를 기대해본다. 👨💻
'spring > trouble shooting' 카테고리의 다른 글
Spring6.1 부터 변경된 Parameter Name Retention (0) | 2025.03.11 |
---|---|
QueryDSL transform() 에서 꼭 @Transactional 을 사용해야 하는 이유 (0) | 2024.01.29 |
테스트에서 @Sql 로 테스트 데이터가 들어가지 않았던 이유 (with. custom TestExecutionListener) (1) | 2024.01.24 |