스프링부트

Spring Data Redis - cache 기능 사용하기

알쓸개잡 2024. 6. 2.

Spring Cache에서 Redis를 활용하는 방법에는 여러 가지가 있다. Spring Boot와 Spring Data Redis를 사용하여 Redis를 캐시로 설정하는 방법에 대해서 정리해 본다.

 

먼저 Spring Cache의 주요 어노테이션에 대해서 다음 포스팅을 참고하면 도움이 될 것이다.

2024.05.20 - [스프링부트] - Spring Cache 주요 어노테이션

 

Spring Data Redis 관련 포스팅은 다음 포스팅을 참고하기 바란다.

2024.05.12 - [스프링부트] - Spring Data Redis - Redis Repository 사용

2024.04.29 - [스프링부트] - Spring Data Redis - RedisTemplate의 HashOperations

2024.04.25 - [스프링부트] - Spring Data Redis - RedisTemplate 트랜잭션

2024.04.24 - [스프링부트] - Spring Data Redis - RedisTemplate의 ValueOperations

2024.03.24 - [스프링부트] - Spring Data Redis - Auto Configuration을 이용한 Redis 연결 설정

 

의존성 추가

spring cache와 redis를 사용하기 위한 디펜던시는 다음과 같다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

 

 

Spring Cache관련 설정

spring cache를 사용하기 위한 application.yml의 설정은 다음과 같다.

spring:
  data:
    redis:
    ...
    redis 관련 설정
    ...
  cache:
    type: redis
    redis:
      cache-null-values: false
      key-prefix: "myCache::"
      use-key-prefix: true
      time-to-live: 1m
      enable-statistics: false

 

spring.cache.type: 캐시 타입을 지정한다. Redis를 캐시로 사용할 경우 'redis'로 지정한다.

spring.cache.redis.time-to-live: 캐시 항목의 만료 시간을 설정한다. Duration 타입으로 지정한다.

spring.cache.redis.cache-null-values: null 값을 캐시에 저장 허용할지 여부를 설정한다. 기본값은 true다.

spring.cache.redis.key-prefix: 캐시 키의 접두사를 설정한다. 기본값은 빈 문자열이다.

spring.cache.redis.use-key-prefix: 키 접두사를 사용할지 여부를 설정한다. 기본값은 true다.

 

Spring Cache AutoConfiguration

spring cache의 auto configuration은 spring-boot-autoconfigure 디펜던시에 정의되어 있다.

org.springframework.boot.autoconfigure.cache 패키지에 spring cache 관련 auto configuration을 위한 클래스들이 정의되어 있다.

@AutoConfiguration(
    after = {CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class}
)
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
    value = {CacheManager.class},
    name = {"cacheResolver"}
)
@EnableConfigurationProperties({CacheProperties.class})
@Import({CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})

org.springframework.boot.autoconfigure.redis 패키지에 있는 CacheAutoConfiguration 클래스를 보면 설정 항목을 로딩하는 클래스는 CacheProperties.class인 것을 알 수 있다.

 

CacheProperties.class에는 다음과 같은 어노테이션이 정의되어 있다.

@ConfigurationProperties(
    prefix = "spring.cache"
)

spring.cache 아래의 설정 항목들을 로딩한다는 것을 알 수 있다.

CacheProperties.class 에 CacheType 멤버 변수가 정의되어 있는데 이를 통해 Spring Cache가 지원하는 저장소를 확인할 수 있다.

public enum CacheType {
    GENERIC,
    JCACHE,
    HAZELCAST,
    COUCHBASE,
    INFINISPAN,
    REDIS,
    CACHE2K,
    CAFFEINE,
    SIMPLE,
    NONE;

    private CacheType() {
    }
}

CAFFEINE, HAZELCAST 등 cache 역할을 하는 여러 저장소를 지원하는 것을 알 수 있다.

 

Spring cache의 저장소로 redis를 사용하기 위해서는 RedisAutoConfiguration이 우선 수행되어 Redis 연결을 위한 ConnectionFactory, RedisTemplate과 같은 필요한 빈들이 등록된 상태여야 한다.

 

org.springframework.boot.autoconfigure.cache 패키지에 보면 RedisCacheConfiguration 클래스를 확인할 수 있다.

RedisCacheConfiguration에서는 spring cache 저장소로 redis를 사용하기 위한 RedisCacheManager 인스턴스를 빈으로 등록한다.

RedisCacheManager는 RedisCacheManager 인스턴스를 생성할 때 다음과 같은 빈들을 주입받는다.

  • CacheProperties : spring cache 관련 설정 참조 정보를 담고 있는 클래스. spring.cache.redis 설정항목을 로딩하여 적용한다.
  • CacheManagerCustomizers: RedisCacheManager를 생성 후 사용자가 직접 변경 사항을 적용하기 위한 클래스.
  • RedisCacheManagerBuilderCustomizer: RedisCacheManager를 생성하기 위한 builder에 사용자가 직접 변경 사항을 적용하기 위한 클래스.

 

Spring CacheManagerBuilderCustomizer

Spring boot autoconfiguration에 의해서 생성되는 CacheManager는 기본적으로 JdkSerializationRedisSerializer를 사용한다.

JdkSerializationRedisSerializer를 사용하게 되면 Redis에 저장되는 데이터의 형태를 읽기가 쉽지 않고 객체를 저장하는 경우에는

Serializable 인터페이스를 확장해야 하는 번거로움이 있어서 권장되지 않는 직렬화 방식이다.

이와 관련하여 직렬화 방식을 변경하고자 하면 CacheManagerBuilderCustomizer에 우리가 원하는 직렬화 방식을 설정해 주면 된다.

CacheManagerBuilderCustomizer 빈을 등록하는 방법은 다음과 같다.

//기본적으로 자동 구성되는 RedisCacheManager 에 RedisCacheConfiguration 만 커스텀하게 변경할 수 있다.
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer( CacheProperties cacheProperties ) {
    //serializer 를 GenericJackson2JsonRedisSerializer 로 변경.
    return builder -> {
        RedisCacheConfiguration cacheConfiguration = builder.cacheDefaults()
                .serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer( RedisSerializer.string() ) )
                .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer( RedisSerializer.json() ) );

        builder.cacheDefaults( cacheConfiguration );
    };
}

만약 application.yml 에 spring.cache.redis 관련 설정을 하지 않았다면 RedisCacheManagerBuilderCustomizer를 통해서

spring.cache.redis 관련 설정을 직접 할 수 있다.

 

RedisCacheManagerBuilderCustomizer 클래스는 FunctionalInterface로 정의되어 있다.

@FunctionalInterface
public interface RedisCacheManagerBuilderCustomizer {
    void customize(RedisCacheManager.RedisCacheManagerBuilder builder);
}

customize 메서드의 파라미터로 현재 생성 중인 RedisCacheManagerBuilder가 전달된다.

우리는 인자로 전달되는 RedisCacheManagerBuilder인스턴스를 가지고 설정만 하면 되는 것이다.

 

샘플 코드

샘플 코드를 통해서 redis 캐시가 제대로 동작하는지 확인해 보자.

application.yml

spring:
  ...
  cache:
    type: redis
    redis:
      cache-null-values: false
      key-prefix: "myCache::"
      use-key-prefix: true
      time-to-live: 1m
      enable-statistics: false

key-prefix로 cache로 저장될 때 myCache:: 를 키 앞에 붙이도록 설정하였다.

key-prefix와 use-key-prefix가 설정된 경우 저장되는 키 형식은
{key-prefix}{cache-name}::{식별자} 형태가 되겠다.
use-key-prefix를 false로 지정하게 되면 {key-prefix}와 {cache-name} 은 키에 포함되지 않고 {식별자}로만 키가 생성된다.

 

Person 클래스 정의

@NoArgsConstructor
@Setter
public class Persistent implements Persistable<UUID> {
    @Id
    protected UUID id;

    public Persistent(UUID id) {
        this.id = id;
    }

    @Override
    public UUID getId() {
        return id;
    }

    @Override
    @JsonIgnore
    public boolean isNew() {
        return id == null;
    }
}
@Getter
@Setter
@NoArgsConstructor
public class Person extends Persistent {
    private String personName;
    private Integer personAge;
    private String personNation;
    private String comment;

    @Builder
    public Person( UUID id, 
                   String personName, 
                   Integer personAge, 
                   String personNation, 
                   String comment ) {
        super(id);
        this.personName = personName;
        this.personAge = personAge;
        this.personNation = personNation;
        this.comment = comment;
    }
}

 

PersonService 클래스 정의

@Service
@Slf4j
@CacheConfig( cacheNames = "persons" )
public class PersonService {

    @Cacheable( key = "#personId" )
    public Person findPerson( UUID personId ) {
        log.info( "findPerson called, personId={}", personId );
        //아래 로직이 실행된다는 것은 persons 캐시에 personId 식별자를 가진 엔트리가 존재하지 않는 것을 의미.
        //보통은 DB에서 조회하는 로직이 들어가지만 편의상 새로운 Person 인스턴스를 생성하여 리턴한다.
        return Person.builder()
                .id( personId )
                .personName( "cached-person" )
                .personNation( "korea" )
                .personAge( 100 )
                .comment( "cached data" )
                .build();
    }

    @CachePut( key = "#person.id" )
    public Person savePerson( Person person ) {
        log.info( "savePerson called, person={}", person );
        //보통은 DB에 업데이트하는 로직이 들어가지만 편의상 person 인스턴스를 그대로 리턴한다.
        //리턴되는 인스턴스는 cache 에 업데이트 된다.
        return person;
    }

    @CachePut( key = "#person.id" )
    public Person updatePerson( Person person ) {
        log.info( "updatePerson called, person={}", person );
        //보통은 DB에 업데이트하는 로직이 들어가지만 편의상 person 인스턴스를 그대로 리턴한다.
        //리턴되는 인스턴스는 cache 에 업데이트 된다.
        return person;
    }

    @CacheEvict( key = "#personId" )
    public void deletePerson( UUID personId ) {
        log.info( "deletePerson called, personId={}", personId );
        //보통은 DB에서 데이터를 삭제하는 로직이 들어가지만 편의상 로그만 생성한다.
        //메서드 실행이 완료되면 personId 에 해당하는 엔트리가 캐시에서 삭제된다.
    }
}

편의상 spring cache의 redis 동작여부를 판단하기 위함이니 따로 DB를 사용하진 않았다.

cache명은 'persons' 다.

Spring Cache를 위해서 사용되는 어노테이션에 대해서는 

2024.05.20 - [스프링부트] - Spring Cache 주요 어노테이션  포스팅을 참고하기 바란다.

 

Controller 클래스

@RestController
@RequestMapping("/persons")
@RequiredArgsConstructor
public class PersonController {
    private final PersonService personService;

    @GetMapping(path = "/{personId}", produces = MediaType.APPLICATION_JSON_VALUE )
    public ResponseEntity<Person> getPerson( @PathVariable UUID personId ) {
        Person person = personService.findPerson( personId );
        return ResponseEntity.ok( person );
    }

    @PostMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE )
    public ResponseEntity<Person> createPerson( @RequestBody Person person ) {
        Person savedPerson = personService.savePerson( person );
        return ResponseEntity.ok( savedPerson );
    }

    @PutMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Person> updatePerson( @RequestBody Person person ) {
        Person updatedPerson = personService.updatePerson( person );
        return ResponseEntity.ok( updatedPerson );
    }

    @DeleteMapping(path = "/{personId}")
    public ResponseEntity<Void> deletePerson( @PathVariable UUID personId ) {
        personService.deletePerson( personId );
        return ResponseEntity.noContent().build();
    }
}

 

 

API 호출

API를 호출했을 때 redis에 저장되는 값을 확인해 보자. redis에 저장된 JSON 문자열을 pretty 하게 보도록 jq를 설치하였다.

참고하기 바란다.

 

저장

request
POST http://localhost:9090/persons/00000000-0000-0000-0000-000000000001

request body
{
    "id": "00000000-0000-0000-0000-000000000001",
    "personName": "saved-person",
    "personAge": 200,
    "personNation": "korea",
    "comment": "saved data"
}

위와 같이 저장 API를 호출 했을 때 redis에 저장되는 데이터는 다음과 같다.

 

 ~/ redis-cli get "myCache::persons::00000000-0000-0000-0000-000000000001" | jq .
{
  "@class": "com.example.spring.redis.model.Person",
  "id": "00000000-0000-0000-0000-000000000001",
  "personName": "saved-person",
  "personAge": 200,
  "personNation": "korea",
  "comment": "saved data"
}

 

조회

request
GET http://localhost:9090/persons/00000000-0000-0000-0000-000000000001

response body
{
    "id": "00000000-0000-0000-0000-000000000001",
    "personName": "saved-person",
    "personAge": 200,
    "personNation": "korea",
    "comment": "saved data"
}

person id '00000000-0000-0000-0000-000000000001'를 조회하는 API를 호출 했을 때 PersonService 클래스의

findPerson 메서드에서 기록하는 'findPerson called, personId={}'와 같은 로그는 생성되지 않는다. 이를 통해서 findPerson 메서드의 로직이 실행되지 않고 redis cache에서 데이터를 가져왔음을 알 수 있다.

 

수정

request
PUT http://localhost:9090/persons
request body
{
    "id": "00000000-0000-0000-0000-000000000001",
    "personName": "modified-person",
    "personAge": 100,
    "personNation": "korea",
    "comment": "modified data"
}

 

redis cache는 다음과 같이 변경됨을 알 수 있다.

 ~/ redis-cli get "myCache::persons::00000000-0000-0000-0000-000000000001" | jq .
{
  "@class": "com.example.spring.redis.model.Person",
  "id": "00000000-0000-0000-0000-000000000001",
  "personName": "modified-person",
  "personAge": 100,
  "personNation": "korea",
  "comment": "modified data"
}

 

삭제

request
DELETE http://localhost:9090/persons/00000000-0000-0000-0000-000000000001

 

redis 결과

127.0.0.1:6379> scan 0
1) "0"
2) (empty array)

redis에 저장된 person 데이터는 삭제되었다.

 

지금까지 spring cache 저장소로 redis를 사용하는 방법에 대해서 정리해 보았다.

대부분의 설정은 application.yml(properties) 파일의 spring.cache.redis.~ 에서 설정 하되 @Configuration 클래스에서RedisCacheManagerBuilderCustomizer 빈을 정의하여 serializer를 변경하여 사용하면 spring cache 저장소로 redis를 간단히 적용 가능하다.

 

샘플 코드는 GITLAB 링크에서 확인할 수 있다.

 

끝.

댓글

💲 추천 글