Spring에서 AOP를 구현하는 방법과 Transactional
AOP에 관한 간략한 개념이 필요하다면 다른 글을 참조한다.
이 내용은 공식 문서를 참조하여 작성하였다.
AOP 구현 방식
Spring에서 AOP는 두 가지 방법으로 구현되어 있다.
- Dynamic Proxy (Reflection)
- CGLIB (Byte Code Instrument)
인터페이스를 클래스로 구현하여 사용하는 경우 Dynamic proxy, 단일 클래스는 CGLIB를 사용한다.
1. 인터페이스에 선언되지 않은 메소드를 권고하는 경우
2. 메소드의 인자로 프록시된 객체를 구체적으로 전달해야 하는 경우
또한 비즈니스 로직을 구현할 때 클래스보다는 인터페이스로 유연하게 설계할 것을 권장하고 있다.
즉, 인터페이스와 구현체로 이루어진 구성에서는 어지간하면 Dynamic proxy가 사용되는 것이다.
Dynamic Proxy
Dynamic Proxy 활용 시 사용하는 JdkDynamicAopProxy
클래스의 주석엔 이 방식의 특징이 적혀있다.
java.lang.reflect.Proxy
를 기반으로 하는 AOP- 인터페이스가 구현된 클래스에만 사용할 수 있다.
- 대상 클래스가 thread-safe한 경우 생성된 프록시도 thread-safe하다.
- Advice/Pointcut 및 TargetSource가 직렬화 가능하다면 프록시도 직렬화가 가능하다.
CGLIB
CGLIB는 byte code를 조작하여 코드를 생성하는 기법이다.
스프링에서 CGLIB 기반 프록시를 수행하는 클래스는 CglibAopProxy
이다.
- CGLIB도 Dynamic Proxy와 마찬가지로 원본 클래스의 thread-safe 특징을 따른다.
- 부모 인터페이스가 없는 클래스에도 적용할 수 있다.
- 상속을 활용한다. (상속이 불가능한 final/private 키워드는 aspect가 적용되지 않는다)
Transactional
AOP 기능 중 가장 많이 활용되는 @Transactional
은 어떻게 동작할까?
Spring boot에 약간의 코드를 작성하고 디버깅으로 추적해보았다.
SpringAopTest
public interface SpringAopTest { void test1(); }
SpringAopTestImpl
@Component public class SpringAopTestImpl implements SpringAopTest{ @Transactional @Override public void test1() { System.out.println("TEST1 - Begin"); Assert.isTrue(true == false, "test1"); System.out.println("TEST1 - End"); } }
AppRunner
@Component public class AppRunner implements ApplicationRunner { private SpringAopTest springAopTest; public AppRunner(SpringAopTest springAopTest) { this.springAopTest = springAopTest; } @Override public void run(ApplicationArguments args) throws Exception { springAopTest.test1(); } }
Assert 조건에 의해 무조건 실패하게 되는 코드를 작성했다.
AppRunner#run
, SpringAopTestImpl#test1
의 첫 줄에 중단점을 걸어두고 디버깅을 진행해보자.
바로 CglibAopProxy
로 넘어가는 것으로 보아 @Transactional
은 CGLIB를 활용함을 알 수 있다.
공식 문서엔 인터페이스를 구현한 경우 Dynamic Proxy를 활용한다고 했는데 실제로는 CGLIB를 사용하고 있다.
그렇다면 대체 어디서 CGLIB를 기본으로 설정했는지 로그로 확인해보자.
로그 레벨을 TRACE로 바꾸고 코드를 실행한다.
- application.yml
logging:
level:
org.springframework: TRACE
실행하면 무수히 많은 로그가 쏟아지는데, cglib로 검색하다보면 눈에 띄는 로그가 있다.
Condition OnPropertyCondition on org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration$CglibAutoProxyConfiguration matched due to @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched
TransactionAutoConfiguration
를 따라가면 내부 클래스로 EnableTransactionManagementConfiguration
가 있다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {
}
}
소스 코드를 보면 AOP의 방식을 결정하는 것으로 보인다.
기본으로 CGLIB를 따르고 있고 (matchIfMissing = true), config 값에 따라 방식을 바꿀 수 있는 것으로 보인다.
설정파일을 조금 바꾸어보자.
- application.yml
spring:
aop:
proxy-target-class: false
실행 후 디버깅하면 JdkDynamicAopProxy
클래스로 넘어가는 것을 확인할 수 있다.
로그를 따라가다 프록시 변경 방법을 발견했지만 스프링 공식 문서을 다시 읽다 보니 6.4.3 항에 해당 내용을 확인할 수 있었다.
또한 검색을 하다 Spring boot를 활용하는 경우는 CGLIB를 기본으로 적용했다는 내용을 발견했다.
CGLIB가 예외를 덜 발생시킨다는 장점이 있어 변경했다고 한다.
추가로 DB Transaction은 일관된 비즈니스 로직을 만들기 위한 기능으로, 실패한 경우 작업한 데이터를 모두 롤백해야 한다.
@Transactional
역시도 롤백처리를 수행한다.
Assert에서 무조건 터지는 조건을 넣어주었기 때문에 이 부분부터 디버깅을 시작하고 진행하다 보면TransactionAspectSupport
클래스로 넘어간다.
TransactionAspectSupport#invokeWithinTransaction
은 트랜잭션 기능 활용 시 사용되는 메소드로, 중간에 try/catch 문이 하나 보인다.
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
명령을 대신 수행하고 예외상황 발생 시 completeTransactionAfterThrowing 메소드에서 에러 처리를 하도록 넘겨준다.
따라가면 롤백처리를 담당하는 아래의 코드가 보인다.
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
이 때문에 @Transactional
을 수행하는 메소드는 반드시 Assert 등으로 잘못된 데이터에 대한 예외를 던지도록 처리해야 한다.
'Java > Spring framework' 카테고리의 다른 글
Spring(boot)에서 초기화 코드를 작성하는 방법 (0) | 2020.05.11 |
---|---|
Spring boot 자동 설정 분석 (0) | 2020.03.15 |
Spring framework 소스코드 읽어보기 - Bean 생성 원리 (3) (0) | 2020.02.23 |
Spring framework 소스 코드 읽어보기 - Bean 생성 원리 (2) (1) | 2020.01.27 |
Spring framework core (12) - Spring AOP (0) | 2020.01.24 |