앞선 게시글에서 캐싱 전략과 전략 선정 기준에 대해서 정리했었다.

실제 진행하고 있는 프로젝트에선 어떻게 활용할지 시작해보겠다.

프로젝트에 Product의 상품 엔티티가 존재하며 상품 정보를 조회할 때 캐싱 기능을 사용하고자 한다.

상품 정보 조회는 Global Caching 전략이 유리할 것이라고 판단했다.

그 이유는 상품 정보는 상대적으로 데이터의 양이 많아 중복으로 데이터를 저장하게 되는 경우 자원의 낭비가 심하며 데이터의 일관성이 중요한 정보라고 생각했기 때문이다.

지난 게시글에서 Global Caching은 서버와 분리된 별도의 저장소에 데이터를 보관한다고 했었다.

우리가 아는 Redis는 대표적인 Global Cache일 뿐만 아니라 Spring에서 Cache 저장소로 Redis를 지원하고 있기 때문에 Redis를 이용해 캐싱을 적용해보도록 하겠다.

개발 환경


SpringBoot 3.0.5

Java 17

0. Dependency 설정


Redis 사용을 위해 라이브러리를 추가해준다.

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


1. Redis 연결 정보 설정


application.yml 혹은 application.properties에 아래와 같이 redis 연결 정보를 입력한다.

spring:
  data:
    redis:
      cache:
        host: localhost
        port: 6379


2. Cache 적용을 위한 CacheConfig 등록


redis 연결 정보까지 입력했기에 redis와 SpringBoot는 연결된 상태이다.

하지만, redis로 캐싱 기능을 사용하겠다는 것은 아직 설정된 상태가 아니기에 지금부터 그 과정을 순서대로 시작해보겠다.


2-1. redis 연결 정보 변수로 가져오기

@Configuration
public class CacheConfig {

    @Value("${spring.data.redis.cache.host}")
    private String host;

    @Value("${spring.data.redis.cache.port}")
    private int port;
}

우선 클래스 위에 @Configuration 어노테이션을 붙여 설정을 위한 클래스임을 명시한다.

그리고, 앞서 application.yml 파일에서 작성했던 redis 연결 정보를 변수로 가져온다.


2-2. 캐싱 기능 연결을 위한 Bean


@Bean(name = "redisCacheConnectionFactory")
public RedisConnectionFactory redisCacheConnectionFactory() {
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setPort(port);
    redisStandaloneConfiguration.setHostName(host);
    return new LettuceConnectionFactory(redisStandaloneConfiguration);
}

다음으로, 캐싱 기능 연결을 위한 Bean을 생성해주어야 한다.

2-1에서 작성한 redis 연결 정보를 RedisConnectionFactory 객체에 입력하여 캐싱을 사용할 수 있도록 한다.

그리고 이 Bean은 뒤에 있을 캐싱 설정을 할 redisCacheManager()에서 사용할 것이다.

코드를 살펴보면,

RedisStandaloneConfiguration 객체는 redis를 연결하기 위한 설정 정보를 가지고 있는 클래스이기 때문에 앞서 변수로 가져온 host와 port를 넣어주기만 하면된다.

다음으로, Java의 Redis Client는 크게 JedisLettuce가 있다.

Lettuce가 몇 배 이상의 성능을 자랑하며 SpringBoot 2.x 부터는 Lettuce가 기본 설정이기 때문에 RedisConnectionFactory를 구현한 LettuceConnectionFactory로 반환해주면 된다.

마지막으로 메서드 위에 @Bean(name='...') 을 지정한 이유는 프로젝트에서 redis를 관심사에 따라서 분리하였고, 동일한 타입의 RedisConnectionFactory Bean이 여러 개 존재할 것이기 때문이다.


2-3. 캐시에서 redis 사용하기 위한 Bean

// 설정 객체 default 설정 -- key/value를 어떻게 직렬화해서 redis에 저장할지를 정의함
private RedisCacheConfiguration defaultCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .disableCachingNullValues()
            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofDays(1L));
}

@Bean
public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) {
    return RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(connectionFactory)    // connection 적용
            .cacheDefaults(defaultCacheConfiguration())  //  캐시 설정 적용
            .build();
}

다음 단계는 캐싱 기능으로 redis를 사용하기 위한 설정이다.

defaultCacheConfiguration() 메서드부터 살펴보자.

RedisCacheConfiguration 클래스는 설정 객체를 생성할 때 defaultCacheConfig()로 여러 조건들의 default를 설정할 수 있다.

redis는 key/value로 데이터를 저장하기 때문에 어떻게 직렬화해서 redis에 저장할 것인지를 여기서 정의해야 한다.

먼저, serializeKeysWith()를 통해 key값을 직렬화 시키는 방법으로 StringRedisSerializer클래스를 넣어주었는데 이 클래스는 String을 UTF-8 기준으로 byte[]로 변환시키고 그 반대의 과정도 해준다.

다음으로, serializeValuesWith()를 통해 value값을 직렬화 시키는 방법을 정의할 것이다.

여기선 조금 다르게 GenericJackson2JsonRedisSerializer클래스를 넣어주었는데 그 이유는 value값의 경우, Object 형태이기 때문에 Object와 JSON 간의 직렬화/역직렬화 과정을 진행해줄 수 있는 클래스가 필요하기 때문이다.

추가적으로, GenericJackson2JsonRedisSerializer 클래스를 넣어주면 객체의 클래스 지정 없이 모든 Class Type을 JSON 형태로 저장할 수 있다.


key/value를 어떻게 직렬화해서 redis에 저장할지에 대한 설정이 끝났다면 CacheManager를 통해 그 동안 설정했던 connection 설정, 직렬화/역직렬화 설정들을 반환해주면 된다.

fromConnectionFactory()엔 앞서 만들었던 redisCacheConnectionFactory Bean을 @Qualifier어노테이션을 통해 넣어주면 된다.

@Qualifier 옵션은 동일한 타입의 빈이 여러 개가 존재하는 경우, 지정된 조건과 일치하는 Bean을 주입해준다.

cacheDefaultsdefaultCacheConfiguration() 메서드에서 정의한 설정 값을 넣어주면 된다.


3. @Cacheable


모든 설정을 마치고 프로젝트에서 상품 단건 조회 서비스 메서드에 아래와 같이 @Cacheable(value = "products", key ="#id")를 붙여주면 된다.

여기서 value값은 필수로 지정해줘야 하고, key값은 선택적으로 적용할 수 있다.

value값은 캐시 아이디, key값은 캐시 이름이 같을 때, 사용되는 구분 값이라고 생각하면 된다.

key값을 비워두면 default로, 적용될 메서드의 파라미터를 캐시 값으로 구성하기 때문에 보통 key값은 적용될 메서드의 파라미터가 여러 개일 때 사용한다.

또한, 만약 파라미터가 존재하지 않을경우 key값은 0으로 처리되는데 이렇기 때문에 보통 key값을 명시적으로 사용하는 것을 권장한다.

@Transactional(readOnly = true)
@Cacheable(value = "products", key ="#id")
public ProductInfo getProductInfo(Long id) {
    return productRepository.findById(id)
            .orElseThrow(() -> new ShoeKreamException(ErrorCode.PRODUCT_NOT_FOUND))
            .toProductInfo();
}


4. 결과 확인


image

image

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Java 8 date/time type `java.time.LocalDate` not supported by 
default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling at ...

java.time.LocalDate 에러가 났다…

분명 난 직렬화/역직렬화 과정을 설정해주었는데 LocalDate타입의 releaseDate 필드가 제대로 변환되지 않는다고 한다.

에러를 해결하는 과정은 꽤나 고달팠다…

다음 게시물에서 해보도록 하겠다.

References

Leave a comment