Multiple CacheManager 이슈 기록
spring framework로 개발을 진행하고 Cache를 사용해본 사람이 보면 좋을 것 같다.
프로젝트를 진행 중에 CacheManger를 두 개를 별도로 가져가야 할 일이 생겼다.
회사에서 사용중인 공통모듈에 EhCacheManager가 추가가 되었는데, 현재 진행중인 프로젝트에서 이미 EhCacheManager를 사용중인 상태여서 문제가 발생했다.
해결방법은 간단하다.
하지만 어디에서 Exception을 발생하는지 그리고 어떤 과정을 통해 진행 되다가 Exception이 발생하는지 궁금해서 기록을 남기려고 한다.
문제 재현
추가한 의존성은 다음과 같다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
...
etc..
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.10</version>
</dependency>
web 프로젝트를 추가하긴 했지만 실제로 브라우져에서 확인할 일은 없고, console에 서버가 잘 실행되는지 확인만 하려는 이유에서 추가했다.
security 프로젝트 역시 다중 Bean 설정을 확인하려고 추가한 것이고 다른 이유는 없다.
spring-boot-starter-cache와, ehcache-core 는 정말 필요해서 추가했다.
우선 다음과 같이 설정을 해보자.
@Configuration
@EnableCaching
public class DefaultCacheConfig {
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
위와 같이 설정한 뒤 어플리케이션을 실행시켜 보도록 하자.
정상적으로 잘 실행이 될 것이다.(안된다면...로그를 잘 확인해보자.)
그럼 에러가 나는 상황을 만들어 보도록 하자.
@Configuration
@EnableCaching
public class DefaultCacheConfig {
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public CacheManager cacheManagerAnother() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
CacheManager Bean을 하나 더 추가 했다.
그리고 다시 어플리케이션을 실행시켜 보면 다음과 같은 메세지를 출력하면서 Exception이 발생 할 것이다.
빨간색 부분을 해석해 보면
CacheResolver가 지정되지 않았으며, CacheManager 타입으로 된 고유한 Bean이 없습니다.
한 개를 Primary로 지정하거나, 사용할 CacheManager를 지정하세요.
첫번 째 방법 : Primary
위에 나온대로 하나를 Primary로 설정해 보자.
Java Config를 사용하는 경우에는 @Primary annotation을 추가하면 되고, xml config를 사용하는 경우 빈 설정 시 primary=true 를 추가하면 된다.
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
@Primary
public CacheManager cacheManagerAnother() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
다시 실행해 보면 정말 Exception이 발생하지 않는 것을 확인할 수 있을 것이다.
잘 되니까 궁금해졌다.
그래서 Primary로 지정하지 않은 상태에서 다시 어플리케이션을 실행하고 Exception을 찾아가본다.
CacheAspectSupport 클래스의 afterSingletonsInstantiated 메소드에서 NoUniqueBeanDefinitionException이 발생한 것이다.
즉 노란색 박스로 되어 있는 setCacheManager 메소드를 실행하는 과정에서 에러가 발생한 것이다.
더 자세하게는 Bean 관련 Exception이기 때문에 beanFactory.getBean 메소드를 실행하는 과정에서 발생했을 것이다. 또 찾아가본다.
디버깅을 통해서 메소드를 따라가다 보면 DefaultListableBeanFactory 클래스의 resolveNamedBean 메소드를 만나게 된다.
-
RequireType 이 org.springframework.cache.CacheManager 인 이름의 Bean 후보군들을 가져온다.
-
candidateNames 에 한 개만 있다면 그 이름으로 된 Bean 을 리턴한다.
-
1개 보다 많다면 Bean이 Primary Bean 을 반환한다. 이 때 Primary로 구분이 불가능 하면 NoUniqueBeanDefinitionException 을 throw 하게된다.
그렇다. 두 개 이상의 CacheManager를 Bean으로 등록해서 사용할 때에는 반드시 하나의 Bean에 Primary 설정을 해줘야 한다.
두 번째 방법 : CachingConfigurer (구현체 : CachingConfigurerSupport)
하나의 Bean을 Primary로 설정하는 방법 말고 다른 한가지 방법이 더 있다.
Config 클래스에서 CachingConfigurer를 구현하거나 CachingConfigurerSupport를 상속하면 된다.
Bean에서 @Primary annotation을 삭제하고 CachingConfigurerSupport를 상속받은 뒤에 다시 실행시켜 보자.
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public CacheManager cacheManagerA() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
문제 없이 잘 실행되는 것을 확인할 수 있다.
왜일까?? 찾아가본다.
위 그림에서 확인할 수 있듯이 getCacheResolver() 메소드가 null 이 아니다.
따라서 beanFactory.getBan() 메소드가 실행되지 않게 되고 NoUniqueBeanDefinitionException 이 발생하지 않게 됐던 것이다.
getCacheResolver 메소드의 내용은 다음과 같다.
@Nullable
public CacheResolver getCacheResolver() {
return SupplierUtils.resolve(this.cacheResolver);
}
즉 CachingConfigurerSupport를 상속받은 뒤에 cacheResolver가 더이상 null이 아닌 상태가 되었다.
cacheResolver가 어떻게 null이 아닌 상태가 될 수 있었는지 확인해 보니 다음과 같은 순서대로 진행되었다.
순서대로 글을 쓴 뒤에 이미지를 첨부하려고 한다.
- ProxyCachingConfiguration 설정 클래스가 실행되는 과정중에 상위 클래스인 AbstractCachingConfiguration 에 있는 setConfigurers 메소드가 실행이 된다.
- setConfigurers 메소드에서 useCachingConfigurer 메소드를 호출하는데, 이 때 파라미터로 받은 CachingConfigurer 객체 에서 cacheManager 를 가져와서 this.cacheManager 에 주입한다.
- ProxyCachingConfiguration 설정 클래스에 있는 BeanFactoryCacheOperationSourceAdvisor Bean이 등록되는 과정에서 CacheInterceptor Bean이 추가로 등록된다.
- CacheInterceptor Bean이 등록되는 과정에서 cacheResolver가 등록이 된다.
- cacheResolver가 등록이 되었기 때문에 getCacheResolver 메소드가 null이 아니므로 Bean을 찾지 않게 되고 에러가 나지 않게 되는 것이다.
글로 요약하면 감이 잘 오지 않기 때문에 소스 이미지를 추가하면서 확인해 보겠다.
1.ProxyCachingConfiguration 설정 클래스가 실행되는 과정중에 상위 클래스인 AbstractCachingConfiguration 에 있는 setConfigurers 메소드가 실행이 된다.
2. setConfigurers 메소드에서 useCachingConfigurer 메소드를 호출하는데, 이 때 파라미터로 받은 CachingConfigurer 객체 에서 cacheManager 를 가져와서 this.cacheManager 에 주입한다.
3.ProxyCachingConfiguration 설정 클래스에 있는 BeanFactoryCacheOperationSourceAdvisor Bean이 등록되는 과정에서 CacheInterceptor Bean이 추가로 등록된다.
4.CacheInterceptor Bean이 등록되는 과정에서 cacheResolver가 등록이 된다.
5.cacheResolver가 등록이 되었기 때문에 getCacheResolver 메소드가 null이 아니므로 Bean을 찾지 않게 되고 에러가 나지 않게 되는 것이다.
마지막으로, CachingConfigurerSupport 클래스를 Config 클래스에서 상속할 때에만 AbstractCachingConfiguration 클래스의 setConfigurers 메소드가 실행되는 이유를 설명하고 끝내겠다.
스프링이 Bean을 등록하는 과정에서 @Autowired annotation이 있는 메소드가 있으면 실행하도록 되어 있다.
하지만 이 때 한가지 조건이 있는데 해당 메소드로 넘겨질 parameter가 있을 경우에만 실행이 된다.
아래 소스를 보자. ( 너무 길어서 캡쳐를 따로 했다. 이어서 보면 된다.)
노란색 박스를 보면 arguments가 null 이 아니면 beanName으로 빈을 생성한다.
빨간색 박스를 보면 arguments가 null 이 아니면 method 를 실행한다.
method.invoke(bean, arguments); 가 실행이 되면 AbstractCachingConfiguration 클래스의 setConfigurers 메소드가 실행되는 것이다.
그런데 arguments에는 무엇이 들어 있을까??
beanFactory.resolveDependency 메소드를 실행 시켜서 Object 객체를 가져오게 되는데
org.springframework.cache.annotation.CachingConfigurer 객체를 가져오려고 한다.
우리가 등록한 설정 클래스인 DefaultCacheConfig 클래스에서 CachingConfigurerSupport 클래스를 상속 받았고 이 클래스는 org.springframework.cache.annotation.CachingConfigurer 인터페이스의 구현체이기 때문에 위에 있는 이미지에서
Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
를 실행한 결과 arg가 null 이 아니게 되며 arguments 역시 null아 아니게 되므로
method.invoke(bean, arguments); 메소드가 실행이 되었던 것이다.
이상으로 기록을 마친다.