흰 스타렉스에서 내가 내리지

메서드 호출 결과를 캐시에 저장하는 @Cacheable 과 @CacheEvict 본문

Spring

메서드 호출 결과를 캐시에 저장하는 @Cacheable 과 @CacheEvict

주씨. 2024. 3. 28. 19:32
728x90

빈번하게 호출되는 API에 대해서, 매번 서비스 로직을 실행하고 응답하는 것은 비효율 적일 수 있다. (응답값이 같다는 가정 하에)

 

API 의 응답 결과를 Elasticache for Redis 를 이용하여 응답 값을 캐싱해주면 어떨까?

 

# API Caching ?

- API 의 호출에 따라 캐시를 이용하여, 서버의 부하를 줄이고 API 성능을 최적화 하여 응답시간을 단축시키는 역할을 한다.

- 데이터를 메모리에 저장하여, 빠른 검색을 가능하게 하는 기능을 제공한다.

 

# Cache 사용 목적

- DB 로부터 데이터를 조회하는 경우, 동일한 데이터를 반복하여 조회함으로써 불필요한 일을 반복하는 문제가 발생한다.

- 캐시를 통해, DB 로부터 반복적으로 데이터를 조회해 오는 일에 대해, 최초 데이터를 조회해 온 뒤 이후는 캐시에서 데이터를 조회해 오는 처리를 수행함으로써 API 의 성능을 올리며 응답시간을 단축하는 효율성을 가져올 수 있다. 

 

 

# Cache 어노테이션 

Annotation 설명
@EnableCaching Cache 를 사용하기 위해 '캐시 활성화' 를 위한 어노테이션
@CacheConfig 캐시정보를 '클래스 단위' 로 사용하고 관리하기 위한 어노테이션을 의미한다.
(주로 @Service 를 선언한 인터페이스의 구현체 부분에서 함께 선언하여 사용한다)
@Cacheable 캐시정보를 메모리 상에 '저장'하거나 '조회'해오는 기능을 수행
(주로 @Service 를 선언한 인터페이스의 구현체 부분에서 함께 선언하여 사용한다)
@CachePut 캐시정보를 메모리 상에 '저장'하며 존재 시 갱신하는 기능을 수행
(")
@CacheEvict 캐시정보를 메모리 상에 '삭제'하는 기능을 수행
@Caching 여러 개의 '캐시 어노테이션'을 '함께 사용'할 때 사용하는 어노테이션 

 

Annotation 주요 기능 캐시 실행 시점
@Cacheable 캐시 조회, 저장 기능 - 캐시 존재 시 '메서드 호출 전 실행'
- 캐시 미 존재 시 '메서드 호출 후 실행'
@CachePut 캐시 저장 기능 - 캐시 존재 시 '메서드 호출 후 실행'
- 캐시 미 존재 시 '메서드 호출 후 실행'
@CacheEvict 캐시 삭제 기능 - beforeInvocation 속성값이 true 일 때 '메서드 호출 전 실행'
- beforeInvocation 속성값이 false 일 떄 '메서드 호출 후 실행'

 

 

# @CacheEvict 의 beforeInvocation?

- 기본값은 false

- 메서드 실행 전에 캐시에서 데이터를 삭제하여, 예외가 발생하면 캐시에서 데이터가 삭제되지 않음

- 예외가 발생할 가능성이 있으면 true 를 사용하지 말 것

 

 

# @Cacheable 과 @CachePut 의 차이는?

- @Cacheable 은 '캐시가 존재하지 않을 경우' 캐시를 저장하지만,

   @CachePut 의 경우는 '캐시의 존재 여부를 떠나서' 항상 저장 혹은 갱신을 수행합니다. 

 

 

 

캐시로 사용하면 좋을 값

1. 조회가 빈번히 발생하나, 수정은 거의 발생하지 않는 값

2. 조회에 오랜 시간이 발생하는 값 

 

 

# RedisCacheConfiguration 설정

@EnableCaching
@Configuration
public class RedisCacheConfig {
    
    @Bean
    @Primary
    public CacheManager studentIdCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // Student Id 기반 조회는 30초로 설정
        RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration()
                .entryTtl(Duration.ofSeconds(30L));
 
        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }
    
    @Bean
    public CacheManager refreshTokenCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration()
                .entryTtl(Duration.ofDays(3));
 
        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }
 
    private RedisCacheConfiguration generateCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new StringRedisSerializer()))
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new GenericJackson2JsonRedisSerializer()));
    }
}

 

- 사용 사례는 아래 곧 나옵니다! 

- @Cacheable 어노테이션에서 쓰일 예정

 

# @Cacheable, @CacheEvict 적용

@Cacheable 어노테이션으로 해당 메서드의 반환 값을 캐싱할 수 있으며, @CacheEvict 어노테이션으로 해당 작업이 수행되면 캐시된 값을 삭제시키게 할 수 있다.

 

 

# Code

# EnableCaching

 

- 캐시를 사용하기 위해 필요한 어노테이션으로, Spring Boot 에서 캐싱을 활성화하는 데 사용되며, @Configuration 이 포함된 클래스에서 이를 적용한다.

 

 

# Domain : Student

 

- 간단하게 Student 를 조회하고, 생성하고, 수정하는 API 를 만들어 보았다. 

- MySQL 의 Student 테이블에는 1000명의 데이터를 넣었다. 

 

 

  •  value 값이 중복될 경우를 대비하여 key 값을 추가로 설정해 준다.
    • redis-cli 에서 studentId::${id} 로 설정된다. 
  • 이제 Student 를 Id 기반으로 조회하면 먼저 O(1) 시간복잡도로 조회한다.
  • In-Memory DB 이기 때문에 I/O 비용이 매우 적게 발생한다.
  • 만약 Redis 서버에서 캐시 데이터를 발견하지 못하면 실제 DB 에 I/O 작업을 통해 데이터를 가져오고, 해당 데이터를 캐싱하여 TTL 기간만큼 캐시로 저장한다. 
  • Id 400 을 가진 학생을 최초 조회하였다. 
  • 응답에 87ms 소요되었다. 

 

  • Redis 서버 조회 결과, 응답 값이 캐시에 잘 저장된 것을 볼 수 있다. 
  • 우리가 의도한 바대로라면, 재 요청을 했을 시 응답시간이 감소해야한다. 

  • 응답 시간이 18ms 까지 현저하게 감소한 것을 확인할 수 있었고, RDB 로의 I/O 요청 (SQL쿼리) 역시 날아가지 않았다. 

 

  • TTL 을 30초로 설정해 두었기 때문에, 30초 뒤에는 Redis 서버에서 데이터가 삭제된다. 
  • 그리고 다시 API 요청을 하면, RDB 에 데이터를 요청해서 불러올 것이기 때문에, 시간이 더 오래 걸릴 것이다.

 

# Cache 된 30초 사이에 데이터가 변경되었다면?

  • Get 요청을 통해 Cache 한 다음, Put 요청을 통해 데이터를 수정하고, 다시 Get 요청을 해본다. 

  • 당연히 Spring 애플리케이션은 Redis 에 id 400 에 대한 Student 데이터가 존재하는 한 (30초) 변경 전 데이터를 응답할 것이다. 
  • 따라서 변경 트리거가 발생할 API 에는, 기존 캐시 데이터가 삭제되도록 @CacheEvict 어노테이션을 설정해준다.

 

  • 수정요청을 보내면 캐시가 사라지는 지 확인해보자
  • 잘 될것임.

 

 


# Null key returned for cache operation 에러

위에 글 작성할 때 실행했던 데모 앱에서는 잘 됐었는데, 배포중인 서버에 연결하려고 하니 발생한 에러였다.

 

그러니까 @Cacheable 어노테이션의 key 값에 null 이 들어간다는 것이었다. 

 

파라미터의 n번째를 뜻하게 #p0, #p1 등으로 수정하니 잘 해결되었다. 

 

캐싱을 하니 응답속도가 확실히 빨라졌다!