[Spring Boot] EhCache Self-Invocation 문제
회사에서 캐시 구조에 대해서 리팩토링을 진행하는 과정에서 EhCache Self-Invocation 문제로 인해 캐시가 정상적으로 동작하지 않았던 사례에 대해서 포스팅하려고 한다.
이 문제가 무엇인지 예제를 통해서 간단하게 살펴보려고 한다.
@Cacheable(value = "testCache")
public String cache() {
log.error("[Info] Create Data!!!");
return "hello, ch4njun";
}
public String test() {
log.error("[Info] test call!");
return cache();
}
위와 같은 구성이 있을 때 test() 의 내부에서 @Cacheable 어노테이션이 붙어있는 cache() 메서드를 호출한다. 그리고 이 test() 메서드와 cache() 메서드가 동일한 클래스에 있다면 이러한 구성을 Self-Invocation 이라고 한다.
이 경우 @Cacheable 어노테이션이 제대로 동작하지 않고 결과적으로 캐시가 되지 않는 문제가 발생한다.
그럼 왜 이런 문제가 발생할까?
바로 @Cacheable 어노테이션이 Spring AOP 를 기반으로 동작하기 때문이다. 즉, Self-Invocation 문제는 @Cacheable 어노테이션 자체의 문제가 아니라 Spring AOP 문제라는 것이다.
그렇기 때문에 Spring AOP 를 기반으로 하는 다른 모든 기능들에서도 이와같은 문제가 발생할 수 있다.
Spring AOP 에서 Self-Invocation 문제가 발생하는 이유에 대해서 살펴보자.
출처 : https://gmoon92.github.io/spring/aop/2019/04/01/spring-aop-mechanism-with-self-invocation.html
Spring AOP 는 인터페이스 구현 여부에 따라서 JDK Dynamic Proxy 와 CGLIB 을 사용해 AOP 를 적용한다. (Spring Boot 에서는 기본적으로 모든 상황에서 CGLIB 을 사용한다)
이렇게 프록시 객체가 생성되면 외부 호출이 발생했을 때 기존 객체 대신 프록시 객체가 해당 호출을 받게된다.
프록시 객체가 해당 메서드 앞뒤에 부가적인 기능(횡단 관심사)를 실행한 후 기존 객체의 메서드를 호출하게 되는 매커니즘으로 동작하게 되는데, 이때 이미 기존객체로 들어와서 거기서 내부 호출이 발생하는 부분에 대해서는 프록시 객체를 거치지 않게된다.
결과적으로 Self-Invocation 으로 호출된 메서드는 AOP 가 정상적으로 적용되지 않는 것이다.
해결방안
1. AopContext
@Cacheable(value = "testCache")
public String cache() {
log.error("[Info] Create Data!!!");
return "hello, ch4njun";
}
public String test() {
log.error("[Info] test call!");
// 호출된 Proxy 객체를 활용
return ((AspectTestService) AopContext.currentProxy()).cache();
}
AopContext.currentProxy() 메서드는 현재 Proxy 객체를 반환해주는데 이를 통해 내부 메서드를 호출한다면 Self-Invocation 문제를 해결할 수 있다.
그러면 this.cache() 가 아니라 ${ ProxyObject }.cache() 가 호출되고 AOP가 정상적으로 적용된다.
주의해야할 점은 AopContext.currentProxy() 메서드를 사용하기 위해선 expose-proxy 옵션을 활성화해줘야 한다는 것이다. Spring Boot 에서는 @EnableAspectJAutoProxy(exposeProxy = true) 를 통해 해당 옵션을 활성화할 수 있다.
2. IoC 컨테이너의 Bean 활용
IoC 컨테이너에 등록된 자기 자신의 Bean 을 활용해 해결하는 방법이다.
// 자기 자신의 Bean 을 주입받는다.
@Resource(name="aspectTestService")
AspectTestService self;
@Cacheable(value = "testCache")
public String cache() {
log.error("[Info] Create Data!!!");
return "hello, ch4njun";
}
public String test() {
log.error("[Info] test call!");
// DI 된 자기자신의 Bean 을 사용한 내부호출
return self.cache();
}
@Resource 이외에도 @Autowired, @Inject 와 같은 방법으로 자기자신의 Bean 을 주입받을 수 있다.
3. AspectJ Weaving
마지막으로 Spring AOP 의 Weaving 방식을 AspectJ Weaving 방식으로 변경하는 것이다. 이 방법이 Spring 에서 권장하는 해결방법이다.
AspectJ Weaving 방식은 Spring AOP 와 달리 바이트 코드를 직접 조작하는 방식이기 때문에 Proxy 객체로 인한 Self-Invocation 문제가 발생하지 않는다.
이러한 문제를 해결하기 위해 나는 애초에 내부호출이 발생하지 않는 구조로 리팩토링 하는 방법을 선택했다. 때마침 공통코드가 존재했고 해당 공통코드를 Provider 로 분리하면서 자연스럽게 내부호출이 발생하지 않도록 수정한 것이다.
그 후 nGrinder 라는 오픈소스 성능테스트 도구를 통해 테스트한 결과 약 20~30%의 성능 향상을 확인할 수 있었다.