들어가며
지난 AOP에 관한 글에 이어 내가 겪은 문제와 이를 해결하는 과정을 담아 보겠다. 그 전에 스프링에서 @Transactional을 처리하는 과정을 먼저 짚고 넘어가보자
@Transactional과 AOP
트랜잭션 처리를 위한 @Transactional 어노테이션은 Spring AOP의 대표적인 예이다. @Transactional 또한 Proxy 형태로 동작한다.
동작과정
- target에 대한 호출이 들어오면 AOP proxy가 이를 가로채서(intercept) 가져온다.
- AOP proxy에서 Transaction Advisor가 commit 또는 rollback 등의 트랜잭션 처리를 한다.
- 트랜잭션 처리 외에 다른 부가 기능이 있을 경우 해당 Custom Advisor에서 그 처리를 한다.
- 각 Advisor에서 부가 기능 처리를 마치면 Target Method를 수행한다.
- interceptor chain을 따라 caller에게 결과를 다시 전달한다.
위의 동작과정을 토대로 이해한다면 우리는 다음과 같은 두 가지의 사실을 알 수 있다.
1. @Transactional이 적용된 메소드는 public만 가능하다.
이유는 기본적으로 CGLIB Proxy는 타겟 클래스의 상속을 받는다. 따라서 private는 상속이 불가능 하기에 불가능 하다.
따라서 protected와 public을 사용하면 컴파일 상으론 에러가 발생하지 않는다.
하지만 protected 또한 정상적으로 동작하지 않는데 이유는 JDK Dynamic proxy 때문이다.
JDK Dynamic proxy는 인터페이스를 상속을 받기 때문에 protected 메소드는 동작할 수 없다.
그래서 스프링에서는 일관된 AOP적용을 위해서 protected로 선언된 메서드 또한 트랜잭션이 걸리지 않도록 한 것이다.
즉, 프록시 설정에 따라 트랜잭션이 적용되었다 안되었다 하는 변칙적인 결과를 막기 위함이다.
2. 같은 클래스내에서 트랜잭션이 걸린 메소드를 호출하면 트랜잭션이 작동하지 않는다.
이번에 내가 프로젝트를 진행하며 겪은 문제이다. 프로젝트에서는 결국 트랜잭션이 걸린 메소드로 트랜잭션이 걸리지 않은 메소드를 호출하여 해결을 했지만, 다음과 같은 방법이 있음을 인지하기 위해 간단한 코드로 해결 방법을 작성해 보았다.
다음의 코드를 보자
public class ex {
public void intit() {
this.move():
}
@Transactional
public void move() {
}
}
Spring AOP에서 프록시의 동작 과정을 보면 프록시를 통해 들어오는 외부 메서드 호출을 인터셉트 하여 작동한다. 바로 이러한 성격때문에 self-invocation이 라고 불리는 현상이 발생하게 되는것이다.
이 말이 이해가 되지 않는다면 프록시의 동작 과정을 다시 이해해야 하는데,
현재 init()메소드에서 호출하는 this는 프록시 객체가 아닌 자기 자신이다. 따라서 자기 자신을 호출할 때에는 프록시 객체가 아닌 자기 자신을 호출하기 때문에 AOP로 구현되어 있는 @Transactional또한 작동하지 않는 것이다.
해결
이를 해결하기 위해서는 AspectJ를 이용해 컴파일 시점에 위빙을 적용하거나, @Resource, @Inject, @Autowired를 통해 자기 자신을 클래스에 주입해줘야 하는데 스프링 2.6부터는 순환참조가 금지되었기에 아래의 설정을 추가해줘야 한다.
spring:
main:
allow-circular-references: true
AspectJ를 이용해 컴파일 시점에 위빙하여 해결 해보자
@Aspect
@Component
public class AopService {
@Pointcut("execution(void com.jojoldu.book.freelecspringbootwebservice.aop.ExampleService*.progress(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before() {
System.err.println("aop works!!");
}
}
@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class ExampleService {
public void init(){
//this.progress();
((ExampleService) AopContext.currentProxy()).progress();
}
public void progress(){
System.out.println("PROGRESS");
}
}
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ExampleServiceTest {
@Autowired
ExampleService exampleService;
@Test
public void AOP_TEST() {
exampleService.init();
}
}
테스트 코드 결과
다음의 결과를 통해 AOP가 작동하 것을 알 수 있다.
마치며
같은 클래스에서 일반 메소드 내에 @Transactional이 붙은 메소드를 호출하면 트랜잭션이 작동하지 않는 이유에 대해서 알아봤다. 이는 AOP와 밀접한 관련이 있으며 앞으로 코드를 짤 때 이를 인지하게 될 것이고 운 좋은 경험을 통해 새로운 지식을 얻었다는 것에 설렘을 느끼게 되었다. 앞으로의 공부 또한 기대를 하며 글을 마무리 한다.
'프로젝트' 카테고리의 다른 글
mysql 쿼리 실행 계획 #2 (0) | 2023.01.28 |
---|---|
mysql 쿼리 실행 계획 #1 (2) | 2023.01.28 |
Equals & HashCode (0) | 2022.11.17 |