본문 바로가기
Spring/SpringBoot

[SpringBoot] Redis 사용하기 (4) - Redis Cache Manager 사용하기

by J4J 2024. 5. 15.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 redis 사용하기 마지막인 redis cache manager 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[SpringBoot] Redis 사용하기 (1) - Redis란 무엇인가?

[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기

[SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기

 

 

반응형

 

 

Redis Cache Manager란 ?

 

redis cache manager는 spring에서 캐싱을 적용하기 위해 제공해주고 있는 cache manager의 저장소를 redis로 변경하여 동일한 방식으로 캐싱 관리를 제공해 주는 모듈입니다.

 

redis cache manager에 대해 더 알아보기 위해 이전에 작성했던 redis repository, redis template과 함께 비교해 보겠습니다.

 

 

 

redis cache manager를 이용하여 redis에 데이터를 관리하는 방법은 어노테이션을 활용합니다.

 

redis repository는 entity와 repository를 이용하여 redis 데이터를 관리했고, redis template은 template을 기반으로 redis 데이터를 관리했었습니다.

 

그러다 보니 redis에 데이터 관리하는 특정 행위가 메서드 내부 비즈니스 로직에 담겨있었습니다.

 

 

 

하지만 redis cache manager는 메서드에 어노테이션을 추가하여 데이터를 관리하기 때문에 메서드 내부에 캐싱 처리와 관련 기능은 담겨 있지 않습니다.

 

대신에 메서드의 return 값을 활용합니다.

 

어노테이션을 통해 redis의 key, id 등의 정보를 설정해 둔다면 해당 정보와 return 값을 매핑하여 redis에 저장하는 방식입니다.

 

 

 

그래서 redis cache manager는 메서드의 return 값을 활용해야 되기 때문에 redis repository, redis template 보다 사용성 측면에서 더 자유롭지 못할 수도 있습니다.

 

그러나 어노테이션을 통한 간단한 설정을 통해 redis를 활용하기 때문에 더 편리하게 사용해 볼 수도 있습니다.

 

 

 

 

Redis Cache Manager 어노테이션

 

redis cache manager에서 사용할 수 있는 어노테이션 종류는 3가지가 있습니다.

 

 

 

[ 1. @Cacheable ]

 

@Cacheable 어노테이션이 적용되어 있는 메서드를 호출하게 될 경우 redis에 저장되어 있는 데이터가 있다면 캐싱된 값을 바로 반환합니다.

 

하지만 redis에 저장되어 있는 데이터가 없다면 메서드 내부 비즈니스 로직 처리 후 return 값을 반환하며 redis에 캐싱 처리를 하는 방식입니다.

 

일반적으로 @Cacheable은 데이터 조회하는 목적으로 많이 사용됩니다.

 

 

 

@Cacheable를 사용하는 방식은 다음과 같이 구성될 수 있습니다.

 

// 일반 사용 방법
@Cacheable(value = "user")
public List<User> getUsers() {}

// key 설정 방법
@Cacheable(value = "user", key = "#id")
public User getUser(String id) {}

// composite key 설정 방법
@Cacheable(value = "user", key = "{#id-#name}")
public User getUser(String id, String name) {}

// 객체를 parameter로 전달 받을 때 key 설정 방법
@Cacheable(value = "user", key = "#parameter.id")
public User getUser(Parameter parameter) {}

 

 

 

속성 값에 대해 설명드리겠습니다.

 

먼저 value는 cache 이름을 설정하는 값으로 redis에 데이터를 저장할 때 prefix로 활용됩니다.

 

그리고 value는 cacheNames 속성으로 대체하여 사용해도 무방합니다.

 

 

 

다음으로 key는 redis의 key 값을 의미합니다.

 

parameter의 이름에 "#" 연산자를 활용하여 key 값으로 활용할 수 있습니다.

 

{} 형태를 활용하여 복합키로 설정할 수도 있고, parameter로 자료형이 아닌 객체로 전달받아도 설정을 할 수 있습니다.

 

 

 

 

[ 2. @CachePut ]

 

@CachePut은 @Cacheable과 유사하게 메서드 내부 비즈니스 로직 처리 후 return 값을 반환하며 redis에 캐싱 처리를 하는 방식입니다.

 

하지만 @Cacheable과 다른 점은 @CachePut에 설정된 정보가 redis에 저장되어 있다고 하더라도 저장된 값을 활용하지 않습니다.

 

그래서 일반적으로 @CachePut은 데이터 적재 목적으로 많이 사용됩니다.

 

 

 

@CachePut의 사용 방식은 @Cacheable과 동일합니다.

 

// 일반 사용 방법
@CachePut(value = "user")
public List<User> saveUsers() {}

// key 설정 방법
@CachePut(value = "user", key = "#id")
public User saveUser(String id) {}

// composite key 설정 방법
@CachePut(value = "user", key = "{#id-#name}")
public User saveUser(String id, String name) {}

// 객체를 parameter로 전달 받을 때 key 설정 방법
@CachePut(value = "user", key = "#parameter.id")
public User saveUser(Parameter parameter) {}

 

 

 

 

[ 3. @CacheEvict ]

 

위의 어노테이션들이 redis에 데이터를 저장하는 것과 관련이 있었다면 @CacheEvict는 redis에 저장된 정보를 삭제하는 역할을 수행합니다.

 

그래서 다른 어노테이션들과 달리 return 값이 존재하지 않아도 됩니다.

 

 

 

@CacheEvic 또한 다른 어노테이션과 마찬가지로 사용하는 방식은 동일합니다.

 

// 일반 사용 방법
@CacheEvict(value = "user")
public void deleteUsers() {}

// key 설정 방법
@CacheEvict(value = "user", key = "#id")
public void deleteUser(String id) {}

// composite key 설정 방법
@CacheEvict(value = "user", key = "{#id-#name}")
public void deleteUser(String id, String name) {}

// 객체를 parameter로 전달 받을 때 key 설정 방법
@CacheEvict(value = "user", key = "#parameter.id")
public void deleteUser(Parameter parameter) {}

 

 

 

 

Redis Cache Manager 사용 환경 설정

 

이번에는 redis cache manager를 활용할 수 있는 환경 설정을 해보겠습니다.

 

예시는 redis repository, redis template과 동일하게 사용자 정보를 redis에 저장하는 코드로 작성해 보겠습니다.

 

참고 사항으로 이전 글들과 달리 이번에는 jpa를 활용해 보겠습니다.

 

 

 

[ 1. dependency 추가 ]

 

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

 

 

 

[ 2. redis properties 추가 ]

 

spring:
  data:
    redis:
      host: localhost # redis host 입력
      port: 6379 # redis port 입력

 

 

 

[ 3. redis config 클래스 추가 ]

 

package com.jforj.rediscachemanager.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {
    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(
                new RedisStandaloneConfiguration(
                        redisProperties.getHost(),
                        redisProperties.getPort()
                )
        );
    }

    private RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration
                .defaultCacheConfig() // default cache config 설정 적용
                .disableCachingNullValues() // null 값에 대한 캐싱 불허
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // key serialize 설정
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // value serialize 설정
    }

    private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        return redisCacheConfigurationMap;
    }

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration())
                .withInitialCacheConfigurations(redisCacheConfigurationMap())
                .build();
    }
}

 

 

 

redis cache configuration 설정 정보를 확인하면 key와 value에 대한 serialize 설정이 되어 있습니다.

 

serialize에 대한 설정은 이전 글인 [SpringBoot] Redis 사용하기 (3) - Redis Template 사용하기에 정리를 해놨기 때문에 관련 정보를 모르시는 분들은 참고 부탁드립니다.

 

 

 

 

[ 4. user entity 및 jpa repository 추가 ]

 

// user entity
package com.jforj.rediscachemanager.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;

@Builder
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
@Table(name = "user")
public class User {
    @Id
    private String id;
    private String name;
}


// user jpa repository
package com.jforj.rediscachemanager.repository;

import com.jforj.rediscachemanager.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserJpaRepository extends JpaRepository<User, String> {
}

 

 

 

[ 5. user repository 추가 ]

 

package com.jforj.rediscachemanager.repository;

import com.jforj.rediscachemanager.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final UserJpaRepository userJpaRepository;

    @CachePut(value = "user", key = "#id")
    public User create(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        return userJpaRepository.save(user);
    }

    @Cacheable(value = "user")
    public List<User> selectAll() {
        return userJpaRepository.findAll();
    }

    @CacheEvict(value = "user", key = "#id")
    public void delete(String id) {
        userJpaRepository.deleteById(id);
    }
}

 

 

 

 

위에서 작성한 코드들이 올바르게 동작되는지 테스트해 보겠습니다.

 

다음과 같이 repository에 대한 테스트 코드를 작성한 뒤 각 테스트들을 동작시켜 보면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.jforj.rediscachemanager.repository;

import com.jforj.rediscachemanager.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void create() {
        userRepository.create("first id", "first name");
        userRepository.create("second id", "second name");
    }

    @Test
    void selectAll() {
        List<User> users = userRepository.selectAll();
        System.out.println(users);
    }

    @Test
    void delete() {
        userRepository.delete("first id");
    }
}

 

create test

 

create test 후 redis key, value 정보

 

캐싱 전 selectAll test

 

selectAll test 후 redis key, value 정보

 

캐싱 후 selectAll test

 

delete test

 

delete test 후 redis key 정보

 

 

 

 

Redis Key 전역 Prefix 설정

 

redis cache manager를 사용할 경우 설정 값을 변경하여 모든 캐싱 데이터에 대해 전역 prefix 설정을 할 수 있습니다.

 

해당 설정은 만약 여러 API 서비스에서 동일한 redis 서버를 바라보고 있는 경우 도움 될 수 있습니다.

 

 

 

전역 prefix 설정하는 방법은 다음과 같이 redis config 정보를 변경해 주시면 됩니다.

 

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    private RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration
                .defaultCacheConfig() // default cache config 설정 적용
                .disableCachingNullValues() // null 값에 대한 캐싱 불허
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // key serialize 설정
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // value serialize 설정
                .prefixCacheNameWith("jforj::"); // 전역 prefix 설정
                // .computePrefixWith(CacheKeyPrefix.prefixed("jforj::")) // 다음 코드로 전역 prefix 설정 대체 가능
    }
    
    ...
    
}

 

 

 

그리고 데이터를 생성하는 테스트 코드를 다시 실행해 보면 다음과 같이 redis에 저장되는 것을 볼 수 있습니다.

 

전역 prefix 설정 후 create test

 

전역 prefix 설정 후 redis key 정보

 

 

 

 

하지만 만약 value (= cacheNames) 마다 서로 다른 prefix를 설정하고 싶을 수 있습니다.

 

그런 경우에는 다음과 같이 설정할 수 있습니다.

 

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put("user", redisCacheConfiguration().prefixCacheNameWith("jforj-user::")); // user cacheNames에만 prefix 설정
        return redisCacheConfigurationMap;
    }

    ...
    
}

 

 

 

 

유효 시간 설정

 

TTL 설정은 전역 prefix 설정한 것과 동일한 방식으로 적용해 볼 수 있습니다.

 

먼저 전역으로 유효 시간 설정 방법은 다음과 같습니다.

 

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    private RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration
                .defaultCacheConfig() // default cache config 설정 적용
                .disableCachingNullValues() // null 값에 대한 캐싱 불허
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // key serialize 설정
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // value serialize 설정
                .entryTtl(Duration.ofMinutes(60)); // default cache ttl 설정 (60분)
    }

    ...
    
}

 

 

 

그리고 value (= cacheNames) 마다 서로 다른 TTL 설정은 다음과 같이 해볼 수 있습니다.

 

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

    ...
    
    private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put("user", redisCacheConfiguration().entryTtl(Duration.ofMinutes(10))); // user cacheNames에만 ttl 설정 (10분)
        return redisCacheConfigurationMap;
    }
    
    ...
    
}

 

 

 

 

동일 클래스 내부 메서드 캐싱 이슈

 

redis cache manager를 사용할 경우 동일 클래스 내부 메서드 캐싱 이슈에 대해 알고 계셔야 합니다.

 

예를 들어 위의 예제 코드에서 redis repository 코드를 다음과 같이 변경하여 동일 클래스 내부 메서드를 통해 @CachePut 처리가 이루어지도록 해보겠습니다.

 

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final UserJpaRepository userJpaRepository;

    public User create(String id, String name) {
        return createCache(id, name);
    }

    @CachePut(value = "user", key = "#id")
    public User createCache(String id, String name) {
        User user = User.builder()
                .id(id)
                .name(name)
                .build();

        return userJpaRepository.save(user);
    }
    
    ...
    
}

 

 

 

그리고 테스트를 실행해 보면 다음과 같이 redis에 데이터가 저장되지 않는 것을 확인할 수 있습니다.

 

내부 메서드 설정 후 create test

 

내부 메서드 설정 후 redis key 정보

 

 

 

 

위의 문제는 spring cache manager가 기존에 가지고 있던 문제입니다.

 

spring cache manager는 어노테이션 기반 AOP를 활용하여 캐싱 처리를 수행하고 있는데 AOP가 기본적으로 클래스 내부에서 내부에 있는 메서드를 호출할 때 올바르게 동작되지 않는 부분이 있습니다.

 

AOP의 이슈로 인해 spring cache manager가 영향을 받고 redis cache manager까지 영향을 주고 있습니다.

 

그래서 redis cache manager를 활용하여 캐싱 처리를 하시는 분들은 내부 메서드 호출하는 곳에 캐싱을 적용하는 것은 주의하셔야 합니다.

 

 

 

 

 

 

 

 

 

이상으로 redis 사용하기 마지막인 redis cache manager 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글