SW 개발/Spring

Spring Boot, Spring Cache, Infinispan 연동하기

지단로보트 2021. 12. 28. 21:23

개요

  • JBoss Infinispan에 대해서는 본 블로그의 이 글에서 자세히 소개한 적이 있다. 이번 글에서는 여러 캐시 라이브러리가 공존하는 복잡한 프로젝트 환경에서 JBoss Infinispan을 추가하고, Spring CacheJCache를 연동하는 방법을 정리하였다.

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
    }
}

참고 글