본문 바로가기

Programing/Framework

스프링 캐시 인터셉터

어노테이션 기반의 캐시를 종종 애용해주시는 우기님 덕분에 공부를 하게 되었다.

기회를 제공한 우기님께 감사를 드립니다.

 

@Cacheable 어노테이션이 붙어 있는 메서드를 호출하게 되면 어떤 과정이 수행되어 캐시에 데이터가 저장되고 하는지가 궁금했다.

현상에 대한 디버깅을 통해 분석한 것이라 여러 방법 중에 하나일 수 있다.

 

디버깅을 하다보면 CacheInterceptor 클래스가 보이는데 아래와 같은 계층 구조로 되어 있다.

아래와 같은 MethodInterceptor 인터페이스를 구현하고 있기에 존재하는 메서드는 invoke 메서드 하나 뿐이다.

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;

}

중간에  CacheAspectSupport 라는 추상 클래스가 존재하는데 이 클래스에 많은 메서드들이 있다.

캐시의 주요 기능이 값이 있으면 캐싱된 값을 돌려주고, 없으면 원천 소스에서 값을 찾아 캐시에 저장하고 돌려주는 것인데 이 기능이 execute 메서드에 있었다.

package org.springframework.cache.interceptor;

public abstract class CacheAspectSupport extends AbstractCacheInvoker
		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
        
	// ..
	@Nullable
	protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
		// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
		if (this.initialized) {
			Class<?> targetClass = getTargetClass(target);
			CacheOperationSource cacheOperationSource = getCacheOperationSource();
			if (cacheOperationSource != null) {
				Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
				if (!CollectionUtils.isEmpty(operations)) {
					return execute(invoker, method,
							new CacheOperationContexts(operations, method, args, target, targetClass));
				}
			}
		}

		return invoker.invoke();
	}
    

사실 위의 메서드는 실행의 준비 단계이고 실제 수행은 3중 if 에 있는 execute이다.

package org.springframework.cache.interceptor;

public abstract class CacheAspectSupport extends AbstractCacheInvoker
		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
        
	// ..
	@Nullable
	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		// Process any early evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Check if we have a cached item matching the conditions
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// Collect puts from any @Cacheable miss, if no cached item is found
		List<CachePutRequest> cachePutRequests = new LinkedList<>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// Invoke the method if we don't have a cache hit
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}

		// Process any late evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}
    

캐시를 찾는 작업이면 중간에 findCachedItem 메서드를 호출하여 결과(cacheHit)가 null 일 경우에는 collectPutRequests 를 수행하고,

있으면 해당 값을 돌려주게 되는 것이었다.

 

그래서 아래와 같은 CALL stack 이 만들어진다.

execute: 435, CacheAspectSupport (org. springframework.cache.interceptor) <-
execute: 345, CacheAspectSupport (org. springframework.cache.interceptor)
invoke:61, Cachelnterceptor (org.springframework.cache.interceptor)
proceed: 186, ReflectiveMethodInvocation (org.springframework.aop.framework)
intercept:688, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)