본문 바로가기

backend/Spring

@Postconstruct 과 @CachePut

@Postconstruct과 @CachePut을 사용하면서 생겼던 이슈.

문제

@Postconstruct를 이용하여 캐시 데이터를 저장하는 로직을 구현하였는데, (캐시가 put되지 않는)저장되지 않는 이슈가 발생했다.

문제를 해결하기 위해서 알아야 했던 것

1

@CachePut을 사용하여 데이터를 캐시에 저장할 때, CacheAspectSupport를 상속받은 CacheInterceptor 클래스의 invoke()메서드를 통해서 execute()메서드를 실행한다.
중요한게 이때 initialized값을 통해서 aspect를 사용할 수 있는지에 대해서 체크하고 캐시에 저장한다.
initialized값은 기본값이 false이고, false인 경우에는 저장없이 실행만시킨다.

아래 코드는 execute()메서드이고, 주석으로도 해당내용이 남겨져있다.

@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();
    }

initialized값을 true로 초기화해주는 메서드는 CacheAspectSupport안에 afterSingletonsInstantiated()에서 적용된다.
afterSingletonsInstantiated 메서드는 SmartInitializingSingleton인터페이스 메서드를 구현한 내용이다.

아래 코드는 afterSingletonsInstantiated() 메서드 내용이다.

    @Override
    public void afterSingletonsInstantiated() {
        if (getCacheResolver() == null) {
            // Lazily initialize cache resolver via default cache manager...
            Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
            try {
                setCacheManager(this.beanFactory.getBean(CacheManager.class));
            }
            catch (NoUniqueBeanDefinitionException ex) {
                throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
                        "CacheManager found. Mark one as primary or declare a specific CacheManager to use.");
            }
            catch (NoSuchBeanDefinitionException ex) {
                throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
                        "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
            }
        }
        **this.initialized = true;**
    }

SmartInitializingSingleton란?

Callback interface triggered at the end of the singleton pre-instantiation phase during BeanFactory bootstrap. This interface can be implemented by singleton beans in order to perform some initialization after the regular singleton instantiation algorithm, avoiding side effects with accidental early initialization (e.g. from ListableBeanFactory.getBeansOfType(java.lang.Class<T>) calls). In that sense, it is an alternative to InitializingBean which gets triggered right at the end of a bean's local construction phase.

내용상 SmartInitializingSingleton는 BeanFactory에서 싱글톤 인스턴스화를 모두 끝난후 트리거를 발동시키는 인터페이스이다.

SmartInitializingSingleton의 afterSingletonsInstantiated() 메서드는 모든 싱글턴 빈이 이미 생성되었을 경우(생성되었음을 보장)에 실행되었다.

아래는 document에서 afterSingletonsInstantiated()에 대한 설명이다.

afterSingletonsInstantiated()
Invoked right at the end of the singleton pre-instantiation phase, with a guarantee that all regular singleton beans have been created already.

2

단순히 @Postconstruct가 빈이 생성되고 실행된다고 알고 있었다. 더 자세한 설명 내용은 아래와 같다.

The PostConstruct annotation is used on a method that needs to be executed
 after dependency injection is done to perform any initialization. ...

(해석)PostConstruct 주석은 초기화를 수행하기 위해 종속성 삽입이 수행 된 후에 실행해야하는 메소드에 사용됩니다.
즉, 빈의 초기화를 위해 모든 의존성이 injection된 후에 실행된다.

지금까지 내용정리

  1. @CachePut으로 캐시에 데이터를 저장하기 위해서는 모든 빈이 생성된 이후 부터 가능하다.
  2. @PostConstruct는 빈의 초기화를 위해 모든 의존성이 injection된 후에 실행된다.

@PostConstruct를 이용해서 캐시를 업데이트 할 경우, CacheInterceptor를 통한 캐시 저장에 대한 준비된 사항(모든 빈이 생성된 이후)에 대해서 보장할 수 없다.

해결방법

모든빈이 사용가능 할 경우, 캐시업데이트를 실행한다.

스프링이 지원하는 applicationLisener를 이용하여 모든빈이 사용가능 할 경우에 캐시를 업데이트를 실행한다.

1. ApplicationStartingEvent is sent at the start of a run but before any processing, except for the registration of listeners and initializers.

2. ApplicationEnvironmentPreparedEvent is sent when the Environment to be used in the context is known but before the context is created.

3. ApplicationPreparedEvent is sent just before the refresh is started but after bean definitions have been loaded.

4. ApplicationStartedEvent is sent after the context has been refreshed but before any application and command-line runners have been called.

5. ApplicationReadyEvent is sent after any application and command-line runners have been called. It indicates that the application is ready to service requests.

6. ApplicationFailedEvent is sent if there is an exception on startup.

ApplicationReadyEvent를 적용하면 캐시 업데이트가 정상동작한다.

캐시를 업데이트해야하는 클래스 implements ApplicationListener<ApplicationReadyEvent> {
      @Override
      public void onApplicationEvent(final ContextRefreshedEvent event) {
         //캐시 업데이트 실행.
      }
}

더 알게 된내용

  1. @PostConstruct는 스프링 빈 라이플사이클 전용이 아니다. JSR250 스펙이다.
  2. 스프링 2.5 버전부터 스프링에서 지원하기 시작했다.

참고