Spring Boot

Spring Data Redis 설정(1) - RedisTemplate 설정

sendkite 2025. 4. 27. 16:57
  • Redis 설정 관련하여 아키텍처, 직렬화/역직렬화에 대한 기록

1. Redis 아키텍처

1) StandAlone

  • 1대의 Redis 서버

2) Redis Replication (master/slave 구조)

  • master (읽기/쓰기), slave (master 데이터 복사 - 읽기전용)로 구성
  • master → slave 복사시간이 1-2초 소요되는 단점이라 잘 쓰이지 않는 구조이다.
  • redis cluster를 잘 운영하지 못할때 사용한다.

master, slave 구조

3) Redis Sentinel

  • master/replica 구조에서 primary 장애 시 자동 장애 조치(auto failover)를 수행 구조
  • sentinel 노드 과반 수 이상이 master 장애를 감지하면 slave 하나를 master로 승격
    • 지속적으로 모티터링/감시만 하는 node(sentinel)를 따로 관리해줘야한다.
      • client가 올바른 노드를 찾을 수 있도록 도움
      • 최소 3개 이상의 노드를 권장
      • 감시 역할은 Redis와 동일한 노드에 구성해도 되고, 별로로 구성해도 됨
  • 일반적으로 단일 master로 구성함,
    • master 하나가 모든 쓰기 작업을 처리해야하는 단점 있음
    • 대규모 쓰기 작업시 병목 발생
    • Slave에 복제되는 Latency 증가

Sentinel

4) Redis Cluster

  • 최소 3대의 마스터 노드 필요, 각 노드 당 최소 Slave node 필요함
  • 데이터는 16384개 슬롯으로 나뉘어 특정 Master에 할당
  • 모든 데이터는 master 단위로 sharding되고, slave 단위로 복제됨 (즉 슬롯으로 sharding함)
  • 설정과 관리가 매우 복잡하고 특정 작업(ex - 다중키 작업)은 일부 제한이 있을 수 있음

Cluster 구조

 

application.yml에서 어떤 Connection mode인지에 따라서 다른 환경 변수를 사용할 수 있다.

Spring Data Redis에서 제공하는 환경변수

 

3. Redis 설정

  • OMS는 본격적으로 Redis를 활용하는 구조가 아니기 때문에 StandAlone으로 설정을 진행한다.

Redis 접속 설정

  • Lettue, Jedis 선택지가 있으나 Lettue가 성능상 더 좋다.
@Configuration
class RedisConfig {
    @Bean
    fun connectionFactory(): LettuceConnectionFactory {
        val redisConfig = RedisStandaloneConfiguration("localhost", 6379)

        val clientConfig =
            LettuceClientConfiguration
                .builder()
                .commandTimeout(Duration.ofSeconds(30)) // 명령 실행 타임아웃
                .clientOptions(
                    ClientOptions
                        .builder()
                        .autoReconnect(true)  // 자동 재연결
                        .socketOptions(
                            SocketOptions
                                .builder()
                                .connectTimeout(Duration.ofSeconds(5)) // 네트워크 연결 타임아웃
                                .build(),
                        ).disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) // // 연결 끊겼을 때 처리 방식
                        .build(),
                )
                .build()

        return LettuceConnectionFactory(redisConfig, clientConfig)
    }
 }
  • disconnectedBehavior 옵션 다른 선택지
    • DEFAULT: 요청을 큐에 쌓았다가 나중에 처리 (❗️주의: 메모리 소비 증가 가능)
    • REJECT_COMMANDS: 연결이 없으면 명령을 즉시 거부 (→ 예외 발생)
    • ACCEPT_COMMANDS: 연결이 끊겨도 명령을 계속 받아서 큐에 저장

RedisTemplate 설정

  • Key-Value 구조가 <String, String> 이라면 StringRedisTemplate를 사용한다.
  • 라이브러리 자체에서 최적화 설정이 되어 있기 때문이다.
 @Bean("stringRedisTemplate")
 fun stringRedisTemplate(
     @Qualifier("connectionFactory") connectionFactory: LettuceConnectionFactory,
 ): StringRedisTemplate = StringRedisTemplate(connectionFactory)

라이브러리 내부 직렬화 최적화

 

1) RedisTemplate<String, Object> - 가장 기본적인 사용 방법

  • 아래와 같이 String은 StringRedisSerializer로, Object는 GenericJackson2JsonRedisSerializer로 객체를 JSON 형태로 직렬화하여 저장한다.
   @Bean
   public RedisTemplate<String, Object> redisTemplate() {
       RedisTemplate<String, Object> template = new RedisTemplate<>();
       template.setConnectionFactory(redisConnectionFactory());

       // 직렬화 설정
       template.setKeySerializer(new StringRedisSerializer());
       template.setHashKeySerializer(new StringRedisSerializer());
       template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
       template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

       return template;
   }
  • 이 방식의 단점
    • Json 객체를 그대로 바이트 배열로 변환하여 저장하므로, 저장 공간이 많이 사용됨
    • 설정에 따라 직렬화 시에 클래스 정보가 포함되어 여러 문제가 생길 수 있음
    • ObjectMapper를 사용하여 직렬화를 시도함
    • 참고 - Spring Redis 직렬화의 한계 / https://mangkyu.tistory.com/402
  • Redis에 저장 데이터가 크다면 직접 직렬화를 구현하여 압축 저장하는 방법도 있다.

2) RedisTemplate<String, ByteArray>로 구현하고 ObjectMapper를 커스텀하는 방식

@Bean("objectRedisTemplate")
fun objectRedisTemplate(
    @Qualifier("connectionFactory") connectionFactory: LettuceConnectionFactory,
): RedisTemplate<String, ByteArray> {
    val redisTemplate = RedisTemplate<String, ByteArray>().apply {
        keySerializer = StringRedisSerializer()
        hashKeySerializer = StringRedisSerializer()
        setConnectionFactory(connectionFactory)
    }
    return redisTemplate
}

@Bean("redisObjectMapper")
fun redisObjectMapper(): ObjectMapper {
    return SmileMapper().registerModules(JavaTimeModule())
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
        .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .setSerializationInclusion(JsonInclude.Include.ALWAYS)
        .disable(MapperFeature.USE_ANNOTATIONS)
}
  • smile로 저장함으로 JSON 문자열 대비 2/3 절약된 크기 저장
  • 전반적인 고성능
  • 단점으로는 ObjecMapper 커스텀 구현 및 redis 저장소에서 데이터 읽기가 불편할 수 있다. 
반응형