본문 바로가기

Spring

SpringBoot + Redis 를 이용한 글로벌 캐시

 

 

Cache?

요청에 대한 응답을 저장해두었다가, 그 값을 반환하는것

 

일반적으로 영구적인 데이터 저장을 하는경우, RDBMS 와 같은 디스크에 데이터를 쓰는 저장소를 사용합니다.

보통 캐시는 디스크 저장소보다 훨씬 빠른 Redis 나 Memcached 와 같은 메모리 기반의 저장소에 저장하여 성능향상을 도모합니다.

 

When Cache?

결과 값이 잘 변하지 않는, 일반적으로 같은 응답을 주는 경우가 많은 경우에 용이합니다.

데이터 변경이 잦은 경우, 오히려 실제 데이터와 캐시까지 두번 변경해주어야 하므로 서버에는 더 부담을 안겨주게 됩니다.

 

서버에 들어온 요청중에서 캐시에 저장된 데이터를 조회한 비율을 캐시 히트율 이라고 하는데, 

이 캐시 히트율이 높을수록 캐싱이 성능에 좋은 영향을 주고있다는 것을 의미합니다.

 

Type

어플리케이션 레벨에서는 일반적으로 로컬 캐시 와 글로벌 캐시 (= 분산 캐시) 로 구분합니다.

 

로컬 캐시는 어플리케이션 메모리에 데이터를 캐싱합니다.

별도의 네트워크 I/O 가 필요하지 않기 때문에 성능상으로는 이점이 가장 크지만, 저장 공간 자체가 작고 캐시한 데이터 자체가 어플리케이션의 성능에 직접적으로 영향을 줍니다. 또한 여러 대의 분산 서버환경에서는 서버간 캐싱된 데이터의 불일치성 문제도 존재합니다.

 

글로벌 캐시는 별도의 캐시 저장소를 두어 여러개의 서버에서 공유할 수 있게 하는것을 말합니다. 로컬 캐시에 비해 큰 저장용량과, 서버간 캐싱 데이터를 일치시킬수 있다는 장점이 있습니다. 

 

Policy

캐시는 혹시 모를 실 데이터와의 갭과,  메모리상에 저장하는 특성 때문에 무한정 저장하지 않고 주기적으로 삭제 혹은 교체를 해줍니다.

실제 구현 코드를 보기전에 자주 사용하는 캐시 교체 정책(알고리즘) 을 몇가지 간략히 소개해보겠습니다.

 

FIFO : First In First Out 의 약어입니다. 가장 오래된 캐시부터 교체합니다. 자주 히트되는 캐시가 삭제될 우려가 있습니다.

LFU: Least Frequently Used 의 약어입니다. 가장 히트된 횟수가 적은 페이지를 교체합니다. 많이 히트가 될 가능성이 있는 최근에 적재된 페이지가 교체될 우려가 있습니다.

LRU: Least Recentrly Used 의 약어입니다. 가장 오래전에 히트된 페이지를 교체합니다. 불러올 때마다 timestamp 를 기록해야하여 오버헤드가 크다는 단점이 있습니다. 하지만 가장 캐시의 목적에 맞기 때문에 가장 많이 사용되는 방식입니다.

 


Spring 과 Redis 를 이용한 글로벌 캐시 구현

캐시의 개념에 대해 간략히 살펴보았습니다. 

Spring 은 어노테이션으로 PSA 된 캐싱 인터페이스를 제공하여 사용자가 원하는대로 구현체를 바꾸어 적용할 수 있도록 지원합니다.

 

그 중에서 본 포스팅에서는 Redis 를 이용한 캐싱 방법을 소개해보겠습니다.

 

우선 redis 를 사용하기 위해 spring-data-redis 의존성을 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

application.yaml

spring:
  cache:
    type: redis  # 캐시 구현체를 지정합니다. Ecache, Caffeine, Redis 등 많은 구현체를 지원합니다.
    
  redis:
    host: redisHost 
    password: redisPassword
    port: redisPort

 

 

설정 프로퍼티들을 모두 작성해주었다면 바로 사용이 가능하지만, 상세한 설정을 위해 설정파일을 하나 만들어보도록 하겠습니다.

 

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) // 0 {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 1
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(ApiResponse.class))) // 2
                .disableCachingNullValues() // 3
                .entryTtl(Duration.ofMinutes(3L)); // 4

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }
}

 

 

0 : yaml 에서 설정했던 레디스의 정보를 바탕으로 Spring Boot 가 자동으로 redisConnectionFactory 를 생성해 빈으로 등록했습니다. RedisCacheManager 생성을 위해 주입 받습니다.

 

1 : serializeKeysWith 은 레디스 키를 생성할 때 직렬화 하는 클래스를 설정합니다. 

2. serializeValuesWith 은 레디스 키에 저장할 값을 직렬화 하는 클래스를 지정합니다. 여러 구현체가 있지만, 제가 적용한 서비스에는 Response DTO 에 jackson 관련 어노테이션이 많이 적용되어 있고, ApiResponse<T> 클래스로 반환 값이 고정되어 있어 Jackson2JsonRedisSerializer 를 구현체로 지정하였습니다.

3. disabledCachingNullValues 는 Null 값에 대한 캐시를 저장하지 않게하는 설정입니다.

4. entryTtl 은 캐시의 유효기간을 뜻합니다. 위 코드에서는 3분이 지나면 캐시는 삭제됩니다. 설정 소개를 위해 작성하였지만, 레디스의 maxmemory 를 지정하고 maxmemory-policy 로 캐시 삭제 알고리즘을 지정해주는편이 더 좋아보입니다.

 

이외에도 캐시별로 entryTtl 을 설정하거나 키의 Prefix 를 지정하는 설정등이 더 있습니다.

 

 

이제 레디스를 구현체로하는 캐시를 사용할 준비가 완료되었으니 Spring 에서 추상화한 캐시 적용방법을 살펴보겠습니다.

 

@Cacheable

    @Cacheable(value = "category-goods", key = "#categoryId", unless = "#result.response.data.size() < 100")
    @GetMapping(value = "category/{categoryId}/goods")
    public ApiResponse<List<Goods>> getGoods(@PathVariable String categoryId) {
        return ResponseUtils.ok(goodsAdaptor.findAll(categoryId));
    }

@Cacheable 은 캐시 저장소에 해당하는 키의 캐시가 존재하지 않고, 지정한 조건에 부합하면 캐시를 저장합니다.

반대로 해당하는 키의 캐시가 존재한다면, 로직을 수행하지 않고 캐시 저장소에서 캐시를 꺼내 반환합니다.

 

value : 캐시의 이름 입니다. Redis 에는  

key : 해당 캐시 이름으로 저장될 캐시의 식별 키 값입니다. value::key 형식으로 저장됩니다.

unless: 서버의 비즈니스 로직이 수행된 뒤 검증되며, 해당 검증 로직을 초과해야 캐시에 최종적으로 저장됩니다. 수행 결괏값에 의한 캐시 저장조건을 지정하고 싶을때 사용합니다.

이외에 condition 옵션은 서버의 비즈니스 로직이 수행되기 전에 검증됩니다. 그러므로 결괏 값이 아닌 사용자 입력값에 따른 캐시 저장조건을 명시할 수 있습니다.

 

Spring의 캐시 인터페이스의 값들은 모두 SpEL 을 이용하여 작성할 수 있습니다.

 

@CacheEvict

@CacheEvict 는 지정한 캐시와 키값에 해당하는 캐시를 삭제합니다. 

    @Cacheable(value = "category-goods", key = "#request.categoryId")
    @PostMapping(value = "/goods")
    public ApiResponse<Goods> addGoods(@RequestBody PostRequest.Create request) {
        return ResponseUtils.ok(goodsFacade.save(request));
    }

캐시를 저장할 때, 캐싱된 데이터가 변경되고 실시간으로 반영되어야 하는 곳에는 CacheEvict 를 지정해주어야 합니다.

 

이외에도 캐시를 조회할 때는 사용하지 않고 캐시 저장만하는 @CachePut, 여러가지 캐싱 어노테이션을 한번에 적용하는 @Caching 등의 어노테이션도 제공합니다.