이 포스팅은 2024.06.26에 작성되었습니다.
@Transactional
에 대해 알아보다 Spring의 AOP Proxy까지 찾아보게 되었다.
Spring AOP는 프록시 기반으로 JDK Dynamic Proxy와 CGLIB을 활용하여 AOP를 제공한다. 이번에 이 두 가지 프록시에 대해서 알아봄으로써, Spring AOP의 프록시를 기반으로 동작하는 @Transactional
에 대해 더 잘 이해해보려고 한다.
JDK Dynamic Proxy
와 CBLIB
을 알아보고, 차이점을 비교해보기프록시 패턴은 프록시 객체가 원래 객체를 감싸서 client 요청을 처리하게 하는 패턴이다. 프록시 객체는 원래 객체를 감싸고 있는 객체이며, 원래 객체와 타입이 동일하다.
접근 권한을 부여하거나, 부가 기능을 추가할 때 사용된다.
Spring에서는 크게 두 가지 프록시를 사용한다.
첫 번째는 JDK Proxy
(Dynamic Proxy)이고, 두 번째는 CGLIB
이다.
Spring AOP는 프록시 기반으로 동작하며, 상황에 따라 두 가지 프록시 방식 중 하나를 사용하여 대상 객체에 대한 Proxy Bean을 생성한다.
Spring AOP는 사용자의 특정 호출 시점에 IoC 컨테이너에 의해 Proxy Bean을 생성한다.
동적으로 생성된 Proxy Bean은 타깃의 메서드가 호출되는 시점에 부가기능을 추가할 메서드를 자체적으로 판단해 가로채어 부가기능을 주입한다. 이를 호출시점에 동적으로 위빙한다고 하여 런타임 위빙이라고 한다.
JDK Proxy는 JDK에 내장되어 있는 자바 기본 제공 기능이다.
JDK Proxy에서 Proxy Bean은 리플랙션 패키지에 존재하는 Proxy
라는 클래스를 통해 생성된 객체를 의미하며, 타깃의 인터페이스를 기준으로 Proxy를 생성한다. (* 리플랙션의 Proxy 클래스가 동적으로 Proxy를 생성해준다고 하여, Dynamic Proxy라고도 불린다)
Spring 공식문서에는 Spring AOP가 기본적으로 AOP Proxy에 JDK Proxy를 사용한다고 적혀있다.
Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.
간단한 사용방법은 다음과 같다
public class ProxyTest {
// 인터페이스 생성
interface Target {
void print();
}
// 프록시를 적용할 구현체 생성
class TargetImpl implements Target {
@Override
public void print() {
System.out.println("Hello!");
}
}
// InvocationHandler 구현체 생성
class TestHandler implements InvocationHandler {
private Object target;
public TestHandler(final Object target) {
this.target = target;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
System.out.println("######## ---> method start ");
Object result = method.invoke(target, args);
System.out.println("######## ---> method end");
return result;
}
}
// Proxy 생성 및 호출
@Test
void test() {
Target proxy = (Target) Proxy.newProxyInstance(
ProxyTest.class.getClassLoader(),
new Class[]{Target.class}, //타깃의 인터페이스
new TestHandler(new TargetImpl()) //InvocationHandler 구현체
);
proxy.print();
}
}
JDK Proxy가 Proxy 객체를 생성하는 방식은 다음과 같다.
JDK Proxy를 사용할 경우, 인터페이스를 기준으로 Proxy 객체가 생성된다. 그러므로 구현체는 인터페이스를 상속해야 하고, 생성된 Proxy Bean을 사용하기 위해서는 반드시 인터페이스 타입으로 지정해줘야 한다.
만약, 아래와 같이 인터페이스 타입이 아닌 클래스타입으로 DI를 시도할 경우, 예외가 발생한다.
@Controller
public class MemberController{
@Autowired
private MemberServiceImpl memberService; // <- Runtime Error 발생...(Interface가 아닌 Class 타입으로 DI를 헀다)
...
}
@Service
public class MemberServiceImpl implements MemberService{
@Override
public Map<String, Object> findUserId(Map<String, Object> params){
...isLogic
return params;
}
}
MemberServiceImpl 클래스는 인터페이스를 상속받고 있기 때문에 Spring은 JDK Proxy방식으로 Proxy Bean을 생성한다. JDK Proxy
는 인터페이스 타입으로 받아야하지만, 위 코드는 인터페이스가 아닌 구현체로 DI를 시도했기 때문에 예외가 발생한다.
CGLIB는 일반적인 오픈소스 클래스 정의 라이브러리이다. Springboot 3.2 버전부터는 의존성을 주입하지 않고 사용이 가능하다. (Spring Core 패키지에 포함됨)
클래스의 바이트 코드를 조작하여 Proxy 객체를 생성하므로, 인터페이스가 아닌 클래스에 대해서도 Proxy를 생성할 수 있다.
CGLIB는 Enhancer
라는 클래스를 통해 Proxy를 생성한다.
간단한 구현방식은 다음과 같다
public class ProxyTest {
// 프록시를 적용할 구현체 생성
class TargetImpl {
public void print() {
System.out.println("Hello!");
}
}
// Proxy 생성 및 호출
@Test
void test() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("######## ---> method start ");
Object result = proxy.invoke(obj, args);
System.out.println("######## ---> method end");
return result;
});
TargetImpl proxy = (TargetImpl) enhancer.create();
proxy.print();
}
}
CGLIB는 타깃 클래스를 상속받는 방식으로 Proxy를 생성한다. 그렇기 때문에 Proxy 대상 클래스는 인터페이스를 상속 받지 않아도 된다.
JDK Proxy보다 CGLIB가 성능이 좋다. 이유는 CGLIB가 타겟에 대한 정보를 제공받기 때문이다.
CGLib는 제공받은 타겟 클래스에 대한 바이트 코드를 조작해 Proxy를 생성하기 때문에, Handler안에서 타겟의 메서드를 호출할 때 다음과 같은 코드가 형성된다.
public Object invoke(Object proxy, Method proxyMethod, Object[] args) throws Throwable {
Method targetMethod = target.getClass().getMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
Ojbect retVal = targetMethod.invoke(target, args);
return retVal;
}
메서드가 처음 호출되었을 때 동적으로 타겟의 클래스의 바이트 코드를 조작하고, 이후 호출시엔 조작된 바이트 코드를 재사용한다.
Spring AOP에서는 기본적으로 JDK Dynamic Proxy를 기반으로 Proxy를 생성한다. 다만 인터페이스가 없는 경우에는 CGLIB proxy를 사용한다.
(CGLIB 프록싱을 강제로 사용할 수 있긴 하다)
Spring Boot에서는 2.0 버전 이후부터 기본적으로 CGLIB proxy를 사용하도록 바뀌었다.
(https://www.springcloud.io/post/2022-01/springboot-aop/#gsc.tab=0)
SpringBoot가 진작 CGLIB을 사용하지 않은 이유는, 기존 CGLIB가 3가지의 한계가 있었기 때문이다.
하지만 어느 시점부터 Spring Boot 에서 CGLIB 방식으로 Proxy를 생성해주고 있었고, SpringBoot github-issues에서 어느 Spring 개발자가 문제를 제기하였다.
이에 대해 Spring Boot의 리더인 Phil은 CGLIB 프록시가 예상치 못한 cast exception을 일으킬 가능성이 적다고 답변하였다.
Spring Core
패키지에 포함시켜 더이상 의존성을 추가하지 않아도 개발할 수 있게 됨CGLIB-based proxy classes no longer require a default constructor. Support is provided via the objenesis library which is repackaged inline and distributed as part of the Spring Framework. With this strategy, no constructor at all is being invoked for proxy instances anymore.
(New Features and Enhancements in Spring Framework 4.0)
결과적으로 기존 CGLIB가 가지고 있던 대부분의 한계들이 개선되어,
Spring 4.3과 Spring Boot 2.0부터는 성능이 더 좋은 CGLIB
로 Proxy를 생성하게 되었다.
이번 기회를 통해 Spring의 proxy전략에 대해 이해할 수 있었다.
이후에는 이 지식을 기반으로 @Transactional 의 동작방식을 이해해보고자 한다!
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
https://yeonyeon.tistory.com/289
https://stackoverflow.com/questions/29650355/why-in-spring-aop-the-object-are-wrapped-into-a-jdk-proxy-that-implements-interf