Spring Boot 3에서 @EnableCaching을 활용해 다양한 캐싱 전략을 적용하는 방법을 소개합니다. 실전 예제와 함께 메모리, Redis, Caffeine 등 다양한 캐시 구현체의 특징과 적용법을 쉽게 설명합니다.

개요

현대 웹 애플리케이션에서 성능 최적화는 매우 중요한 요소입니다. 그 중에서도 캐싱은 데이터베이스나 외부 API 호출을 줄이고, 응답 속도를 향상시키는 데 큰 역할을 합니다. Spring Boot 3에서는 @EnableCaching 어노테이션을 통해 손쉽게 캐싱 기능을 활성화할 수 있습니다. 본 포스트에서는 Spring Boot 3 환경에서 다양한 캐시 구현체(메모리, Redis, Caffeine 등)를 활용해 캐싱을 적용하는 방법을 상세히 다룹니다.

1. @EnableCaching 기본 개념

@EnableCaching은 Spring에서 캐싱 기능을 활성화하는 어노테이션입니다. 이 어노테이션을 사용하면, @Cacheable, @CachePut, @CacheEvict와 같은 캐시 관련 어노테이션을 사용할 수 있습니다.

// Application 클래스에 추가
@SpringBootApplication
@EnableCaching
class Application

이제 서비스 레이어에서 메서드 캐싱이 가능합니다.

2. 기본 메모리 캐시(SimpleCacheManager)

Spring Boot는 기본적으로 ConcurrentMap 기반의 SimpleCacheManager를 제공합니다. 별도의 설정 없이도 바로 사용할 수 있습니다.

@Service
class UserService {
    @Cacheable("users")
    fun getUserById(id: Long): User {
        // DB 조회 로직
    }
}
  • @Cacheable("users"): users라는 이름의 캐시에 결과를 저장합니다.
  • 기본적으로 JVM 내 메모리를 사용하므로, 서버 재시작 시 캐시가 사라집니다.

3. Redis 캐시 적용하기

대규모 서비스에서는 분산 캐시가 필요합니다. Redis는 대표적인 인메모리 데이터 저장소로, Spring Boot와 쉽게 연동할 수 있습니다.

의존성 추가 (build.gradle.kts)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

설정 파일(application.yml)

spring:
  cache:
    type: redis
  redis:
    host: localhost
    port: 6379

RedisCacheManager 빈 등록 (옵션)

@Configuration
class CacheConfig {
    @Bean
    fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager {
        return RedisCacheManager.builder(redisConnectionFactory).build()
    }
}
  • Redis 서버가 필요하며, 네트워크를 통해 여러 인스턴스가 캐시를 공유할 수 있습니다.
  • TTL(Time To Live) 설정, 직렬화 포맷 등 세부 설정도 가능합니다.

4. Caffeine 캐시 적용하기

Caffeine은 JVM에서 동작하는 고성능 캐시 라이브러리입니다. Guava Cache의 후속작으로, LRU, LFU 등 다양한 정책을 지원합니다.

의존성 추가 (build.gradle.kts)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-cache")
    implementation("com.github.ben-manes.caffeine:caffeine")
}

설정 파일(application.yml)

spring:
  cache:
    type: caffeine
  caffeine:
    spec: maximumSize=500,expireAfterWrite=600s
  • JVM 내에서 동작하지만, 다양한 만료 정책과 높은 성능을 제공합니다.
  • 대규모 분산 캐시가 필요 없을 때 적합합니다.

5. 커스텀 캐시 구현 및 활용

특정 요구사항에 맞춰 커스텀 캐시를 직접 구현할 수도 있습니다. CacheManager 인터페이스를 구현하거나, 기존 캐시 구현체를 조합할 수 있습니다.

import org.springframework.cache.Cache
import org.springframework.cache.CacheManager
import org.springframework.cache.support.SimpleValueWrapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.concurrent.ConcurrentHashMap

// 단일 메모리 캐시 구현 예시
class SimpleCustomCache(private val name: String) : Cache {
    private val store = ConcurrentHashMap<Any, Any?>()

    override fun getName(): String = name

    override fun getNativeCache(): Any = store

    override fun get(key: Any): Cache.ValueWrapper? =
        store[key]?.let { SimpleValueWrapper(it) }

    override fun <T : Any?> get(key: Any, type: Class<T>): T? =
        store[key]?.let { type.cast(it) }

    override fun put(key: Any, value: Any?) {
        store[key] = value
    }

    override fun evict(key: Any) {
        store.remove(key)
    }

    override fun clear() {
        store.clear()
    }
}

// 커스텀 CacheManager 구현
class CustomCacheManager : CacheManager {
    private val caches = listOf(SimpleCustomCache("customCache"))

    override fun getCache(name: String): Cache? =
        caches.find { it.name == name }

    override fun getCacheNames(): Collection<String> =
        caches.map { it.name }
}

@Configuration
class CacheConfig {
    @Bean
    fun cacheManager(): CacheManager = CustomCacheManager()
}
  • 위 예시는 customCache라는 이름의 단일 메모리 캐시를 제공합니다.
  • 실제 서비스에서는 여러 캐시 인스턴스를 동적으로 생성하거나, 만료 정책 등을 추가할 수 있습니다.

6. 실전 예제: 다양한 캐시 전략 조합하기

여러 캐시 전략을 조합해 사용하는 것도 가능합니다. 예를 들어, 일부 데이터는 Redis에, 일부는 Caffeine에 저장할 수 있습니다.

@Configuration
class MultiCacheConfig {
    @Bean
    fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager {
        val caffeineCacheManager = CaffeineCacheManager()
        caffeineCacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES))

        val redisCacheManager = RedisCacheManager.builder(redisConnectionFactory).build()

        return CompositeCacheManager(caffeineCacheManager, redisCacheManager)
    }
}

7. 캐시 무효화와 주의사항

  • @CacheEvict로 캐시를 삭제하거나, 조건부로 무효화할 수 있습니다.
  • 캐시 데이터와 실제 데이터의 불일치(캐시 일관성 문제)에 주의해야 합니다.
  • 적절한 만료 정책과 캐시 크기 설정이 중요합니다.

8. 캐시 테스트 및 모니터링

  • Spring Boot Actuator를 활용해 캐시 상태를 모니터링할 수 있습니다.
  • JMX, 로그, 별도 모니터링 툴과 연동해 캐시 적중률, 만료, 용량 등을 확인하세요.

참고 자료 및 레퍼런스

Spring Boot 3에서 다양한 캐시 전략을 조합하면, 서비스의 성능과 확장성을 크게 향상시킬 수 있습니다. 상황에 따라 적절한 캐시 구현체를 선택하고, 일관성 유지와 모니터링에도 신경쓰세요.