[Redis] @Cacheable의 작동 원리

2025. 3. 5. 20:14·Redis
728x90
반응형
SMALL

프록시를 통한 작동

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);
        }
728x90
반응형
SMALL

'Redis' 카테고리의 다른 글

[Redis/ Caching ] Spring에서 Cache를 구현하는 방식  (1) 2025.03.04
[TIL] Redis 종류 및 전략 및 문서 정리  (0) 2025.02.27
'Redis' 카테고리의 다른 글
  • [Redis/ Caching ] Spring에서 Cache를 구현하는 방식
  • [TIL] Redis 종류 및 전략 및 문서 정리
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • 면접 준비 (22)
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      java
      리눅스
      spring redis
      jsp기초
      JSP
      레디스
      Til
      springsecurity
      자바
      자바 면접
      Spring
      자바스크립트
      리눅스마스터2급정리
      JavaScript
      redis 채팅
      Springframework
      스프링프레임워크
      자바 반복문
      자바 면접질문
      자바 알고리즘
      jsp request
      Spring Data Redis
      백준
      자바기초
      HTML
      리눅스마스터2급
      JS
      CSS
      프로그래머스
      redis
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [Redis] @Cacheable의 작동 원리
    상단으로

    티스토리툴바