티스토리 뷰

반응형

회사에서 캐시 구조에 대해서 리팩토링을 진행하는 과정에서 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

 

Self Invocation은 왜 발생할까?

Moon

gmoon92.github.io

 

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%의 성능 향상을 확인할 수 있었다.

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함