ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Data Redis 설정(1) - RedisTemplate 설정
    Spring Boot 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 저장소에서 데이터 읽기가 불편할 수 있다. 
    반응형
Designed by Tistory.