본문으로 바로가기

 이유는 Spring AOP!


Spring Cache (EhCache 등 구현) 사용시 동일 클래스(Bean)내 @Cacheable 설정된 적용된 메서드를 자기 호출(Self-invocation)할 경우 Proxy Class에서 이미 캐싱된 결과를 가져오지 못하고 메서드를 다시 실행하게 됩니다.


이유는 @Cacheable 어노테이션을 통한 cache처리는 @Async, @Transactional 등과 마찬가지로 Spring AOP(Aspect-Oriented Programming)를 이용하기 때문입니다.


Spring Cache는 프록시(Proxy)를 통해 동작하고 이를 조금 더 확장한 개념이 Spring AOP인데 이를 통해 로깅, 트랜잭션, 캐시 처리등 핵심로직에 처리 중 반복되는 횡단의 관심사를 모아서 분리하여 처리 할 수 있습니다. 이게 스프링의 대표적인 특징인 관점지향 프로그래밍( AOP Aspect-Oriented Programming)이죠.


@Cacheable 설정된 메서드를 캐싱(Aspect) 처리하기 위해 스프링에서는 인터셉터(Interceptor)를 통해 적당한 시점에 요청을 가로채서 필요한 처리를 실행하게 되는데 Proxy를 통해 들어오는 외부 메서드 호출만 인터셉트하는 AOP 특성상 동일 클래스내(this) 에서 접근하는 경우에는 동작하지 않는 것입니다.


스프링에서는 자기 호출(Self-invocation) 환경에서의 제약사항을 해결하기 위해 AOP 표준인 AspectJ를 제안하지만 ByteCode를 위빙(Weaving)하여 AOP를 구현하기 위해서 관련 의존성 추가와 컴파일을 위한 별도 plugin 등의 설정 추가가 필요하여 이미 빌드환경이 구성된 시점에서 사용하긴 쉽지 않습니다.


이번 포스팅에서는 Spring AOP 환경에서 내부 호출시에도 Proxy Class를 경유 할 수 있는 방법을 알아 보겠습니다.

 Self invocation ( cache x )


 아래와 같이 Bean으로 등록된 Class가 있다고 가정해 봤을때 클래스내 자기 호출( Self invocation )환경에서의 캐싱 여부를 확인해 보겠습니다.

cacheableMethod() - 연산을 위한 메서드 ( Cacheable 어노테이션 추가 - 캐싱 적용 )

selfInvocaionMethod() - 동일 클래스 내부의 cacheableMethod() 메서드의 의 결과를 받아 그대로 반환

@Service
public class CacheService{
		
	@Cacheable(value = "cacheTest", key = "{#root.methodName}")
	public int cacheableMethod() {
		System.out.println("CacheableMethod() >> Make a calculation for result");
		int result = 0;
		for(int i = 0; i < 3; i++) {
			System.out.println("processing...");
			result ++;
		}
		return result;
	}
	
	public int selfInvocaionMethod() {
		System.out.println("SelfInvocaionMethod() >> return this cacheableMethod() ");
		return cacheableMethod();
	}
}

/* Self-Invocaion Cache Test */
System.out.println("==== cacheableMethod() result1 : " + cacheService.cacheableMethod());
System.out.println("==== cacheableMethod() result2 : " + cacheService.cacheableMethod());		
System.out.println("==== selfInvocaionMethod() result : " + cacheService.selfInvocaionMethod());

결과 ▼

CacheableMethod() >> Make a calculation for result
processing...
processing...
processing...
==== cacheableMethod() result1 : 3
==== cacheableMethod() result2 : 3
SelfInvocaionMethod() >> return this cacheableMethod() 
CacheableMethod() >> Make a calculation for result
processing...
processing...
processing...
==== selfInvocaionMethod() result : 3

결과를 보면 첫번째 cacheableMethod 호출 할 때 캐싱된 데이터가 없기 때문에 연산 후 결과를 반환했고 

두번째 cacheableMethod 호출 때는 연산없이 Proxy class에 이미 캐싱된 결과를 받아 온 것을 확인해 볼 수 있습니다.

그러나 앞에서 이미 cacheableMethod 메서드가 캐싱되어 있는 상태임에도 selfInvocaionMethod 호출을 통해 cacheableMethod 를 내부 호출(this) 했을 경우에는 캐싱된 결과값을 받지 못하고 새로 연산을 하여 결과값을 반환 하고 있습니다.


  해결 방법 ( Proxy )


 내부 호출시에도 this가 아닌 Proxy를 참조 할 수 있도록 한다면 이미 Proxy에 캐싱된 데이터를 받아 올 수 있을 것입니다.
이를 위해 ApplicationContext에서 동일한 클래스(CacheService)의 Proxy Bean을 가져와서 사용하도록 처리하여 캐싱된 결과값을 반환 받을 수 있습니다.
@Service
@AllArgsConstructor
public class CacheService{
	private ApplicationContext applicationContext;

	@Cacheable(value = "cacheTest", key = "{#root.methodName}")
	public int cacheableMethod() {
		System.out.println("CacheableMethod() >> Make a calculation for result");
		int result = 0;
		for(int i = 0; i < 3; i++) {
			System.out.println("processing...");
			result ++;
		}
		return result;
	}

	public int proxyInvocaionMethod() {
		System.out.println("proxyInvocaionMethod() >> return proxy CacheableMethod() ");
		return this.getSpringProxy().cacheableMethod();
	}	

	private CacheService.getSpringProxy() {
	    return applicationContext.getBean(CacheService.class);
	}
}
/* Proxy-Invocaion Cache Test */	
System.out.println("==== cacheableMethod() result1 : " + cacheService.cacheableMethod());
System.out.println("==== cacheableMethod() result2 : " + cacheService.cacheableMethod());		
System.out.println("==== proxyInvocaionMethod() result : " + cacheService.proxyInvocaionMethod());
결과 ▼
CacheableMethod() >> Make a calculation for result
processing...
processing...
processing...
==== cacheableMethod() result1 : 3
==== cacheableMethod() result2 : 3
proxyInvocaionMethod() >> return proxy CacheableMethod() 
==== proxyInvocaionMethod() result : 3

결과값을 보시면 proxyInvocaionMethod 에서 동일 클래스내 cacheableMethod를 호출 했음에도 Proxy Bean을 새로 가져와서 실행했기 때문에 연산없이 캐싱된 데이터를 반환 받은 것을 알 수 있습니다.


Spring 4.3부터 @Resource 주석 상단에 self-autowiring을 사용하여 문제를 해결할 수 있다고 합니다.


이외에도 여러 해결방법이 있긴 한데 근본적으로 서비스 설계 시점부터 스프링에서 Cache가 동작하는 원리를 잘 이해하여 캐싱된 서비스가 내부호출이 일어나지 않도록 잘 구성 하는게 가장 좋은 솔루션이 아닐까 싶습니다.


Reference

https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring

https://code-examples.net/ko/q/101de14