Spring Cache 알아보기 - 1

2024년 08월 03일 00:00:00


목차

  • No items found

개발을 하며 알게 된 것들을 개인적으로 적고 공유 하는 거라 내용이 많이 틀릴 수 있습니다.

이번에는 스프링부트에서 캐시를 사용하며 알게 된 내용이나 여러가지 정보들을 정리하려고 합니다.

1편에서는 주로 스프링부트에서 등록 및 어노테이션을 이용한 간단한 사용방법에 대해 알아봅시다.

캐시란?

일단 딱딱하게 정의부터 말하자면 데이터를 임시로 저장해두는 메모리 영역이라고 볼 수 있습니다.

저는 백엔드 개발자인데 캐시는 주로 API가 호출되었을때 응답값으로 주는 값이 자주 변하지 않는 데이터를 캐싱 처리하여 많이 사용합니다.

ex) 날짜에 따라 값이 변하는 API일때 < 이런경우는 하루동안 데이터가 변할일이 없기 때문에 캐시에 저장해서 응답해주면 빠르겠죠?

이렇게 할 경우 만약 DB에 데이터를 가져와서 계산을 해서 응답을 주는 API라고하면 캐시저장소에 있는 값을 바로 꺼내서 사용자에게 응답을 내려주게 됩니다.

또한 TTL도 길게 잡아도 상관이 없겠죠? 왜냐하면 날짜를 키값으로 캐시에 저장한다면 날짜에 따라 다른 캐시값이 조회가 될테니까요.

TTL(Time To Live) => 캐시의 유효시간이라고 보시면 됩니다.

그럼 본격적으로 스프링에서 캐시를 사용하는 방법을 알아봅시다.

저도 자세히는 알지 못하지만 스프링은 많은 것들을 추상화하여 사용자가 손쉽게 사용할 수 있도록 제공해줍니다.

일단 스프링에서 사용하기 위해 의존성 부터 추가해줍시다.

스프링 캐시 의존성 추가

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache/3.3.2 Maven 저장소로 갑시다. (작성시점 스프링부트 버전임)

Gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-cache:3.3.2'

프로젝트의 build.gradle에 추가해줍시다.

CacheManager

추상화된 캐시를 사용하기 위해 스프링에서 제공하는것이 Cache Manager입니다.

Cache Manager를 구현해놓은 구현체는 다양하게 존재하는데 대략적인 것은 다음과 같습니다.

다양하게 많은 캐시 매니저가 있으며 Abstract가 붙은 Class 는 추상클래스로 해당 기능을 상속받아 커스텀하게 구현하고 싶을때 사용하시면 됩니다.

이밖에도 많은 캐시 매니저들이 있습니다. 캐시저장소로 많이들 사용하는 redis라던지 이런 경우에도 스프링부트의 redis 의존성을 추가하면 사용하실 수 있습니다.

인텔리지에서 구현체 검색했을때의 목록

Alt text

프로젝트 캐시매니저 설정 추가

외부 의존성을 제외하고 단순히 로컬에서 캐시를 사용하기 위해 저는 CaffeineCacheManager를 이용하여 추가해보겠습니다.

만약 Redis나 외부저장소에 저장되는 캐시매니저를 사용한다면 key,value의 Serialize와 Deserialize설정을 잘해주셔야 문제가 발생하지 않아요. 나중에 이부분도 자세히 작성 할 예정입니다.

캐시 설정추가
@EnableCaching // 캐시를 사용하려면 추가하여야합니다.
@Configuration
class CacheConfig {
    @Bean(name = ["caffeineCacheManager"])
    fun cacheManager(): CacheManager =
        CaffeineCacheManager().apply {
            setCaffeine(
                Caffeine.newBuilder()
                    .initialCapacity(200)
                    .maximumSize(500)
                    .expireAfterWrite(JwtProvider.REFRESH_TOKEN_VALID_MILLISECONDS, TimeUnit.MILLISECONDS)
                    .weakKeys()
            )
        }
}
 

@EnableCaching

@EnableCaching
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({CachingConfigurationSelector.class})
public @interface EnableCaching {
    boolean proxyTargetClass() default false;
    //용도: 캐시 어드바이저가 CGLIB 프록시를 사용할지 여부를 결정합니다.
    //설명: false(기본값)으로 설정된 경우 JDK 동적 프록시를 사용하고, true로 설정된 경우 CGLIB를 사용하여 프록시를 생성합니다. CGLIB를 사용하면 클래스 전체를 프록시로 감쌀 수 있으며, 인터페이스가 없는 클래스에 대해 프록시를 생성할 수 있습니다.
 
    AdviceMode mode() default AdviceMode.PROXY;
    //용도: 캐싱 어드바이스의 모드를 설정합니다.
    //설명: AdviceMode.PROXY가 기본값으로, 프록시 기반의 어드바이저를 사용하여 캐싱 기능을 적용합니다. AdviceMode.ASPECTJ를 설정할 경우, AspectJ를 사용하여 어드바이스를 적용할 수 있습니다. AdviceMode.PROXY는 프록시 기반의 캐싱을 사용하는 기본적인 방식입니다.
 
    int order() default Integer.MAX_VALUE;
    //용도: 캐시 어드바이저의 실행 순서를 설정합니다.
    //설명: order 속성은 여러 어드바이저가 있을 때, 이들의 실행 순서를 정의합니다. 기본값인 Integer.MAX_VALUE는 가장 높은 우선순위를 가지며, 기본적으로 가장 마지막에 실행됩니다. 이를 통해 어드바이저의 우선순위를 조정할 수 있습니다.
}

@Cacheable

어노테이션은 아래처럼 정의 되어있습니다. 각각의 자세한 설명은 ChatGPT의 도움을 받았습니다.

@Cacheable
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Cacheable {
    @AliasFor("cacheNames")
    String[] value() default {};
    //용도: 캐시 이름을 지정합니다. cacheNames 속성과 동일하게 동작하며, 두 속성은 서로의 별칭(alias)입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    @AliasFor("value")
    String[] cacheNames() default {};
    //용도: 캐시 이름을 지정합니다. value 속성과 동일하게 동작하며, 두 속성은 서로의 별칭입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    String key() default "";
    //용도: 캐시 항목을 저장할 때 사용할 키를 SpEL(Spring Expression Language)로 지정합니다.
    //설명: 기본값은 빈 문자열이며, 메서드 파라미터를 기준으로 자동 생성됩니다. 특정 키를 명시적으로 지정하고 싶을 때 사용합니다.
 
    String keyGenerator() default "";
    //용도: 커스텀 키 생성기를 지정합니다.
    //설명: 기본적으로는 스프링이 제공하는 키 생성기를 사용하지만, keyGenerator 속성을 통해 커스텀 키 생성기를 명시할 수 있습니다.
 
    String cacheManager() default "";
    //용도: 사용할 캐시 매니저 빈의 이름을 지정합니다.
    //설명: 여러 캐시 매니저가 있는 경우, 특정 캐시 매니저를 명시적으로 지정할 때 사용합니다.
 
    String cacheResolver() default "";
    //용도: 캐시 리졸버를 지정합니다.
    //설명: 캐시 리졸버는 어떤 캐시를 사용할지 결정하는 역할을 합니다. cacheResolver를 통해 커스텀 캐시 리졸버를 지정할 수 있습니다.
 
    String condition() default "";
    //용도: 캐시 조건을 지정합니다.
    //설명: SpEL 표현식을 사용하여 조건을 지정할 수 있으며, 조건이 참일 경우에만 캐싱이 이루어집니다.
 
    String unless() default "";
    //용도: 캐시에서 제외할 조건을 지정합니다.
    //설명: SpEL 표현식을 사용하여 조건을 지정할 수 있으며, 조건이 참일 경우 캐싱되지 않습니다. condition과 반대로 동작합니다.
 
    boolean sync() default false;
    //용도: 캐시를 동기적으로 사용할지 여부를 지정합니다.
    //설명: true로 설정하면 캐시를 동기적으로 사용합니다. 이는 캐시가 아직 로드되지 않았을 때, 동일한 키에 대한 여러 호출이 블로킹되지 않도록 보장합니다.
}

사용예) users라는 캐시이름을 갖고 있으며 키값으로 username으로 된값이 없으면 UserDetails값을 저장하게됩니다. 있으면 캐시 값을 반환해줍니다.

@Cacheable
@Cacheable(value = ["users"], key = "#username")
override fun loadUserByUsername(username: String): UserDetails {
    return userRepository.findByIdOrNull(username.toLong())?.run {
        CurrentUserInfo(
            userId = "userId",
            nickName = nickName,
            userNo = userNo!!,
            role = role,
        )
    } ?: throw UserNotFoundException()
}

@CacheEvict

애노테이션은 아래처럼 정의 되어있습니다. 대부분 Cacheable와 동일한 것을 볼 수 있습니다. 각각의 자세한 설명은 ChatGPT의 도움을 받았습니다.

@CacheEvict
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CacheEvict {
    @AliasFor("cacheNames")
    String[] value() default {};
    //용도: 캐시 이름을 지정합니다. cacheNames 속성과 동일하게 동작하며, 두 속성은 서로의 별칭(alias)입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    @AliasFor("value")
    String[] cacheNames() default {};
    //용도: 캐시 이름을 지정합니다. value 속성과 동일하게 동작하며, 두 속성은 서로의 별칭입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    String key() default "";
    //용도: 캐시 항목을 저장할 때 사용할 키를 SpEL(Spring Expression Language)로 지정합니다.
    //설명: 기본값은 빈 문자열이며, 메서드 파라미터를 기준으로 자동 생성됩니다. 특정 키를 명시적으로 지정하고 싶을 때 사용합니다.
 
    String keyGenerator() default "";
    //용도: 커스텀 키 생성기를 지정합니다.
    //설명: 기본적으로는 스프링이 제공하는 키 생성기를 사용하지만, keyGenerator 속성을 통해 커스텀 키 생성기를 명시할 수 있습니다.
 
    String cacheManager() default "";
    //용도: 사용할 캐시 매니저 빈의 이름을 지정합니다.
    //설명: 여러 캐시 매니저가 있는 경우, 특정 캐시 매니저를 명시적으로 지정할 때 사용합니다.
 
    String cacheResolver() default "";
    //용도: 캐시 리졸버를 지정합니다.
    //설명: 캐시 리졸버는 어떤 캐시를 사용할지 결정하는 역할을 합니다. cacheResolver를 통해 커스텀 캐시 리졸버를 지정할 수 있습니다.
 
    String condition() default "";
    //용도: 캐시 조건을 지정합니다.
    //설명: SpEL 표현식을 사용하여 조건을 지정할 수 있으며, 조건이 참일 경우에만 캐싱이 이루어집니다.
 
    boolean allEntries() default false;
    //용도: 캐시 내의 모든 항목을 제거할지 여부를 지정합니다.
    //설명: true로 설정하면 지정된 캐시 이름의 모든 항목이 제거됩니다. key 속성보다 우선적으로 적용됩니다.
 
    boolean beforeInvocation() default false;
    //용도: 메서드 호출 전(before) 또는 후(after)에 캐시 항목을 제거할지 여부를 지정합니다.
    //설명: true로 설정하면 메서드가 호출되기 전에 캐시 항목을 제거합니다. 기본값은 false로, 메서드 호출 후에 캐시 항목을 제거합니다.
}
 

사용예) users라는 캐시이름을 갖고 있으며 키값으로 username으로 된 값을 찾아 메서드가 정상 실행되면 캐시 저장소에서 삭제하게됩니다.

@CacheEvict(value = ["users"], key = "#username")
fun remove(username: String) {
    ....
}

@CachePut

@CachePut
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CachePut {
    @AliasFor("cacheNames")
    String[] value() default {};
    //용도: 캐시 이름을 지정합니다. cacheNames 속성과 동일하게 동작하며, 두 속성은 서로의 별칭(alias)입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    @AliasFor("value")
    String[] cacheNames() default {};
    //용도: 캐시 이름을 지정합니다. value 속성과 동일하게 동작하며, 두 속성은 서로의 별칭입니다.
    //설명: 캐시 이름을 지정할 때 사용되며, 하나 이상의 캐시 이름을 배열 형태로 설정할 수 있습니다.
 
    String key() default "";
    //용도: 캐시 항목을 저장할 때 사용할 키를 SpEL(Spring Expression Language)로 지정합니다.
    //설명: 기본값은 빈 문자열이며, 메서드 파라미터를 기준으로 자동 생성됩니다. 특정 키를 명시적으로 지정하고 싶을 때 사용합니다.
 
    String keyGenerator() default "";
    //용도: 커스텀 키 생성기를 지정합니다.
    //설명: 기본적으로는 스프링이 제공하는 키 생성기를 사용하지만, keyGenerator 속성을 통해 커스텀 키 생성기를 명시할 수 있습니다.
 
    String cacheManager() default "";
    //용도: 사용할 캐시 매니저 빈의 이름을 지정합니다.
    //설명: 여러 캐시 매니저가 있는 경우, 특정 캐시 매니저를 명시적으로 지정할 때 사용합니다.
 
    String cacheResolver() default "";
    //용도: 캐시 리졸버를 지정합니다.
    //설명: 캐시 리졸버는 어떤 캐시를 사용할지 결정하는 역할을 합니다. cacheResolver를 통해 커스텀 캐시 리졸버를 지정할 수 있습니다.
 
    String condition() default "";
    //용도: 캐시 조건을 지정합니다.
    //설명: SpEL 표현식을 사용하여 조건을 지정할 수 있으며, 조건이 참일 경우에만 캐싱이 이루어집니다.
 
    String unless() default "";
    //용도: 캐시에서 제외할 조건을 지정합니다.
    //설명: SpEL 표현식을 사용하여 조건을 지정할 수 있으며, 조건이 참일 경우 캐싱되지 않습니다. condition과 반대로 동작합니다.
}
 

사용예) users라는 캐시이름을 갖고 있으며 키값으로 username으로 된 UserDetails값을 저장하게됩니다.

@CachePut(value = ["users"], key = "#username")
override fun loadUserByUsername(username: String): UserDetails {
    return userRepository.findByIdOrNull(username.toLong())?.run {
        CurrentUserInfo(
            userId = "userId",
            nickName = nickName,
            userNo = userNo!!,
            role = role,
        )
    } ?: throw UserNotFoundException()
}

@Caching

@Caching
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Caching {
    Cacheable[] cacheable() default {};
 
    CachePut[] put() default {};
 
    CacheEvict[] evict() default {};
}

사용예) 아래처럼 사용하진 않겠지만 여러개를 적용하는 방법의 예시입니다.

@Caching(
    cacheable = [Cacheable(value = ["productCache"], key = "#product.id")],
    put = [CachePut(value = ["productCache"], key = "#product.id")],
    evict = [CacheEvict(value = ["productCache"], key = "#product.id")]
)
fun updateProduct(product: Product): Product {
    return product
}

@CacheConfig

@CacheConfig
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
    String[] cacheNames() default {};
    //용도: 캐시의 이름을 정의합니다.
    //설명: 캐시를 적용할 때 사용할 캐시 이름을 배열 형태로 지정할 수 있습니다. 여러 캐시 이름을 지정할 수 있으며,
    //     이 이름은 @Cacheable, @CachePut, @CacheEvict 애노테이션의 value 또는 cacheNames 속성에 사용될 수 있습니다.
    String keyGenerator() default "";
    //용도: 캐시 키를 생성할 때 사용할 커스텀 키 생성기의 이름을 지정합니다.
    //설명: 지정된 이름의 커스텀 KeyGenerator 빈을 사용하여 캐시 키를 생성합니다. 기본적으로는 스프링의 기본 키 생성기를 사용하지만, 이 속성을 통해 커스텀 키 생성기를 사용할 수 있습니다.
    String cacheManager() default "";
    //용도: 사용할 캐시 매니저 빈의 이름을 지정합니다.
    //설명: 여러 개의 캐시 매니저가 있을 때, 특정 캐시 매니저를 지정하여 캐시 작업을 수행합니다. 이 속성에 지정된 캐시 매니저가 캐시 작업에 사용됩니다.
 
    String cacheResolver() default "";
    //용도: 캐시 리졸버의 이름을 지정합니다.
    //설명: 캐시 리졸버는 캐시를 선택할 때 사용하는 빈입니다. 특정 캐시 리졸버를 지정하여 캐시를 동적으로 선택할 수 있습니다.
}

사용예)

@Service
@CacheConfig(
    cacheNames = ["productCache"],
    keyGenerator = "keyGenerator",
    cacheManager = "cacheManager"
)
class ProductService {
 
    @Cacheable
    fun getProductById(productId: String): Product {
        // 제품 정보를 조회하고 반환
    }
 
    @CachePut
    fun updateProduct(product: Product): Product {
        // 제품 정보를 업데이트하고 반환
        return product
    }
}

JJ

황재정

백엔드 개발자로 일하고 있습니다.

Github