SW 개발/Spring
Spring Boot, Spring Cache, Infinispan 연동하기
지단로보트
2021. 12. 28. 21:23
개요
JBoss Infinispan
에 대해서는 본 블로그의 이 글에서 자세히 소개한 적이 있다. 이번 글에서는 여러 캐시 라이브러리가 공존하는 복잡한 프로젝트 환경에서 JBoss Infinispan을 추가하고,Spring Cache
와JCache
를 연동하는 방법을 정리하였다.
Spring Cache 적용 원리
- 프로젝트의 @Configuration 빈의 클래스 레벨에
@EnableCaching
을 명시하면, 전처리기가 활성화되어 모든 스프링 빈의 퍼블릭 메써드 레벨에 명시된@Cacheable
,@CachePut
,@CacheEvict
를 확인하여 캐시가 작동하게 된다. - 스프링 캐시는 별도의 설정을 하지 않고 활성화할 경우, 단순히 CuncurrentHashMap 오브젝트를 캐시로 사용하게 된다. Infinispan의 경우, 스프링 캐시의 CacheManager 인터페이스 구현체인
org.infinispan.spring.provider.SpringEmbeddedCacheManager
빈을 생성하면 Infinispan의 스프링 캐시 역할을 수행할 수 있게 된다. (이 방법을 아래 설명하였다.) - 스프링 캐시와 Infinispan의 연동이 완료되면 스프링 캐시를 사용할 모든 빈의 클래스 레벨에서
@CacheConfig(cacheNames = ["{infinispan-cache-name}"])
와 같이 Infinispan으로 생성한 캐시의 이름을 명시할 수 있다. 이를 통해 Infinispan의 강점인Near Cache
(= Invalidation Cache)와 같은 분산 캐시를 멀티 노드에 걸쳐 스프링 캐시로 사용할 수 있다.
pom.xml
- Maven을 사용할 경우, 프로젝트의 /pom.xml에 아래 내용을 추가한다.
<properties>
<infinispan.version>13.0.5.Final</infinispan.version>
</properties>
<dependencies>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-commons</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-spring5-embedded</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-jcache</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.jgroups.aws.s3</groupId>
<artifactId>native-s3-ping</artifactId>
<version>1.0.0.Final</version>
</dependency>
</dependencies>
build.gradle.kts
- Gradle을 사용할 경우, 프로젝트의 /build.gradle.kts에 아래 내용을 추가한다.
ext {
infinispanVersion = '13.0.5.Final'
}
dependencies {
implementation("org.infinispan:infinispan-core:$infinispanVersion")
implementation("org.infinispan:infinispan-commons:$infinispanVersion")
implementation("org.infinispan:infinispan-spring5-embedded:$infinispanVersion")
implementation("org.infinispan:infinispan-jcache:$infinispanVersion")
implementation("org.infinispan:native-s3-ping:1.0.0.Final")
}
@SpringBootApplication 클래스 수정
- 여러 캐시 라이브러리가 공존할 경우, 아래와 같이
CachingProvider
빈이 복수개가 존재하여 부트스트랩 시점에 오류가 발생한다.
javax.cache.CacheException: Multiple CachingProviders have been configured when only a single CachingProvider is expected
- 아래와 같이 Infinispan만 작동하도록 수정해 주어야 한다.
@SpringBootApplication
class FooApplication
fun main(arg: Array<String>) {
val iterator = Caching.getCachingProviders(Caching.getDefaultClassLoader()).iterator()
while (iterator.hasNext()) {
val provider = iterator.next()
if (provider !is org.infinispan.jcache.embedded.JCachingProvider) {
iterator.remove()
}
}
runApplication<FooApp>(*args)
}
@EnableCaching 클래스 작성
- Infinispan의 핵심이 되는 환경 설정을 아래와 같이 작성한다. (다른 클래스에
@EnableCaching
이 명시되어 있다면 모두 제거해야 한다.)
import org.infinispan.Cache
import org.infinispan.configuration.cache.CacheMode
import org.infinispan.configuration.cache.ConfigurationBuilder
import org.infinispan.configuration.global.GlobalConfigurationBuilder
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.manager.EmbeddedCacheManager
import org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager
import org.infinispan.transaction.LockingMode
import org.infinispan.transaction.TransactionMode
import org.infinispan.util.concurrent.IsolationLevel
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
import java.util.concurrent.TimeUnit
@Configuration
@EnableCaching
class InfinispanCacheConfig(
private val environment: Environment
) {
@Bean("infinispanCacheManager")
fun infinispanCacheManager(): EmbeddedCacheManager {
val cacheName = "local-cache"
val global = GlobalConfigurationBuilder()
.transport().defaultTransport()
// 운영 환경에 따라 적합한 전송 프로토콜 파일을 지정
.addProperty("configurationFile", "default-configs/default-jgroups-udp.xml")
.clusterName("foo-cache-cluster-${environment.getProperty("SPRING_PROFILES_ACTIVE") ?: "dev"}")
.defaultCacheName(cacheName)
return DefaultCacheManager(ConfigurationBuilderHolder(Thread.currentThread().contextClassLoader, global), true)
}
@Bean("fooCache")
fun fooCache(@Qualifier("infinispanCacheManager") cacheManager: EmbeddedCacheManager): Cache<String, Any> {
// Spring Cache에서 cacheName에 사용할 캐시 이름으로 복수개의 캐시를 생성 가능
val cacheName = "foo-cache"
val config = ConfigurationBuilder()
config.apply {
this.expiration().lifespan(1, TimeUnit.MINUTES)
this.memory().maxCount(1024L)
this.clustering().cacheMode(CacheMode.INVALIDATION_ASYNC)
this.locking().isolationLevel(IsolationLevel.READ_COMMITTED).useLockStriping(false)
.lockAcquisitionTimeout(10, TimeUnit.SECONDS)
this.transaction().lockingMode(LockingMode.OPTIMISTIC).transactionMode(TransactionMode.NON_TRANSACTIONAL)
}
cacheManager.defineConfiguration(cacheName, config.build())
return cacheManager.getCache(cacheName)
}
@Bean
fun springEmbeddedCacheManager(@Qualifier("infinispanCacheManager") cacheManager: EmbeddedCacheManager?): SpringEmbeddedCacheManager {
return SpringEmbeddedCacheManager(cacheManager)
}
}
application.yml
- 캐시가 정상적으로 히트되는지 확인하려면 아래와 같이 로그 레벨을 TRACE로 조정한다.
logging:
level:
org:
springframework:
cache: TRACE
Spring Cache 사용 예
- 앞서 연동한 결과로 아래와 같이 Spring Cache를 사용할 수 있다.
import org.springframework.cache.annotation.CacheConfig
import org.springframework.cache.annotation.Cacheable
import org.springframework.cache.annotation.CachePut
import org.springframework.cache.annotation.CacheEvict
import org.springframework.stereotype.Service
@Service
// 현재 빈의 모든 캐시의 CRUD를 수행할 캐시 이름을 설정
@CacheConfig(cacheNames = ["foo-cache"])
class FooService {
// 메써드의 리턴 값을 캐시에 저장
@CachePut(key = "#foo.id")
fun createFoo(foo: Foo) {
...
return
}
// 캐시가 존재하지 않을 경우, 메써드의 리턴 값을 캐시에 저장
// 캐시가 존재할 경우, 캐시 값을 리턴
@Cacheable(key = "#id")
fun getFoo(id: String): Foo {
...
return foo
}
// 캐시에서 삭제
@CacheEvict(key = "#id")
fun deleteFoo(id: String) {
...
return
}
}