목차

더보기
  1. 스프링 데이터 레디스란
    1. 학습 순서
      1. 스프링 학습
      2. NoSQL과 Key Value 저장소 학습
  2. why spring data redis
  3. Redis Support
    1. Redis Support High-level View
    2. 레디스와 연결
    3. RedisConnection and RedisConnection Factory
    4. Working with Objects through RedisTemplate
    5. Serializers
  4. Redis Repositories
    1. 사용 예시
    2. 객체 매핑 기초 - 추후 다시보자
      1. 권장 사용 방법
      2. 코틀린 지원
        1. 코틀린 객체 생성
        2. 코틀린 데이터 클래스의 프로퍼티 채우기
    3. Object to Hash 매핑
    4. keyspaces
    5. 보조 인덱스
      1. 간단한 인덱스
      2. geospatial 인덱스
    6. Query by Example
    7. Time to Live
    8. Persisting References
    9. Persisting Partial Updates
    10. Queris and Query Method
    11. Redis Repositories Running on a Cluster
    12. CDI Integration

 

1. 스프링 데이터 레디스란

스프링 데이터 레디스란, key-value 저장소를 사용한 솔루션 개발에 핵심 스프링 개념을 적용한 프로젝트이다. 메세지 전송과 수신을 위해 추상화된 템플릿을 제공한다.

 

1.1 학습 순서

1.1.1 스프링 학습

스프링 데이터를 스프링의 핵심 기능을 사용하기 때문에 최소한 IoC에 익숙해야 한다.

 

레디스 지원의 핵셤 기능은, 스프링 컨테이너의 도움 없이 직접 호출이 가능하다는 점이다. 이는 Spring 컨테이너의 다른 서비스 없이 독립형으로 사용할 수 있는 JdbcTemplate과 매우 유사하다.

 

repository 지원과 같은 스프링 데이터 레디스의 모든 기능을 활용하기 위해서는, 스프링을 사용하도록 라이브러리 일부를 구성해야 한다.

 

1.1.2 NoSQL과 key value 저장소 학습

 

2. why spring data redis

스프링 데이터 레디스를 사용하면 레디스 key-value 저장소를 사용한 스프링 프로젝트를 쉽게 구축할 수 있다. 스프링 인프라 지원을 통해, 저장소와 상호작용하기 위한 보일러 플레이트 코드와 불필요한 작업들을 제거할 수 있다.

 

3. Redis support

스프링 데이터가 지원하는 Key-value 저장소 중에 하나는 레디스이다. 스프링 레디스 데이터는 스프링 애플리케이션에서 레디스로의 액세스와 간단한 설정을 제공한다. 저장소 접근을 위해 저수준과 고수준 추상화를 모두 제공한다. 따라서 사용자는 인프라 우려로부터 자유로울 수 있다.

 

3.1 Redis Support High-level View

redis support는 여러 구성 요소를 제공한다. 대부분의 작업에서는 고수준의 추상화와 지원 서비스를 사용하는게 최선의 선택이다. 물론, 필요하다면 레디스와 직접 통신할 수 있는 저수준 커넥션(또는 네이티브 라이브러리)을 얻을 수 있다.

 

3.2 레디스와 연결

레디스와 스프링을 사용하기 위해서는 먼저 IoC 컨테이너를 통해 저장소와 연결해야 한다. 이를 자바 커넥터나 바인딩이 필요하다. 어떤 라이브러리를 선택하든 하나의 spring data redis api를 사용하면 된다. : redis.connection 하위의 RedisConnection와 RedisConnectionFactory 인터페이스.

 

3.3 RedisConnection and RedisConnectionFactory

RedisConnection 가 제공하는 핵심 빌딩 블록을 통해 레디스와 통신할 수 있다. 또한, 기본 연결 라이브러리 예외를 스프링 dao 예외로 변환해 준다.

 

참고로 네이티브 라이브러리 api가 필요하다면 레디스 커넥션이 제공하는 getNativeConnection 메서드를 사용하면 된다. 그러면 통신에 사용되는 원 기반 객체를 반환받을 수 있다.

 

활성 RedisConnection 객체는 RedisConnectionFactory를 통해 생성된다. 또한 RedisConnectionFactory는 PersistenceExceptionTranslator로써 작동하기도 한다. 그러므로 @Repository와 AOP 사용을 통해 예외 자동 변환이 가능하다.

 

참고로 설정에 따라, 커넥션 팩토리는 새로운 커넥션을 반환하거나 기존 커넥션을 반환하게 할 수 있다.

 

RedisConnectionFactory로 작업하는 가장 쉬운 방법은, IoC 컨테이너를 통해 적절한 커넥터를 구성하고 이를 사용 클래스에 주입하는 것이다.

 

현재 모든 커넥터들이 레디스의 모든 기능을 제공하고 있지는 않다. 지원하지 않는 기능을 호출하면 UnsupportedOperationException 예외를 던진다. 대표적인 커넥터는 Jedis와 Lettuce가 있으며, 두 커넥터가 지원하는 기능은 해당 문서를 참고한다.

 

3.4 Working with Objects through RedisTemplate

대부분의 사용자들은 RedisTemplate과 해당 패키지인 org.springframework.data.redis.core 사용하려 할 것이다. 실제로, 해당 템플릿은 풍부한 기능을 제공하기 때문에 Redis 모듈의 중심 클래스이다.

RedisConnection은 이진값을 허용하고 반환하는 저수준 메서드를 제공한다. 반면에, 레디스 템플릿은 레디스 상호작용을 위한 고수준의 추상화를 제공한다. 직렬화 및 연결 관리를 처리해 주기 때문에 사용하는 이런 세부 사항을 다루지 않아도 된다. 또한 템플릿은 SetOperations나 BoundGeoOperations와 같이, 특정 유형이나 키에 대한 작업에 대해 풍부한 기능을 제공한다.

 

일단 템플릿을 구성해 두면, thread-safe 하면서 여러 인스턴스에서 재사용될 수 있다.

 

레디스 템플릿은 대부분의 작업에서, 자바 기반의 serializer를 사용한다. 즉, 템플릿에서 쓰거나 읽는 모든 객체가 자바를 통해 직렬화 및 역직렬화됨을 의미한다. 물론 템플릿에서 직렬화 메커니즘을 변경할 수 있다.

 

3.5 String-focused Convenience Classes

보통 레디스에 저장된 키와 값은 String인 경우가 많다. 따라서 레디스 모듈은 string에 특화된 레디스 커넥션(StringRedisConnection)과 레디스 템플릿(StringRedisTemplate)을 제공한다.

String 키를 바운딩하는 것 외에도, 템플릿과 커넥션은 StringRedisSerializer를 기반으로 사용한다. 이는 저장된 키와 값을 사람이 읽을 수 있음을 의미한다.(레디스와 코드에서 동일한 인코딩을 사용한다는 가정)

 

다른 스프링 템플릿과 마찬가지로, 레디스 템플릿과 스프링 레디스 템플릿 모두 RedisCallback 인터페이스를 통해 레디스와 직접 통신이 가능하다. 이 기능은 레디스 커넥션과 직접 통신하므로 완전한 제어를 제공한다.

public void useCallback() {

  redisTemplate.execute(new RedisCallback<Object>() {
    public Object doInRedis(RedisConnection connection) throws DataAccessException {
      Long size = connection.dbSize();
      // Can cast to StringRedisConnection if using a StringRedisTemplate
      ((StringRedisConnection)connection).set("key", "value");
    }
   });
}

3.6 Serializers

프레임워크 관점에서, 레디스에 저장되는 데이터는 오직 바이트뿐이다. 레디스는 다양한 타입을 제공하지만, 대부분의 경우 데이터가 표현되는 것보다는 저장되는 방식을 나타낸다. 사용자는 정보가 문자열이나 다른 객체로 변환되는지 여부를 결정할 수 있다.

 

org.springframework.data.redis.serializer 패키지에는 두 유형의 serializers가 있다.

  1. RedisSerializer를 기반으로 하는 양방향 serializers - 주로 byte[]로 직렬화한다.
  2. element writers(RedisElementWriter)와 element readers(RedisElementReader) - byteBuffer를 사용한다.

 

serializers은 아래와 같이 다양한 종류가 있다.

  • JdkSerializationRedisSerializer : RedisCache와 RedisTemplate의 기본 Serializer이다. 바이트 코드 그대로 보여진다.
  • StringRedisSerializer : String 타입만 저장할 수 있으며, 레디스에서도 문자열 그대로 보여진다.
  • Jackson2JsonRedisSerializer : object를 json 형태로 저장한다.

 

serializer 실습 코드

더보기

serializer 테스트 코드

@Autowired
    lateinit var redisTemplate: RedisTemplate<Any, Any>

    @BeforeEach
    fun init() {
        redisTemplate.keySerializer = StringRedisSerializer()
    }

    @Test
    fun contextLoads() {
    }

    @Test
    fun testJdkSerializationRedisSerializer() {
        // JdkSerializationRedisSerializer는 default다.
        val user: User = User("stella", 123, User.Level.GOLD)

        redisTemplate.opsForValue().set("key1", user)
    }

    @Test
    fun testStringRedisSerializer() {
        // JdkSerializationRedisSerializer는 default다.
        redisTemplate.valueSerializer = StringRedisSerializer()
        val user: User = User("stella", 123, User.Level.GOLD)

//        redisTemplate.opsForValue().set("key2", user) // user가 문자열이 아니라서 에러 발생
        redisTemplate.opsForValue().set("key2", "name : stella, id : 123, level : GOLD")
    }

    @Test
    fun testJackson2JsonRedisSerializer() {
        // JdkSerializationRedisSerializer는 default다.
        redisTemplate.valueSerializer = Jackson2JsonRedisSerializer(User::javaClass.javaClass)
        val user: User = User("stella", 123, User.Level.GOLD)

        redisTemplate.opsForValue().set("key3", user)
    }

 

테스트 결과(터미널)

  • JdkSerializationRedisSerializer(default)
  • StringRedisSerializer
  • testJackson2JsonRedisSerializer

 

tip

참고로 기본 Serializer인 JdkSerializationRedisSerializer를 그대로 사용하는 것은 지양하는 게 좋다. 애플리케이션의 역직렬화 단계에서 조작된 입력으로 인해 원하지 않는 코드가 실행될 수 있기 때문이다. 따라서 신뢰할 수 없는 환경에서는 직렬화를 사용하지 말고, JSON이나 다른 메세지 형식을 사용한다.

 

4. Redis Repositories

Redis Repository를 사용하면, 도메인 객체를 레디스 해시로 원활하게 전환 및 저장할 수 있다. 사용자 지정 매핑 전략을 적용할 수도 있고, 보조 인덱스도 사용할 수 있다.

 

참고로 Redis Repository는 레디스 서버 버전 2.8 이상이 필요하다. 그리고 트랜잭션과 함께 작동하지 않으므로, 트랙잭션 지원이 비활성화된 RedisTemplate을 사용해야 한다.

 

4.1 사용 예시

crud를 할 수 있는 Repository 인터페이스는 다음과 같다.

public interface PersonRepository extends CrudRepository<Person, String> {

}

스프링 데이터 레디스를 사용하면 아래와 같이 도메인 객체를 쉽게 해시로 변환할 수 있다.

@RedisHash("people")
public class Person {

  @Id String id;
  String firstname;
  String lastname;
  Address address;
}

식별자로 사용할 필드에 @Id를 붙여준다. 그러면 save를 할 때 id가 null가 새 id를 세팅해 주고, id 값이 세팅되어 있다면 해당 Id를 그대로 사용한다. 키는 people:5d67b7e1-8640-4475-beeb-c666fab4c0e5 형태를 띨 것이다.

 

4.2 객체 매핑 기초 - 추후 다시 보자

해당 섹션에서는 JPA와 같은 기본 데이터 저장소의 객체 매핑을 사용하지 않는 Spring Data 모듈에만 적용된다.

 

Spring Data 객체 매핑의 핵심 책임은 1) 도메인 객체 인스턴스를 생성하고, 2) 이를 저장소의 데이터 구조에 매핑하는 것이다. 즉, 다음의 두 가지 단계가 필요하다.

  1. 노출된 생성자를 바탕으로 인스턴스 생성
  2. 노출된 모든 속성을 구체화하기 위해 인스턴스를 채우기

 

4.2.1 권장 사용 방법

1. 불변 객체를 고수한다.

불변 객체는 생성자만으로 객체를 생성할 수 있다. 이러면 setter 메서드로 인해 도메인 객체가 흩어지는 것을 피할 수 있다. 필요한 경우에는 package protected 하게 제한을 걸어주는 게 좋다. 생성자 전용 구체화는 프로퍼티틑 채우는 속도가 최대 30%까지 빠르다.

 

2. all args constructor 제공

프로퍼티 채우는 과정을 스킵할 수 있기 때문에 성능이 좋다.

 

3. @PersistenceCreator를 피하기 위해, 생성자 오버로딩 대신 팩토리메서드를 사용한다.

최적의 성능을 위해 all args 생성자를 사용하면, 일반적으로 사용이 편하게 생성자를 오버로딩하는 경우가 많다.(ex. 식별자를 자동 생성하는 생성자) all args 생성자의 변형을 노출하기 위해, 정적 팩토리 메서드를 사용하는 것이 좋은 패턴이다.

 

4. 생성된 instantiator와 property accessor classes가 사용될 수 있도록, 적절한 제약조건을 준수해야 한다.

 

5. 식별자가 생성되기 위해서는, 필드에 final을 걸어주고 all args 생성자를 사용한다.(권장 방법) 또는, with… 메서드를 사용한다.

 

6. 상용구 코드에 롬복을 사용한다.

자바의 경우, lombok의 @AllArgsContructor로 반복적인 작업을 효과적으로 줄일 수 있다.

 

4.2.2 코틀린 지원

스프링 데이터는 코틀린으로의 객체 매핑을 지원한다.

 

4.2.2.1 코틀린 객체 생성

다음처럼 데이터 클래스로 선언한다.

data class Person(val id: String, val name: String)

다른 생성자를 추가하고 싶다면, 다음처럼 @PersistenceCreator와 사용하면 된다.

data class Person(var id: String, val name: String) {

    @PersistenceCreator
    constructor(id: String) : this(id, "unknown")
}

하지만 코틀린은 파라미터에 기본값을 줄 수 있기 때문에 아래처럼 사용하는 게 낫다.

data class Person(var id: String, val name: String = "unknown")

 

4.2.2.2 코틀린 데이터 클래스의 프로퍼티 채우기

다음처럼 데이터 클래스로 클래스를 선언하자.

data class Person(val id: String, val name: String)

이 클래스를 불변이다. 코틀린은 데이터 클래스에 copy 메서드를 자동으로 생성해 주기 때문에 새로운 인스턴스 생성이 가능하다.

 

4.3 object to hash 매핑

스프링 데이터 레디스에서는, 기본적으로 객체와 레더스 해시 간의 매핑을 byte[] 형식으로 지원한다. 해당 방식은 단일 값, 객체, 리스트 등의 타입과 상관없이 모두 flat 형태로 저장한다.

기본 매핑 룰

만약 객체나 리스트, 맵 등을 json 형태로 저장하고 싶다면, Converter를 커스텀하게 구현하면 된다.

 

4.4 Keyspaces

키스페이스로 Redis 해시 키의 prefix를 정의할 수 있다. 기본값으로 접두사로 getClass().getName()이 붙는다. 변경하고 싶다면 @RedisHash를 설정하거나 설정 파일에서 로직을 추가하면 된다. 참고로 @RedisHash방식이 우선순위가 높다.

 

설정 코드는 공식 문서를 참고한다.

 

4.5 보조 인덱스

보조 인덱스는 네이티브 레디스 구조 기반으로 조회 작업을 가능하게 하기 위해 사용된다. 값들은 저장될 때마다 인덱스에 매번 추가된다. 또한 객체가 제거되거나 만료될 때마다 인덱스에서 제거된다.

 

4.5.1 간단한 인덱스

실습 코드 

더보기

Person

@RedisHash("people")
data class Person(
    @Id
    var id: String?,
    @Indexed var firstname: String? = null,
    var lastname: String? = null,
    var address: Address? = null
)

data class Address(
    @Indexed val city: String? = null,
    val country: String? = null
)

 

 

인덱스 테스트 코드

@Test
fun basicTest() {
    val rand = Person("Junyoung", "Choi").apply {
        address = Address("sillim", "korea")
    }

    val saveRand = personRepository.save(rand)
    println(saveRand)

    val findRand = personRepository.findById(saveRand.id!!).get()

    println(findRand)

    println(personRepository.count())

//      println(personRepository.delete(rand))
}

 

 

인덱스 생성 결과

127.0.0.1:6379> keys *
1) "people:Junyoung:idx"
2) "people:address.city:sillim"
3) "people:Junyoung"
4) "people:firstname:Choi"
5) "people"

127.0.0.1:6379> smembers people
1) "Junyoung" // id값

127.0.0.1:6379> smembers people:Junyoung:idx
1) "people:address.city:sillim"
2) "people:firstname:Choi"

127.0.0.1:6379> smembers people:address.city:sillim
1) "Junyoung" // id값

127.0.0.1:6379> smembers people:firstname:Choi
1) "Junyoung" // id값

127.0.0.1:6379> hkeys people:Junyoung // 해시에 저장된 필드들
1) "_class"
2) "address.city"
3) "address.country"
4) "firstname"
5) "id"

 

 

필드에 @Indexed 없이, 아래와 같이 Configuration 파일에서 인덱스를 구성할 수 있다.

@Configuration
@EnableRedisRepositories
class RedisConfig {
    //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

    @Bean
    fun keyValueMappingContext(): RedisMappingContext {
        return RedisMappingContext(
            MappingConfiguration(MyIndexConfiguration(), KeyspaceConfiguration())
        )
    }

    class MyIndexConfiguration : IndexConfiguration() {
        override fun initialConfiguration(): Iterable<IndexDefinition> {
            return setOf<IndexDefinition>(SimpleIndexDefinition("people", "firstname"))
        }
    }
}

 

4.5.2 geospatial 인덱스

클래스에 위치 정보를 담고 있는 Point 필드가 있다고 가정해 보자. 해당 필드에 @GeoIndexed을 걸어주면, 위경도 기반(x, y)으로 객체 조회가 가능하다.

 

실습 코드

더보기

Person

@RedisHash("people")
data class Person(
    @Id
    var id: String?,
    var firstname: String? = null,
    var lastname: String? = null,
    var address: Address? = null
)

data class Address(
    val city: String? = null,
    @GeoIndexed val location: Point? = null,
    val country: String? = null
)

 

PersonRepository

interface PersonRepository : CrudRepository<Person, String> {
    fun findByAddressLocationNear(point: Point, distance: Distance): List<Person>
    fun findByAddressLocationWithin(circle: Circle): List<Person?>
}

 

geospatial 인덱스 테스트 코드

@Test
fun geospatialIndexTest() {
    val rand = Person("Junyoung", "Choi").apply {
        address = Address(location = Point(13.361389, 38.115556))
    }

    personRepository.save(rand)

    // GEORADIUS people:address:location 15.0 37.0 200.0 km 와 동일
    println(personRepository.findByAddressLocationNear(Point(15.0, 37.0), Distance(200.0, Metrics.KILOMETERS)))
}

 

Print 결과

[Person(id=Junyoung, firstname=Choi, lastname=null, address=Address(city=null, location=Point [x=13.361389, y=38.115556], country=null))]

 

redis-cli

127.0.0.1:6379> keys *
1) "people:address:location"
2) "people:Junyoung"
3) "people:Junyoung:idx"
4) "people"

127.0.0.1:6379> smembers people:Junyoung:idx
1) "people:address:location"

127.0.0.1:6379> GEORADIUS people:address:location 15.0 37.0 200.0 km
1) "Junyoung" // id값

4.6 Query by Example

Query by Example은 간단한 인터페이스를 가진 사용자 친화 쿼리 기술이다. 동적 쿼리 생성이 가능하며, 필드 이름이 포함된 쿼리를 작성하지 않아도 된다. 그저 예시로 삼을 객체와 매칭 조건을 파라미터로 넘겨주면, 조건에 맞는 결과를 얻을 수 있다.

 

4.7 Time to Live

레디스의 데이터 만료시간을 설정하기 위해서는 Time to Live 값을 세팅해 주면 된다. 만료 시간 세팅은 @RedisHash(timeToLive=…)이나 KeyspaceSettings로 할 수 있다. 또는 @TimeToLive 애노테이션을 사용하면, 숫자 프로퍼티나 메서드 단위로 더욱 유연하게 세팅이 가능하다.

 

참고로 레디스는 생성, 변경 등의 이벤트를 수신하는 기능을 지원한다. 만료 이벤트도 지원하며, RedisMessageListenerContainer를 통해 수신받을 수 있다.

 

4.8 Persisting References

참조 객체를 아래와 같이 id 로만 들고 있게 할 수 있다.

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

그러기 위해서는 대상 참조 객체에 @Reference를 붙여주고, 참조 객체는 별도로 저장한다.

 

4.9 Persisting Partial Updates

어떤 경우에는 레디스에 저장된 객체를 불러오지 않고 수정 작업을 처리하고 싶을 수 있다. 레디스의 부분 변경 기능을 활용하면, 객체 로딩 없이 수정과 삭제, 만료시간 업데이트 등의 작업을 수행할 수 있다.

 

4.10 Queries and Query Method

jpa와 마찬가지로, 메서드로 쿼리를 생성하는 기능을 제공한다. 참고로 대상 필드에 인덱스가 걸려있는지 체크하자.

public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByFirstname(String firstname);

  List<Person> findByFirstnameOrderByAgeDesc(String firstname); // 정렬 기능도 제공한다.

  List<Person> findByFirstname(String firstname, Sort sort); // 동적 정렬도 가능하다.
}

쿼리 메서드 기능만으로는 원하는 쿼리 생성에 어려움이 있을 수 있다. 그럴 때는 RedisCallback 기능을 고려해 보자.

 

4.11 Redis Repositories Running on a Cluster

레디스 클러스터 환경에서 Redis Repository를 사용하려면 몇몇 추가 구성이 필요하다. 왜냐하면 기본 세팅에서는 키들이 전체 클러스터와 그 슬롯들로 분산되기 때문이다. SINTER와 SUNION과 같은 몇몇 명령어들은 동일 슬롯에 한에서만 작동한다.

원하는 슬롯을 설정하고 싶다면 @RedisHash("{yourkeyspace}")를 사용하면 된다.

 

4.12 CDI Integration

repository 인터페이스 인스턴스는 보통 컨테이너에 의해 생성된다. 또한 Spring Data로 작업할 때는 보통 스프링을 많이 선택한다.

Spring Data Redis는 커스텀 CDI 확장 기능을 제공하며, 해당 기능을 통해 CDI 환경에서 repository 추상화가 가능하다.

 

참고 링크

+ Recent posts