[내부코드 파헤치기] Transaction과 AOP
Transaction은 스프링에서 AOP로 동작하여 연결과 커밋, 롤백등을 관리한다고 한다. Transaction 내부코드를 보다보니 어떻게 AOP로 동작하는지 확인이 어려웠다. 먼저 간단한 AOP만들기를 시작으로 좀 더 내부를 파헤쳐보려고 한다.
1. AOP, 프록시 이해하기
횡단 관심사를 분리하기 위해서 AOP를 사용한다. AOP를 사용하기 위해서는 프록시 패턴을 사용한다. reflection을 사용하는 DynamicProxy 방식과 byte 코드를 조작하는 CGLIB 방식이 있다.
AOP에서 사용되는 용어
- Target: 어떤 대상에 부가 기능을 부여할 것인지
- Advice: 어떤 부가 기능을 부여할 것인지
- Join Point: 어디에 적용할 것인지
- Point Cut: 실제 Advice가 적용될 시점을 의미. Spring AOP에서는 advice가 적용될 메서드를 선정.
1-1. DynamicProxy 만들기
reflection를 사용하여 동작하며 인터페이스 구현이 필요하다.
// Hello
public interface Hello {
String sayHello(String name);
}
// LowerHello
public class LowerHello implements Hello {
public String sayHello(String name) {
return "lowercase: hello " + name;
}
}
// UpperHelloProxy
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UpperHelloProxy implements InvocationHandler {
private final LowerHello target;
public UpperHelloProxy(LowerHello target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String result = (String) method.invoke(target, args);
return "[DynamicProxy]" + result.toUpperCase();
}
}
Hello라는 인터페이스, target이 될 LowerHello를 작성하고 InvocationHandler를 구현하는 UpperHelloProxy를 만들어주었다.
// Test
@Test
void proxyHello() {
Hello hello = new LowerHello();
System.out.println("_______original________");
System.out.println(hello.sayHello("better"));
System.out.println("__________________________");
Hello proxyHelloTarget = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{Hello.class},
new UpperHelloProxy(new LowerHello())
);
System.out.println("__________proxy_________");
System.out.println(proxyHelloTarget.sayHello("better"));
System.out.println("__________________________");
}
// 결과
_______original________
lowercase: hello better
__________________________
__________proxy_________
[DynamicProxy]LOWERCASE: HELLO BETTER
__________________________
프록시를 생성한 코드에서는 대문자로 출력되는 것을 볼 수 있다. 다이나믹 프록시는 인터페이스를 구현하여야 하며 reflection을 사용하는 것을 볼 수 있다.
인터페이스를 구현해야한다는 단점과 바이트코드를 조작하는 CGLIB 방식보단 객체를 생성하기 때문에 조금 성능적으로 느릴 수 있다는 단점을 가지고 있다.
1-2. CGLIB방식으로 프록시 만들기
바이트코드를 조작하여 proxy 객체를 생성해주는 라이브러리이다. final, private 메서드는 advise 할 수 없다. Enhancer를 바탕으로 프록시가 구현된다고 한다. 상속(Extends)방식을 이용하여 Proxy화 할 메서드를 오버라이딩 하는 방식이다. CGLIB에서는 핸들러가 MethodInterceptor라는 인터페이스로 정의되어 있다고 한다. 따라서 해당 인터페이스를 구현한 프록시를 구체하면 된다.
// UpperHelloCglib
public class UpperHelloCglib implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
String result = (String) methodProxy.invokeSuper(o, objects);
return "[CGLIB]" + result.toUpperCase();
}
}
// Test
@Test
void cglibProxyTest() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(LowerHello.class);
enhancer.setCallback(new UpperHelloCglib());
Hello cglibHello = (Hello) enhancer.create() ;
System.out.println("__________cglib proxy_________");
System.out.println(cglibHello.sayHello("better"));
System.out.println("__________________________");
}
// 결과
__________cglib proxy_________
[CGLIB]LOWERCASE: HELLO BETTER
__________________________
Enhancer 객체는 반드시 SuperClass(부모클래스)를 설정하고 Callback(Handler)을 설정해주어야 한다. CGLIB은 바이트코드를 조작하기 때문에 다이나믹 프록시보다 성능적으로 우세하다고 한다. 하지만 상속을 이용하기 때문에 final 객체 또는 private 접근자로 된 메서드는 구현에 제약적이라는 단점이 있다.
자료들을 보다보면 과거에는 Enhancer 의존성 주입이나 디폴트 생성자, 생성자2번 호출 등의 단점이 있었으나 Spring 3.2 버전부터 이런 단점들을 보완하였다고 한다.
2. Spring에서 AOP
프록시 또한 매번 객체를 생성하는 것이 아니라 하나의 객체를 만들어 빈으로 등록하고 사용할 수 있지 않을까? 빈 후처리기를 사용하여 자동으로 프록시를 등록하여 사용하게 해준다. 이렇게 스프링에서 관리되기 위해서는 빈으로 등록되어 있어야 한다.
위의 클래스 관계처럼 각각의 클래스들은 인터페이스의 구현체 관계를 이루고 있는 것을 내부 코드로 확인할 수 있다. 아래의 DefaultAopProxyFactory를 보면 다이나믹 프록시와 Cglib 방식 중 하나로 프록시를 생성하는 것을 볼 수 있다.
// DefaultAopProxyFactory
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
내부에 breakpoint를 걸고 돌려보았을 때, repository는 else 구문의 JdkDynamicAopProxy를 타는 것을, service는 ObjenesisCglibAopProxy를 타는 것을 알 수 있었다. 아마 인터페이스를 사용하는 repository 빈들은 DynamicProxy를 통해 생성하고 그외는 CGLIB 방식으로 동작하는 듯 싶다. (스프링부트에서는 기본적으로 CGLIB 방식으로 동작한다고 하는데 블로그나 그렇다더라로 들어서 사실 정확한 정보 출처까지는 확인해보지 못했다.)
3. Transaction과 Proxy
// TransactionProxyFactoryBean
public class TransactionProxyFactoryBean extends AbstractSingletonProxyFactoryBean
implements BeanFactoryAware {
private final TransactionInterceptor transactionInterceptor = new TransactionInterceptor();
@Nullable
private Pointcut pointcut;
//...
}
// TransactionInterceptor
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
// ...
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
}
트랜잭션에 대해서는 Proxy로 동작하며 TransactionInterceptor를 사용하고 이것은 MethodInterceptor를 구현한 방식으로 되어 있다.
public class TransactionAutoConfiguration {
// ...
@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 {
}
}
}
TransactionAutoConfiguration에 보면 다이나믹프록시와 CGLIB 프록시 설정으로 등록해주는 조건이 있는 듯 싶다.
정리
Transaction이 AOP로 동작하는 부분에 대해서 내부를 확인하고 싶었다. 스프링 프레임워크가 워낙 복잡해서 내부를 명쾌하게 뚫어보는 느낌은 아니지만, AOP의 동작원리와 Spring 내부코드를 조금 살펴보았다. Transactional 어노테이션이 붙어있는 메서드가 어떻게 proxy로써 동작하는지 명확하게 흐름을 파악하기는 어려웠지만 대략적으로 이런식으로 동작하겠구나를 좀 더 알아볼 수 있는 시간이었다.
참고
- 토비의 스프링 vol.1
- AOP 테코톡
- 스프링 내부코드