brunch

You can make anything
by writing

C.S.Lewis

by anonymDev Jun 14. 2022

Cacheable과 Transactional을 동시에?

캐시가 먼저일까? 트랜잭션이 먼저일까?

지난 글 @Transactional은 어떻게 만들어졌을까? 에서 @Transactional이 붙은 클래스가 Proxy로 생성되는 과정과 호출 과정을 알아봤다. 그렇다면 @Cacheable이 붙은 메서드를 가진 클래스는 어떻게 생성되고 동작할까?


public class TransactionalService {
   @Cacheable(value = "setSomethingCache")
   public String setSomething(String name) {

       //doSomething
      return result;
   }
}

@Transactional이 붙은 빈 클래스와 동일한 비슷한 방식으로 생성되고 동작한다. @Transactional의 경우 TransactionInterceptor로 감싼 프락시로 생성됐듯이 CacheInterceptor로 감싸진 프락시가 생성된다.


복습할 겸 코드를 간략하게 훑고 넘어가 보자.

CacheInterceptor가 호출되면 부모 클래스인 CacheAspectSupport의 execute를 호출한다.
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor

...
   @Override
   @Nullable
   public Object invoke(final MethodInvocation invocation) throws Throwable {
      Method method = invocation.getMethod();

...(중략)
      try {
         return execute(aopAllianceInvoker, target, method, invocation.getArguments());
      }
...
}


캐시 값을 가져와 캐시 된 값이 존재하는 경우 TrasactionalService의 setSomething을 호출하지 않는다.

대신 캐시 된 값을 반환한다.

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {

...

/*캐시 된 값을 가져온다*/
   // Check if we have a cached item matching the conditions
   Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
...
   Object cacheValue;
   Object returnValue;

   if (cacheHit != null && !hasCachePut(contexts)) {

/*캐시 값이 존재하면 returnValue에 캐시 값을 할당한다*/
      // 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);
   }
...
   return returnValue;
}


그렇다면 아래 예제처럼 @Transaction과 @Cacheable 두 어노테이션이 함께 메서드에 붙는다면 어떻게 될까? 트랜잭션이 시작되고 캐시를 조회할까? 아니면 캐시를 조회하고 트랜잭션을 시작할까? 후자의 경우 CacheHit 된다면 트랜잭션은 시작조차 안될 것이다. 왜냐하면 캐시에서 가져온 값을 바로 반환하기 때문이다.

public class TransactionalService {
   @Transactional
   @Cacheable(value = "setSomethingCache")
   public String setSomething(String name) {

       //doSomething
      return result;
   }
}


먼저 결론부터 말하자면 각 어드바이저의 order 설정을 하지 않았다면 무엇이 먼저 호출할지 모른다. 다만 두 개 중 무엇이 먼저 호출될지 설정할 수 있다. @EnableCaching과 @EnableTransactionManage 어노테이션에서 order값을 설정할 수 있다. order 값이 낮을수록 호출 우선순위가 높아진다. 두 어노테이션의 order의 기본 값은 Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE)으로 같으므로 기본 설정인 경우 순서를 보장할 수 없다.


프락시를 생성할 때 빈 클래스에 맞는 어드바이저를 가져와 프락시에 어드바이스를 주입했다. 이때 주입된 어드바이스(CacheInterceptor, TransactionInterceptor)는 정렬된 순서대로(앞에서 뒤로) 호출된다. 이때 CacheInterceptor 앞에 있고 TransactionInterceptor가 뒤에 있다면 캐시 로직이 먼저 실행될 것이다.

어떻게(어째서) 어드바이스가 정렬된 순서에 따라 호출되는지는 다른 글에서 따로 다루도록 하겠다. 여기서는 그냥 그렇구나 하고 넘어가자.


<AbstractAutoProxyCreator.java>

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
...

/*어드바이스를 가져온다. specificInterceptors에 담긴 순서대로 어드바이스가 호출된다*/
   // Create proxy if we have advice.
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   if (specificInterceptors != DO_NOT_PROXY) {
      this.advisedBeans.put(cacheKey, Boolean.TRUE);

/*빈 클래스의 프락시 인스턴스를 생성해서 넘긴다*/
      Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
      this.proxyTypes.put(cacheKey, proxy.getClass());
      return proxy;
   }

...
}


어드바이스를 가져오는 getAdvicesAndAdvisorsForBean을 타고 들어가 보자.

protected Object[] getAdvicesAndAdvisorsForBean(
      Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

   List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
   if (advisors.isEmpty()) {
      return DO_NOT_PROXY;
   }
   return advisors.toArray();
}


beanClass에 매칭 된 어드바이저를 골라내는데 이때 매칭 된 어드바이저가 존재하는 경우 정렬하는 것을 확인할 수 있다. 여기서 어떻게 정렬되는가에 따라 캐시 어드바이스가 앞에 올 수도 있고 트랜잭션 어드바이스가 앞에 올 수도 있게 되는 것이다.

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
   List<Advisor> candidateAdvisors = findCandidateAdvisors();

/*매칭 된 어드바이저를 골라낸다.*/
   List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
   extendAdvisors(eligibleAdvisors);
   if (!eligibleAdvisors.isEmpty()) {

/*매칭 된 어드바이저를 정렬한다*/
      eligibleAdvisors = sortAdvisors(eligibleAdvisors);
   }
   return eligibleAdvisors;
}


어드바이저 정렬은 Comparator의 구현체인 AnnotationAwareOrderComparator가 한다. Ordered의 구현체를 정렬할 때 비교하는 역할을 한다.

protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
   AnnotationAwareOrderComparator.sort(advisors);
   return advisors;
}


BeanFactoryTransactionAttributeSourceAdvisor와 BeanFactoryCacheOperationSourceAdvisor는 트랜잭션 어드바이저와 캐시 어드바이저로 AbstractPointCutAdvisor를 상속하고 있다. 이 부모 클래스가 org.springframework.core.Ordered 인터페이스를  구현하고 있다.

public abstract class AbstractPointcutAdvisor implements PointcutAdvisor, Ordered,

...


캐싱 어드바이저와 트랜잭션 어드바이저의 order 값은 어노테이션 @EnableCaching @EnableTransactionManagement의 order 값으로부터 각각 가져온다. 캐시 어드바이저 빈을 정의할 때 this.enableCaching의 order 값을 참조한다.

@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
      CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {

   BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
   advisor.setCacheOperationSource(cacheOperationSource);
   advisor.setAdvice(cacheInterceptor);

//@EnableCaching 어노테이션의 order 값을 가져와 어드바이저의 order를  세팅한다.
   if (this.enableCaching != null) {
      advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
   }
   return advisor;
}

...


트랜잭션 어드바이저도 역시 빈을 정의할 때 this.enableTx(@EnableTransactionManagement)로 부터 order 값을 참조하여 순서 값을 세팅하고 있다.

@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
      TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
   BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();

   advisor.setTransactionAttributeSource(transactionAttributeSource);
   advisor.setAdvice(transactionInterceptor);

//@EnableTransactionManagement의 order 값을 참조한다.
   if (this.enableTx != null) {
      advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
   }
   return advisor;
}


정리하면

 @Transactional과 @Cacheable 메서드를 가진 빈이 생성될 때 매칭 되는 어드바이스로 감싸진 프락시가 된다. 프락시가 생성될 때 어드바이스가 배열로 넘겨지는데 정렬된 순서에 따라 어드바이스의 호출 순서가 결정된다. 어드바이스 정렬은 어드바이저의 order 값을 오름차순으로 정렬한 순서와 같다. 다만 order값을 별도로 설정해주지 않은 경우 캐시 어드바이저와 트랜잭션 어드바이저가 동일하게 기본값을 갖기 때문에 순서를 보장할 수 없다. 순서를 정하고 싶다면 @EnableTransactionManagement 또는 @EnableCaching 어노테이션의 order에 값을 설정해서 우선순위를 정할 수 있다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari