@Transactional 과 PROXY
들어가며
@Transactional
에는 Spring AOP
의 Proxy
방식이 사용된다. 그렇기 때문에 우선 Proxy
에 대해서 알아보자.
프록시 패턴을 사용하는 이유 ?
프록시 객체는 원래 객체를 감싸고 있는 객체로, 원래 객체와 타입은 동일하다. 프록시 객체가 원래 객체를 감싸서 client의 요청을 처리하게 하는 패턴이다.
- 접근 권한을 부여할 수 있다
- 부가 기능을 추가할 수 있다
IoC 컨테이너와 AOP Proxy
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
을 생성해준다.
JDK PROXY와 CGLib를 선택하는 기준
이 두 가지AOP Proxy
를 생성하는 기준은 자체 검증 로직을 통해 타깃(Target)의 인터페이스 유무를 판단하는 것이다.
Spring AOP와 JDK Dynamic Proxy
JDK Proxy
는 Spring AOP
의 근간이 된다.JDK Proxy
는 JAVA의 reflection
의 Proxy
클래스가 동적으로 Proxy
를 생성해준다. 이 클래스를 사용하기 위한 몇 가지 조건이 있지만 핵심은 타겟의 인터페이스를 기준으로 Proxy
를 생성해준다는 점이다.
JDK 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(Code Generator Library)
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 의 동작 원리
@Transactional
은 AOP를 사용하여 구현된다. transaction 의 begin
과 commit
을 메인 로직 앞 뒤로 수행해주는 기능을 담당한다.
@Transactional
가 붙은 메서드가 호출되기 전 begin
을 호출하고, 메서드가 종료되고 commit
을 호출한다. 이 때 Spring AOP
는 기본적으로 PROXY 패턴
을 사용한다.
Spring AOP, CGLib와 JDK Proxy
Spring boot
는 프록시 객체를 생성할 때 기본적으로 CGLib
를 사용한다. 그 이유는 JDK Proxy
는 내부적으로 Reflection
을 사용하기 때문이다. Reflection
은 비용이 비싼 효율성이 떨어지는 API이고, JDK Proxy
는 타겟으로 인터페이스만을 허용하기 때문이다.
의무적으로 서비스 계층에서 인터페이스를 xxxxImpl
클래스로 작성하던 관례도 다 JDK Proxy
의 특성 때문이기도 하다.
JDK Proxy
의 InvocationHandler
의 invoke
메서드를 오버라이딩 해서 Proxy
위임 기능을 수행하는데, 이 때 메서드에 대한 명세를 가져올 때 Refelction
이 사용된다.
@Transactional 사용 주의점
@Transactional
은 Proxy 형태로 동작한다.
1. private은 @Transactional이 적용되지 않는다.
@Transactional // -> 오류 발생
private void createUser(){
// createUser 로직
}
@Transactional
은 Proxy 형태로 동작하기 때문에 외부에서 접근이 가능한 메서드만 설정할 수 있다.
2. 같은 클래스 내 여러 @Transactional method 호출
@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
만 동작하게 되는 것이다.
만약 진입점의 @Transactional이 없다면 ?
// 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 는 서로를 호출해서 사용할 수 없음
출처
- https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
- https://minkukjo.github.io/framework/2021/05/23/Spring/
- https://cobbybb.tistory.com/17
Author And Source
이 문제에 관하여(@Transactional 과 PROXY), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@chullll/Transactional-과-PROXY저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)