Post

(Java) RedisClient

spring-boot-starter-data-redis-reactive

의존성

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

AutoConfiguration

1
2
3
4
5
@ConfigurationProperties(prefix = "spring.redis")    // spring.redis 이하 properties들을 불러온다.
public class RedisProperties {
	private String host = "localhost";
	private int port = 6370;
	...
  • RedisAutoConfiguration.java에서는 다음 두 가지 Bean을 미리 만들어서 제공하고 있음
    • StringRedisTemplate
      • key, value, hashKey, hashValue의 Serializer로 RedisSerializer.string() 사용
    • RedisTemplate<Object, Object>
      • key, value, hashKey, hashValue의 defaultSerializer로 JdkSerializationRedisSerializer 사용
      • 결국 Java Obj로 변환한다는 건데… 이는 여러모로 단점이 있음. ([Effective Java] 12장 직렬화 참고)

=> Jackson을 Serializer로 사용하는 RestTemplate을 따로 정의해서 사용하는 것이 좋아 보인다.

Auto Configuration Disable?

  • RedisReactiveAutoConfiguration::class는 exclude 가능.
  • 반면 RedisAutoConfiguration::class는 exclude하면 factory까지 생성이 안돼서 exclude 불가.

Spring Data Redis API

  • low-level API는 RedisConnection 계열. binary 통신 제공
    • RedisClusterConnection
  • 이를 고수준으로 추상화한 API는 RedisTemplate 계열. 객체 수준으로 다룰 수 있음
    • ReactiveRedisTemplate
    • RedisTemplate은 클래스이고, 보통 주고 받을 때는 RedisOperations라는 인터페이스를 사용한다
    • RedisOperations.opsForValue() 요런 식으로 있다고 보면 됨
  • (아래) Template = connection + serializer 정도로 생각하면 된다.
1
2
3
4
5
6
@Bean
   fun redisOperations(factory: ReactiveRedisConnectionFactory): ReactiveRedisOperations<String, Coffee> {
   val builder = RedisSerializationContext.newSerializationContext<String, Coffee>(StringRedisSerializer())
   val context = builder.value(Jackson2JsonRedisSerializer(Coffee::class.java)).build()
   return ReactiveRedisTemplate(factory, context)
}

예제

  • 크게 CrudRepository 사용하는 방법과 RedisTemplate 사용하는 방법으로 나뉨
  • (1:1) : RedisTemplateFactory - 메타 데이터나 설정 데이터 같은, 클래스는 존재하지만 개념적으로 해당 객체가 유니크한 경우
  • (1:n) : @RedisHash & Repository - Student 클래스 같이, 여러 인스턴스가 존재할 수 있으며 각각이 고유한 id 별로 식별되어야 하는 경우
  • reactive가 필요하거나, json serialize/deserialize 가 필요한 경우 : RedisTemplateFactory
  • @RedisHash & Repository는 redis 타입 hash, RedisTemplateFactory는 json이므로 redis 타입 string

CrudRepository 예제 (like DB)

class 이름 변경, 패키지 변경 시 매우 주의!! redis 해시에 _class 필드로 해당 클래스 이름(+패키지 경로)가 들어가기 때문!!
마찬가지로 필드명 변경도 주의해야 한다.

1
2
3
4
@Repository
interface AsyncWithdrawItemRepository: CrudRepository<AsyncWithdrawItem, String>
// 이런 구조의 장점은, mocking 하기 쉽다는 것이다.
class MockAsyncWithdrawItemRepository: AsyncWithdrawItemRepository {...}

RedisTemplate 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Configuration  
class RedisConfig { // 방법 1, 3 공통  
    @Bean  
    fun reactiveRedisTemplate(factory: ReactiveRedisConnectionFactory, objectMapper: ObjectMapper): ReactiveRedisTemplate<String, Any> {  
  
        val stringSerializer = StringRedisSerializer()  
        val jsonSerializer = Jackson2JsonRedisSerializer(Any::class.java)  
        jsonSerializer.setObjectMapper(objectMapper)  
        val context = RedisSerializationContext.newSerializationContext<String, Any>()  
            .key(stringSerializer)  
            .value(jsonSerializer)  
            .hashKey(stringSerializer)  
            .hashValue(jsonSerializer)  
            .build()  
        return ReactiveRedisTemplate(factory, context)  
    }  
    // StringTemplate은 안해도 된다. RedisReactiveAutoConfiguration.java 에서 설정하고 있음.  
    @Bean  
    fun reactiveStringRedisTemplate(  
        reactiveRedisConnectionFactory: ReactiveRedisConnectionFactory  
    ): ReactiveStringRedisTemplate {  
  
        return ReactiveStringRedisTemplate(reactiveRedisConnectionFactory)  
    }  
    /**  
     * 방법 1. 이런 식으로 data class 마다 Bean을 직접 만든다. 완전 비추.  
     */    
     @Bean  
    fun coffeeRedisOperations(factory: ReactiveRedisConnectionFactory): ReactiveRedisOperations<String, Coffee> {  
  
        val builder = RedisSerializationContext.newSerializationContext<String, Coffee>(StringRedisSerializer())  
        val context = builder  
            .value(Jackson2JsonRedisSerializer(Coffee::class.java))  
            .build()  
  
        return ReactiveRedisTemplate(factory, context)  
    }}  
  
/**  
 * 방법 2. 각 서비스 클래스에서 getRedisTemplate을 호출해서 해당 타입의 RedisTemplate을 반환 받는 방법  
 */  
@Component  
class RedisTemplateFactory(  
    val connectionFactory: ReactiveRedisConnectionFactory, val objectMapper: ObjectMapper  
) {  
  
    final inline fun <reified V> getRedisTemplate(): ReactiveRedisTemplate<String, V> {  
        val stringSerializer = StringRedisSerializer()  
        val jsonSerializer = Jackson2JsonRedisSerializer(V::class.java)  
        jsonSerializer.setObjectMapper(objectMapper)  
        val context = RedisSerializationContext.newSerializationContext<String, V>()  
            .key(stringSerializer)  
            .value(jsonSerializer)  
            .hashKey(stringSerializer)  
            .hashValue(jsonSerializer)  
            .build()  
        return ReactiveRedisTemplate(connectionFactory, context)  
    }}  
  
/**  
 * 방법 3. RestTemplate과 비슷하게 사용하기 위해서...? 내부적으로는 Any로 처리하고 set, get해서 내려줄 때는 Casting해서 내려주는 방법.  
 * 단점은 set, get 뿐만 아니라 다양한 메서드(keys, expire 등)를 모두 wrapping 해주어야 한다는 점.  
 */
@Component  
class RedisComponent(val redisTemplate: ReactiveRedisTemplate<String, Any>, val objectMapper: ObjectMapper) {  
  
    fun <V> set(key: String, value: V): Mono<Boolean> {  
        return redisTemplate.opsForValue().set(key, value as Any)  
    }  
    /**  
     * 방법 3-1.     * opsForValue().get()으로 Object를 받고 바깥에서 ObjectMapper.convertValue()를 사용해 V로 변환  
     */  
    fun <V> get(key: String, clazz: Class<V>): Mono<V> {  
        return redisTemplate.opsForValue().get(key)  
            .map { obj -> objectMapper.convertValue(obj, clazz) }  
    }  
  
    /**  
     * 방법 3-2.     * 아예 RedisTemplate 자체를 저수준부터 구현. (RestTemplate 처럼 메서드 기반으로.)  
     */
 }

방법2가 제일 괜찮아 보임.

This post is licensed under CC BY 4.0 by the author.