프록시를 통한 작동
Cacheable 어노테이션 또한 CachingConfigurationSelector 클래스를 통해 Configuration이 등록된다. 이 때 @EnableCaching에서 설정한 AdviceMode에 따라 Proxy, AspectJ 중 하나로 동작하기 위해 각각의 Configuration 목록이 제공된다.
내부 메서드 호출 해결 방법
기본적으로 Spring AOP에서 사용하는 Proxy 기반으로 작동하지만 특수한 상황에서는 AspectJ를 사용하거나 다른 방식으로 사용할 필요가 있다.
예를 들어, Spring AOP는 프록시 클래스를 기반으로 작동하는데 같은 클래스의 메서드를 내부적으로 호출하는 경우 프록시 클래스를 거치지 않기 때문에 AOP 로직이 적용되지 않는다. 이때 AspectJ를 사용하면 Weaving 방식으로 클래스 파일 자체에 AOP 로직을 적용하므로 이런 문제가 발생하지 않는다.
weaving은 컴파일시점, 컴파일 후, 로드시 세 가지 시점에서 호출이 가능하다. 내부 메서드 호출 문제 해결을 위해서는 컴파일 후 시점을 제외한 두 방법이 사용된다.
만약 컴파일 시점에 위빙이 되도록 gradle 또는 maven에서 의존성을 부여받고 AdviceMode를 AspectJ로 설정하면 된다.
혹은 load-time시에 weaving이 되도록 하려면 AspectJ agent 실행 옵션으로 등록하고 @nableLoadTimeWeaving 어노테이션을 붙인다.
그 외에 다른 방법도 존재한다. 직접 Application에서 getBean으로 객체를 찾아 프록시 객체라면 사용하는 방법, @Source라는 어노테이션으로 self autowiring이 가능하다. 이렇게 사용하면 중복적으로 호출되는 문제가 있다면 한 번만 호출되도록 수정도 가능하다.
프록시 방식 작동 방식
ProxyCachingConfiguration 클래스 내부에서는 아래와 같이 세가지 빈을 등록한다.
BeanFactoryCacheOperationSourceAdvisor
SpringCache에서 AOP로 동작하기 위한 포인트 컷과 어드바이스를 등록하는 어드바이저
여러 개의 어드바이스가 특정 조인 포인트에 적용될 경우 어떤 순서로 적용해야 할지 지정하는 order속성을 advisor등록 시에 사용한다.
@Role(2)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource);
advisor.setAdvice(cacheInterceptor);
if (this.enableCaching != null) {
advisor.setOrder((Integer)this.enableCaching.getNumber("order"));
}
return advisor;
}
AnnotationCacheOperationSource
포인트컷으로 등록되는 CacheOperationSourcePointcut 클래스에서 Spring Cache 어노테이션이 붙어있는지 검사하기 위해 내부적으로 가지는 클래스로 AOP 적용 대상인지 확인한다.
@Bean
@Role(2)
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource(false);
}
CacheInterceptor
포인트컷에 적용될 invoke 메서드를 정의한다.
실직적인 로직이 작성된 CacheAspectSupport를 상속받아 구체적인 로직은 상위 클래스인 CacheAspectSupport 클래스에 넘겨 처리한다.
@Bean
@Role(2)
public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
interceptor.setCacheOperationSource(cacheOperationSource);
return interceptor;
}
어노테이션 동작 흐름
1. Cache 어노테이션이 달린 메서드가 호출
2. Spring AOP에 의해서 프록시 클래스에서 CacheInterceptor#invoke를 호출하게 된다.
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
public CacheInterceptor() {
}
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
} catch (Throwable var2) {
Throwable ex = var2;
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
return this.execute(aopAllianceInvoker, target, method, invocation.getArguments());
} catch (CacheOperationInvoker.ThrowableWrapper var6) {
CacheOperationInvoker.ThrowableWrapper th = var6;
throw th.getOriginal();
}
}
}
3. cacheInterceptor는 캐싱 로직을 담당하는 CacheAspectSupport#excute를 호출하여 아래 순서로 어노테이션을 처리한다.
1. cacheEvits(beforeInvocation = true)
• @CacheEvict(beforeInvocation = true)가 설정된 경우, 메서드 실행 전에 캐시를 삭제한다.
@CacheEvict(value = "products", key = "#id", beforeInvocation = true)
public void deleteProduct(Long id) {
throw new RuntimeException("삭제 중 예외 발생");
}
2. Cacheable
@Cacheable이 설정된 경우, 캐시에서 값을 조회.
캐시에 값이 있으면 원본 메서드를 실행하지 않고 캐시된 값을 반환.
캐시에 값이 없으면 원본 메서드를 실행하고 이후 저장.
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return findProductFromDB(id); // DB 조회
}
3. 원본 메서드 invoke (#evaluate)
캐시에 값이 없는 경우, 원본 메서드가 실행됩니다.
예를 들어, @Cacheable이 적용된 메서드라면, 캐시에서 값을 찾지 못할 때 실행.
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return findProductFromDB(id); // DB 조회 후 결과 반환
}
4. Cacheable(save) / cachePut
@Cacheable이면, 메서드 실행 결과를 캐시에 저장.
@CachePut도 마찬가지로 캐시를 업데이트하지만, @Cacheable과 달리 항상 메서드를 실행하고 캐시를 갱신.
5. CacheEvict(beforeInvocation = false)
기본적으로 @CacheEvict는 메서드 실행 후 캐시를 삭제.
• 만약 beforeInvocation = true라면 1️⃣번에서 먼저 실행되지만, 기본적으로는 메서드 실행 후 수행.
@Nullable
private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
if (contexts.isSynchronized()) {
return this.executeSynchronized(invoker, method, contexts);
} else {
this.processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);
Object cacheHit = this.findCachedValue(invoker, method, contexts);
return cacheHit != null && !(cacheHit instanceof Cache.ValueWrapper) ? cacheHit : this.evaluate(cacheHit, invoker, method, contexts);
}
}
@Nullable
private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
if (contexts.processed) {
return cacheHit;
} else {
Object cacheValue;
Object returnValue;
if (cacheHit != null && !this.hasCachePut(contexts)) {
cacheValue = this.unwrapCacheValue(cacheHit);
returnValue = this.wrapCacheValue(method, cacheValue);
} else {
returnValue = this.invokeOperation(invoker);
cacheValue = this.unwrapReturnValue(returnValue);
}
List<CachePutRequest> cachePutRequests = new ArrayList(1);
if (cacheHit == null) {
this.collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
}
this.collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
Iterator var8 = cachePutRequests.iterator();
while(var8.hasNext()) {
CachePutRequest cachePutRequest = (CachePutRequest)var8.next();
Object returnOverride = cachePutRequest.apply(cacheValue);
if (returnOverride != null) {
returnValue = returnOverride;
}
}
Object returnOverride = this.processCacheEvicts(contexts.get(CacheEvictOperation.class), false, returnValue);
if (returnOverride != null) {
returnValue = returnOverride;
}
contexts.processed = true;
return returnValue;
}
}
실제 캐싱 처리
Cache구현체의 메서드는 AbstractCacheInvoker의 doGet, doRetrieve, doPut, doClear 같은 메서드들에서 호출된다.
@Nullable
protected <T> T doGet(Cache cache, Object key, Callable<T> valueLoader) {
try {
return cache.get(key, valueLoader);
} catch (Cache.ValueRetrievalException var7) {
Cache.ValueRetrievalException ex = var7;
throw ex;
} catch (RuntimeException var8) {
RuntimeException ex = var8;
this.getErrorHandler().handleCacheGetError(ex, cache, key);
try {
return valueLoader.call();
} catch (Exception var6) {
Exception ex2 = var6;
throw new RuntimeException(ex2);
}
}
}
@Nullable
protected CompletableFuture<?> doRetrieve(Cache cache, Object key) {
try {
return cache.retrieve(key);
} catch (Cache.ValueRetrievalException var4) {
Cache.ValueRetrievalException ex = var4;
throw ex;
} catch (RuntimeException var5) {
RuntimeException ex = var5;
this.getErrorHandler().handleCacheGetError(ex, cache, key);
return null;
}
}
protected <T> CompletableFuture<T> doRetrieve(Cache cache, Object key, Supplier<CompletableFuture<T>> valueLoader) {
try {
return cache.retrieve(key, valueLoader);
} catch (Cache.ValueRetrievalException var5) {
Cache.ValueRetrievalException ex = var5;
throw ex;
} catch (RuntimeException var6) {
RuntimeException ex = var6;
this.getErrorHandler().handleCacheGetError(ex, cache, key);
return (CompletableFuture)valueLoader.get();
}
}
protected void doPut(Cache cache, Object key, @Nullable Object value) {
try {
cache.put(key, value);
} catch (RuntimeException var5) {
RuntimeException ex = var5;
this.getErrorHandler().handleCachePutError(ex, cache, key, value);
}
}
protected void doEvict(Cache cache, Object key, boolean immediate) {
try {
if (immediate) {
cache.evictIfPresent(key);
} else {
cache.evict(key);
}
} catch (RuntimeException var5) {
RuntimeException ex = var5;
this.getErrorHandler().handleCacheEvictError(ex, cache, key);
}
}
protected void doClear(Cache cache, boolean immediate) {
try {
if (immediate) {
cache.invalidate();
} else {
cache.clear();
}
} catch (RuntimeException var4) {
RuntimeException ex = var4;
this.getErrorHandler().handleCacheClearError(ex, cache);
}
'Redis' 카테고리의 다른 글
[Redis/ Caching ] Spring에서 Cache를 구현하는 방식 (1) | 2025.03.04 |
---|---|
[TIL] Redis 종류 및 전략 및 문서 정리 (0) | 2025.02.27 |