Post

redis - java persistence

[redis - infra, cli ]

spring-boot-starter-data-redis-reactive

연동

build.gradle.kts

1
2
3
4
5
6
implementation
(
"org.springframework.boot:spring-boot-starter-data-redis-reactive"
)

AutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 불가.
예제

더보기

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@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가 제일 괜찮아 보임.

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
7
@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)
   }

Redis Cluster

Redis도 DB처럼 Mapper 사용해서 CRUD하기?

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

마찬가지로 필드명 변경도 주의해야 한다.

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

@RedisHash & Repository 사용하는 방법 VS 방법2-RedisTemplateFactory 비교
  • (1:1)
    RedisTemplateFactory - 메타 데이터나 설정 데이터 같은, 클래스는 존재하지만 개념적으로 해당 객체가 유니크한 경우
  • (1:n)
    @RedisHash & Repository - Student 클래스 같이, 여러 인스턴스가 존재할 수 있으며 각각이 고유한 id 별로 식별되어야 하는 경우
  • reactive가 필요하거나, json serialize/deserialize 가 필요한 경우 : RedisTemplateFactory
  • @RedisHash & Repository는 redis 타입 hash, RedisTemplateFactory는 json이므로 redis 타입 string
This post is licensed under CC BY 4.0 by the author.