@Transactional
에는 Spring AOP
의 Proxy
방식이 사용된다. 그렇기 때문에 우선 Proxy
에 대해서 알아보자.
프록시 객체는 원래 객체를 감싸고 있는 객체로, 원래 객체와 타입은 동일하다. 프록시 객체가 원래 객체를 감싸서 client의 요청을 처리하게 하는 패턴이다.
Spring
에는 크게 두 가지 프록시 구현체를 사용한다. JDK PROXY(=Dynamic PROXY)
와 CGLib
이다. Spring AOP
는 PROXY의 매커니즘을 기반으로 AOP PROXY를 제공하고 있다.
위 그림처럼 Spring AOP
는 사용자의 특정 호출 시점에 IoC 컨테이너에 의해 AOP를 할 수 있는 Proxy Bean
을 생성해준다. 동적으로 생성된 Proxy Bean
은 타깃의 메서드가 호출되는 시점에 부가기능을 추가할 메서드를 자체적으로 판단해 가로채어 부가기능을 주입한다. 이를 호출시점에 동적으로 위빙을 한다해 런타임 위빙(Runtime Weaving)
이라고 한다.
Spring AOP
는 런타임 위빙 방식을 기반으로 하고 있고, Spring
에서는 상황에 따라 JDK Proxy
와 CGLib
방식을 통해 Proxy Bean
을 생성해준다.
이 두 가지AOP Proxy
를 생성하는 기준은 자체 검증 로직을 통해 타깃(Target)의 인터페이스 유무를 판단하는 것이다.
JDK Proxy
는 Spring AOP
의 근간이 된다.JDK Proxy
는 JAVA의 reflection
의 Proxy
클래스가 동적으로 Proxy
를 생성해준다. 이 클래스를 사용하기 위한 몇 가지 조건이 있지만 핵심은 타겟의 인터페이스를 기준으로 Proxy
를 생성해준다는 점이다.
JDK Proxy
가 Proxy 객체를 생성하는 방식은 다음과 같다.
ProxyFactory
에 의해 타겟의 인터페이스를 상속한 Proxy
객체를 생성한다.Proxy
객체에 InvocationHandler
를 포함시켜서 하나의 객체로 반환한다.다음과 같이 Proxy
를 생성하는 과정에서 핵심적인 부분은 무엇보다 인터페이스를 기준으로 Proxy
객체를 생성한다는 점이다. 따라서 구현체는 인터페이스를 상속해야 하고, @Autowired
를 통해, 생성된 Proxy Bean
을 사용하기 위해서는 반드시 인터페이스의 타입으로 지정해줘야 한다.
이런 Proxy의 구조를 이해하지 못한다면 다음과 같은 상황이 벌어질 수 있다.
@Controller
public class UserController{
@Autowired
private MemberService memberService; // <- Runtime Error 발생...(Interface가 아닌 Class 타입으로 DI를 헀다)
...
}
@Service
public class MemberService implements UserService{
@Override
public Map<String, Object> findUserId(Map<String, Object> params){
...isLogic
return params;
}
}
MemberService
클래스는 인터페이스를 상속받고 있기 때문에 Spring
은 JDK Proxy
방식으로 Proxy Bean
을 생성한다. 그렇기 때문에 위 코드를 실행하면 RuntimeException
이 발생한다.
@Autowired MemberService memberService
가 인터페이스 타입이 아니기 때문이다. 즉, UserService userService
로 형식을 변경해줘야 한다.
Proxy
패턴은 접근제어의 목적으로 Proxy
를 구성한다는 점도 중요하지만, 무엇보다 사용자의 요청이 기존의 타겟을 그대로 바라볼 수 있도록 타겟에 대한 위임코드를 Proxy
객체에 작성해줘야 한다. 이 위임코드를 InvocationHandler
에 작성해준다.
사용자의 요청이 최종적으로 생성된 Proxy
의 메서드를 통해서 호출될 때 내부적으로 invoke
에 대한 검증과정이 이뤄진다. 다음 코드를 참조하자.
public Object invoke(Object proxy, Method proxyMethod, Object[] args) throws Throwable {
Method targetMethod = null;
// 주입된 타깃 객체에 대한 검증 코드
if (!cachedMethodMap.containsKey(proxyMethod)) {
targetMethod = target.getClass().getMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
cachedMethodMap.put(proxyMethod, targetMethod);
} else {
targetMethod = cachedMethodMap.get(proxyMethod);
}
// 타깃의 메소드 실행
Ojbect retVal = targetMethod.invoke(target, args);
return retVal;
}
이러한 검증과정이 필요한 까닭은 Proxy
가 인터페이스에 대해서만 Proxy
를 생성하기 때문이다. 따라서 타겟에 대한 정보가 잘못 주입된 경우를 대비해 JDK Proxy
는 내부적으로 주입된 타겟에 대해서 검증코드를 형성한다.
CGLib
는 클래스의 바이트 코드를 조작해 Proxy
객체를 생성해주는 라이브러리이다.
Spring
은 CGLib
를 사용해 인터페이스가 아닌 클래스에 대해서도 Proxy
를 생성해준다. CGLib
는 Enhancer
라는 클래스를 통해 Proxy
를 생성할 수 있다.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemberService.class); // 타깃 클래스
enhancer.setCallback(MethodInterceptor); // Handler
Object proxy = enhancer.create(); // Proxy 생성
이 과정에서 CGLib
는 타겟 클래스에 포함된 모든 메서드를 재정의해서 Proxy
를 생성해준다. 이 때문에 CGLib
는 final
메서드 또는 클래스에 대해서 재정의를 할 수 없기 때문에 Proxy
를 생성할 수 없다는 단점이 있지만, CGLib
는 바이트 코드를 조작해서 Proxy
를 생성하기 때문에 성능적으로 JDK Proxy
보다 좋다.
성능 차이의 근본적인 이유는 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의 Proxy 객체 생성 방법 정리
1. 인터페이스를 구현하고 있는지 확인
2. 인터페이스 구현 -> JDK Proxy
3. 인터페이스 미구현 -> CGLib
@Transactional
은 AOP를 사용하여 구현된다. transaction 의 begin
과 commit
을 메인 로직 앞 뒤로 수행해주는 기능을 담당한다.
@Transactional
가 붙은 메서드가 호출되기 전 begin
을 호출하고, 메서드가 종료되고 commit
을 호출한다. 이 때 Spring AOP
는 기본적으로 PROXY 패턴
을 사용한다.
Spring boot
는 프록시 객체를 생성할 때 기본적으로 CGLib
를 사용한다. 그 이유는 JDK Proxy
는 내부적으로 Reflection
을 사용하기 때문이다. Reflection
은 비용이 비싼 효율성이 떨어지는 API이고, JDK Proxy
는 타겟으로 인터페이스만을 허용하기 때문이다.
의무적으로 서비스 계층에서 인터페이스를 xxxxImpl
클래스로 작성하던 관례도 다 JDK Proxy
의 특성 때문이기도 하다.
JDK Proxy
의 InvocationHandler
의 invoke
메서드를 오버라이딩 해서 Proxy
위임 기능을 수행하는데, 이 때 메서드에 대한 명세를 가져올 때 Refelction
이 사용된다.
@Transactional
은 Proxy 형태로 동작한다.
@Transactional // -> 오류 발생
private void createUser(){
// createUser 로직
}
@Transactional
은 Proxy 형태로 동작하기 때문에 외부에서 접근이 가능한 메서드만 설정할 수 있다.
@Transactional
public void createUserListWithTrans(){
for (int i = 0; i < 10; i++) {
createUser(i);
}
}
@Transactional
public User createUser(int index){
User user = User.builder()
.name("testname::"+index)
.email("testemail::"+index)
.build();
userRepository.save(user);
return user;
}
위 코드는 실행하면 10번의 createUser
가 실행되지만 User는 생성되지 않는다. 그 이유는 @Transactional
이 Proxy 형태로 동작하기 때문이다. JPA가 AOP를 사용해서 생성한 Proxy 객체는 다음과 같은 코드의 형태를 가질 것이다.
public void createUserListWithTrans(){
EntityTransaction tx = em.getTransaction();
tx.begin();
super.createUserListWithTrans();
tx.commit();
}
public User createUser(int index){
EntityTransaction tx = em.getTransaction();
tx.begin();
User user = super.createUser(index);
tx.commit();
return user;
}
Proxy 객체에서 UserService
의 createUserListWithTrans
를 호출하고, createUserListWithTrans
는 그 안에서 같은 클래스의 createUser
를 호출하기 때문에 createUserListWithTrans
의 Transactio
만 동작하게 된다.
Proxy 형태로 동작하게 되면 위 과정대로 동작하기 때문에 최초 진입점인 createUserListWithTrans
의 Transaction
만 동작하게 되는 것이다.
// UserService.java
// No Transaction
public void createUserListWithoutTrans(){
for (int i = 0; i < 10; i++) {
createUser(i);
}
throw new RuntimeException();
}
@Transactional
public User createUser(int index){
User user = User.builder()
.name("testname::"+index)
.email("testemail::"+index)
.build();
userRepository.save(user);
return user;
}
// AopApplication.java
userService.createUserListWithoutTrans();
위 코드는 아래와 같이 동작할 것이다.
// UserService.java
public void createUserListWithoutTrans(){
for (int i = 0; i < 10; i++) {
this.createUser(i);
}
throw new RuntimeException();
}
public User createUser(int index){
User user = User.builder()
.name("testname::"+index)
.email("testemail::"+index)
.build();
userRepository.save(user);
return user;
}
// UserService359caca0.java (proxy객체)
public void createUserListWithoutTrans(){
super.createUserListWithTrans();
// 진입 시점에 @Transactional이 없기 때문에 트랜잭션없이 동작
}
실행 시 10개의 user가 생성된다. 오히려 @Transactional
이 없기 때문에 createUser가 각각 insert하면서 DB의 설정대로 auto commit 까지 동작한 결과다.
@Transactional 사용시 주의사항
1. private method 에 사용할 수 없음
2. 서로 다른 @Transactional method 는 서로를 호출해서 사용할 수 없음
출처
안녕하세요, 정리해주신 글 너무 잘 읽었습니다. 많은 도움이 되었어요!
다름이 아니라 중간에 Spring AOP 의 프록시 객체 생성에 사용되는 기본 기술이 CGLib 라고 기재되어 있는데, 공식 문서를 참고해보니 Dynamic Proxy 를 사용한다고 나와 있어서요.
한번 참고 부탁드립니다!
https://docs.spring.io/spring-framework/reference/core/aop/introduction-proxies.html