프로그래밍 노트/SPRING BOOT

[Spring Boot] RedisCluster 연동 & RedisTemplate 적용 (feat. AutoConfiguration)

깡냉쓰 2024. 10. 22. 11:34
728x90
반응형

Redis 의존성 추가

spring-boot-starter-data-redis 의존성 추가

implementation("org.springframework.boot:spring-boot-starter-data-redis")

SpringBoot에서 외부 라이브러리 버전을 관리하기에, spring-boot-starter-data-redis 를 추가하면 해당 SpringBoot버전에서 관리하는 외부라이브러리가 자동으로 추가된다.

나는 현재 SpringBoot 3.2.4 버전을 사용하여 lettuce 6.3.2.RELEASE 버전이 자동 추가된 것을 볼 수 있다.

관리되는 버전은 https://docs.spring.io/spring-boot/docs/3.2.4/reference/html/dependency-versions.html 에서 확인 가능하다.

RedisAutoConfiguration

Autoconfigure 에서 Spring Data's Redis를 support 해준다.

RedisAutoConfiguration.class 를 살펴보면 Bean이 자동 설정되는 조건들을 볼 수 있다.

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(RedisConnectionDetails.class)
    PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties) {
        return new PropertiesRedisConnectionDetails(properties);
    }

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}
  • RestTemplate은 RedisConnectionFactory Bean이 존재하면 자동 등록된다.
  • RedisConnectionFactory는 RedisAutoConfiguration이 @Import하고 있는 LettuceConnectionConfiguration.class 에서 자동 생성해주지만 우리는 RedisCluster관련한 설정이 추가적으로 필요하므로 재정의하도록 한다.
  • LettuceConnectionFactory(RedisConnectionFactory 구현체)를 custom bean으로 등록하면 RedisTemplate은 자동등록 되므로 해당 설정만 Configuration에 추가해준다.

LettuceConnectionFactory Bean 정의(feat. RedisCluster)

RedisCluster 연동시, fail-over를 위한 설정들이 몇 가지 필요하다. 설정 관련해서는 아래 글들을 참고했다.

@Configuration
class RedisConfig {
    @Bean
    fun redisConnectionFactory(
        @Value("\${redis.node-list}") redisNodeList: String,
        @Value("\${redis.connection-timeout-millis}") connectionTimeoutMillis: Long,
        @Value("\${redis.operation-timeout-millis}") operationTimeoutMillis: Long,
        @Value("\${redis.request-queue-size}") requestQueueSize: Int,
    ): RedisConnectionFactory {
        val nodeList: List<String> = redisNodeList.split(",").toList()

        // (1) Socket Option
        val socketOptions =
            SocketOptions
                .builder()
                .connectTimeout(Duration.ofMillis(connectionTimeoutMillis))
                .keepAlive(true)
                .build()

        // (2) Cluster topology refresh
        val clusterTopologyRefreshOptions =
            ClusterTopologyRefreshOptions
                .builder()
                .dynamicRefreshSources(true) // 모든 Redis 노드로부터 topology 정보 획득
                .enableAllAdaptiveRefreshTriggers() // Redis 클러스터 모든 이벤트(MOVE, ACK)등에 대해 topology 갱신
                .enablePeriodicRefresh(Duration.ofSeconds(30)) // 토폴로지 갱신 텀
                .refreshTriggersReconnectAttempts(1) // 한 번이라도 실패가 일어나면 토폴로지 갱신
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) // 토폴로지 업데이터가 일어난 경우 30초 이내 재갱신 x
                .build()

        // (3) Cluster client
        val clusterClientOptions =
            ClusterClientOptions
                .builder()
                .pingBeforeActivateConnection(true) // 커넥션을 사용하기 위하여 PING 명령어를 사용하여 검증 default : true
                .autoReconnect(true) // 자동 재접속 옵션 default : true
                .socketOptions(socketOptions)
                .topologyRefreshOptions(clusterTopologyRefreshOptions)
                .maxRedirects(nodeList.size)
                .requestQueueSize(requestQueueSize)
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
                .build()

        // (4) Lettuce Client
        val lettuceClientConfig =
            LettuceClientConfiguration
                .builder()
                .commandTimeout(Duration.ofMillis(operationTimeoutMillis))
                .clientOptions(clusterClientOptions)
                .build()

        val clusterConfig =
            RedisClusterConfiguration(nodeList).apply {
                this.maxRedirects = nodeList.size
            }

        // (5) LettuceConnectionFactory
        return LettuceConnectionFactory(clusterConfig, lettuceClientConfig).apply {
            this.validateConnection = false // 연결 검증 비활성화 default : false
        }
    }
}

RedisTemplate Bean이 자동 등록됬는지 테스트

@SpringBootTest(
    classes = [RedisConfig::class],
)
@ImportAutoConfiguration(RedisAutoConfiguration::class)
class RedisConfigTest(
    private val applicationContext: ApplicationContext,
) : FreeSpec({
        extensions(SpringExtension)

        "RedisTemplate이 AutoConfiguration에 의해 자동 등록 된다." {
            applicationContext.getBean("redisTemplate") shouldNotBe null
            applicationContext.getBean("stringRedisTemplate") shouldNotBe null
        }
    })

RedisTemplate이 정상 동작하는지 테스트

@SpringBootTest(
    classes = [RedisConfig::class],
)
@ImportAutoConfiguration(RedisAutoConfiguration::class)
class RedisTemplateTest(
    private val redisTemplate: RedisTemplate<Any, Any>,
) : FreeSpec({
        extensions(SpringExtension)

        beforeTest {
            redisTemplate.delete("key")
        }

        "redisTemplate Test" - {
            "get() - key 미존재시 null 반환" {
                redisTemplate.opsForValue().get("key") shouldBe null
            }

            "delete() - key 존재시 true 반환" {
                redisTemplate.opsForValue().set("key", "value")

                redisTemplate.delete("key") shouldBe true
            }

            "delete() - key 미존재시 false 반환" {
                redisTemplate.delete("key") shouldBe false
            }

            "setIfAbsent() - key 존재시 false(이미 존재) 반환" {
                redisTemplate.opsForValue().set("key", "value")
                val result = redisTemplate.opsForValue().setIfAbsent("key", "newValue")

                result shouldBe false
                redisTemplate.opsForValue().get("key") shouldBe "value"
            }

            "setIfAbsent() - key 미존재시 true 반환" {
                val result = redisTemplate.opsForValue().setIfAbsent("key", "value")

                result shouldBe true
                redisTemplate.opsForValue().get("key") shouldBe "value"
            }
        }
    })
728x90
반응형