스프링부트

Spring Data Redis - Redis Repository 사용

알쓸개잡 2024. 5. 12. 01:16

Spring Data는 Repository 추상화를 통해서 저장소에 대한 편리한 CRUD를 제공한다. 관계형 DB 혹은 NoSQL DB 와는 약간 특성이 다른 Redis에서도 Repository 추상화를 사용할 수 있다.

Spring Data Redis 의 Repository를 사용하여 DTO 클래스를 Redis Hash 타입으로 저장하고 조회하는데 편리한 사용성을 제공한다.

이번 포스팅에서는 Spring Data Redis의 Repository 추상화를 사용하는 방법에 대해서 정리해 보고자 한다.

 

Spring Data Repository 에서 제공되는 확장 인터페이스

Spring Data Repository 추상화의 중심 인터페이스는 Repository 인터페이스 클래스다.

Spring Data 는 Repository 인터페이스를 확장하여 기본적으로 제공되는 인터페이스가 있는데 다음과 같다.

  • CrudRepository
  • ListCrudRepository
  • PagingAndSortingRepository
  • ListPagingAndSortingRepository

제공되는 인터페이스에서 가장 기본이 되는 CrudRepository를 살펴보자.

CrudRepository 클래스 정의는 다음과 같다.

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);

    <S extends T> Iterable<S> saveAll(Iterable<S> entities);

    Optional<T> findById(ID id);

    boolean existsById(ID id);

    Iterable<T> findAll();

    Iterable<T> findAllById(Iterable<ID> ids);

    long count();

    void deleteById(ID id);

    void delete(T entity);

    void deleteAllById(Iterable<? extends ID> ids);

    void deleteAll(Iterable<? extends T> entities);

    void deleteAll();
}

@NoRepositoryBean 어노테이션이 지정된 것을 볼 수 있는데 Repository 확장 인터페이스들은 모두 @NoRepositoryBean 어노테이션이 지정되어 있는데 이는 해당 클래스가 Spring Data의 빈으로 생성되지 않도록 하여 다른 Repository 인터페이스에 의해 확장되도록 하기 위한 목적이 있다.

 

CrudRepository에서 제공하는 메서드는 다음과 같은 역할을 한다.

save(S entity) 인수로 전달된 entity를 저장한다.
findById(ID id) 인수로 전달된 id 식별자로 데이터를 조회한다.
findAll() 동일한 T 타입의 모든 entity를 가져온다.
count() 엔티티 개수를 리턴한다.
delete(T entity) 인수로 전달된 entity를 삭제한다.
existById(ID primaryKey) 인수로 전달된 id 식별자를 가진 엔티티 존재 여부를 확인한다.
deleteById(ID id) 인수로 전달된 id 식별자를 가진 entity를 삭제한다.
deleteAllById(Iterable<? extends ID> ids) 인수로 전달된 ids 들의 식별자를 가진 entity들을 삭제한다.
deleteAll(Iterable<? extends T> entities) 인수로 전달된 entities 들을 삭제한다.
deleteAll() 동일한 T 타입의 모든 entity를 삭제한다.

 

클래스명 앞에 List로 시작하는 인터페이스는 기존 Iterable 타입을 리턴하는 부분을 사용하기 쉽게 List 타입으로 리턴하도록 개선된 클래스다.

 

PagingAndSortingRepository는 여러 entity를 조회하는 메서드 호출 시에 pageable 하게 여러 번 나눠서 데이터를 조회할 수 있도록 하고 정렬 기능도 지원한다.

 

Spring Data Redis Repository 에서 제공되는 확장 인터페이스

Spring Data Redis 에서 제공되는 확장 인터페이스는 다음과 같다.

  • KeyValueRepository
  • SimpleKeyValueRepository (인터페이스가 아닌 클래스다)
  • KeyValueRepository 인터페이스를 상속하여 정의한 Repository의 실제 인스턴스는 SimpleKeyValueRepository 인스턴스인데 이는 디버거를 통해서 확인할 수 있다.
bookRepository=org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository@546083d6
  • KeyValueRepository와 SimpleKeyValueRepository는 org.springframework.data:spring-data-keyvalue 디펜던시 내에 정의되어 있는데 spring-data-keyvalue 디펜던시는 spring-boot-starter-data-redis 디펜던시에 포함되어 있다.
  • KeyValueRepository는 ListCrudRepository, ListPagingAndSortingRepository를 확장한 인터페이스다.
public interface KeyValueRepository<T, ID> extends 
	ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID> {
}

기본적으로 ListCrudRepository, ListPagingAndSortingRepository 에서 제공하는 기능들을 사용할 수 있다.

 

엔티티 상태 감지 전략

간단히 말해서 엔티티가 새로 생성되는 것인지 이미 Redis 데이터에 존재하는 것인지를 판단하는 정책이라고 보면 되겠다.
Redis Repository 내부적으로 엔티티가 새로 생성되는 것이라고 판단되면 @Id(식별자)를 자동으로 생성하여 저장한다.

@Id 속성 필드 검사 (디폴트) @Id 어노테이션으로 지정된 식별자 속성이 null (기본 타입의 경우 0) 인 경우 새로운 엔티티로 간주한다.
@Version 속성 필드 검사 @Version 어노테이션으로 지정된 필드가 존재하고 값이 null (기본 타입의 경우 0) 인 경우 새로운 엔티티로 간주한다. @Version 어노테이션으로 지정된 필드에 값이 존재하지만 @Id 필드에 값이 없는 경우 예외가 발생한다.
Persistable 구현 엔티티가 Persistable 인터페이스를 구현하고 isNew() 메서드를 재정의하여 정의할 수 있다.
custom EntityInformation 구현 모듈별 저장소 팩토리 하위 클래스를 만들고 getEntityInfomation(..) 메서드를 재정의하여 저장소 기본 구현에 사용되는 EntityInformation 추상화를 맞춤 설정할 수 있지만 추천되지 않는 방식이다.

 엔티티가 새로운 것으로 판단되면 DefaultIdentifierGenerator 클래스에서 식별자를 생성하는데 메서드는 다음과 같다.

@Override
@SuppressWarnings("unchecked")
public <T> T generateIdentifierOfType(TypeInformation<T> identifierType) {

    Class<?> type = identifierType.getType();

    if (ClassUtils.isAssignable(UUID.class, type)) {
        return (T) UUID.randomUUID();
    } else if (ClassUtils.isAssignable(String.class, type)) {
        return (T) UUID.randomUUID().toString();
    } else if (ClassUtils.isAssignable(Integer.class, type)) {
        return (T) Integer.valueOf(getSecureRandom().nextInt());
    } else if (ClassUtils.isAssignable(Long.class, type)) {
        return (T) Long.valueOf(getSecureRandom().nextLong());
    }

    throw new InvalidDataAccessApiUsageException(
            String.format("Identifier cannot be generated for %s; Supported types are: UUID, String, Integer, and Long",
                    identifierType.getType().getName()));
}
  • @Id로 지정된 필드의 타입이 UUID, String, Integer, Long 인 경우만 지원되는 것을 알 수 있다.
  • UUID, String 타입에 대해서는 random UUID를 생성하도록 하고 Integer와 , Long 타입에 대해서는 random 값을 생성하도록 한다.

Persistable 인터페이스를 구현하는 방식은 다음과 같이 사용한다.

public class Person implements Persistable<UUID> {
    ...
    ...
    
    @Override
    public UUID getId() {
        return this.id;
    }

    @Override
    public boolean isNew() {
        return this.id == null;
    } 
}

 

SimpleKeyValueRepository와 RedisKeyValueAdapter 클래스

SimpleKeyValueRepository는 KeyValueRepository 인터페이스의 구현체 클래스다.

따라서 KeyValueRepository 인터페이스를 확장한 Repository의 실제 인스턴스는 SimpleKeyValueRepository 인스턴스가 된다.

다음은 SimpleKeyValueRepository의 DI 관계를 나타낸다.

[SimpleKeyValueRepository] -> [KeyValueTemplate] -> [RedisKeyValueAdapter] -> [Redis Database]
  • SimpleKeyValueRepository 클래스는 KeyValueTemplate 인스턴스를 주입받는다. KeyValueTemplate 빈을 별도로 생성하지 않으면 디폴트 인스턴스를 주입받는다.
  • KeyValueTemplate 클래스는 RedisKeyValueAdapter 인스턴스를 주입받는다. RedisKeyValueAdapter 빈을 별도로 생성하지 않으면 디폴트 인스턴스를 주입받는다.
  • RedisKeyValueAdapter 클래스는 RedisTemplate 인스턴스를 주입받고 RedisMappingContext 인스턴스를 주입받거나 디폴트 인스턴스를 생성한다.
RedisKeyValueTemplate과 RedisKeyValueAdapter 빈은 @EnableRedisRepositories 어노테이션이 지정된 경우 자동으로 빈으로 등록된다.

동작흐름

  • SimpleKeyValueRepository 에서 CRUD 작업이 호출되면 KeyValueTemplate을 사용하여 작업을 처리한다.
  • KeyValueTemplate은 작업을 RedisKeyValueAdpater에 위임하여 처리한다.
  • RedisKeyValueAdapter는 Redis 서버에 명령을 실행한다.

결국 RedisKeyValueAdapter는

  • CRUD 처리를 위한 낮은 수준의 Redis 작업을 처리한다.
  • 일반적으로 해시를 사용하여 엔티티를 저장하는 방식으로 도메인 객체를 Redis 데이터 구조에 매핑한다.
  • Redis에서 인덱스 구조를 관리하여 secondary index(보조인덱스)를 생성한다.

와 같은 핵심 역할을 수행한다.

 

Query Method

Spring Data Repository의 Query Method는 데이터 접근을 간소화하고 메서드 이름만으로 쿼리를 생성하는 기능을 제공한다.

Query Method의 구조는 일반적으로 findBy.., readBy.., queryBy.., countBy.., getBy.. 와 같은 형태를 지닌다.

다음은 Redis에서 지원되는 키워드와 기능에 대한 표이다.

Keyword Sample Redis snippet
And findByLastnameAndFirstname SINTER …:firstname:rand …:lastname:althor
Or findByLastnameOrFirstname SUNION …:firstname:rand …:lastname:althor
Is, Equals findByFirstname
findByFirstnameIs
findByFirstnameEquals
SINTER …:firstname:rand
IsTrue findByAliveIsTrue SINTER …:alive:1
IsFalse findByAliveIsFalse SINTER …:alive:0
Top, First findFirst10ByFirstname, findTop5ByFirstname  

참고: https://docs.spring.io/spring-data/redis/reference/redis/redis-repositories/queries.html

 

Data Mapping and Type Conversion

Repository를 통해서 저장되는 엔티티 객체에 지정된 필드에는 다양한 데이터 타입이 지정된다. 예를 들어 List, Map 혹은 사용자 지정 객체가 될 수 있겠다.

다음은 Hash 타입으로 저장되는 엔티티의 각 타입에 매핑되는 Redis 저장형태 설명한다.

Type Sample Mapped Value
Simple Type String firstname = "rand" firstname = "rand"
Byte array byte[] image = "rand".getBytes() image = "rand"
Complex Type Address address = new Address("Seoul") address.city = "Seoul"
Simple List List<String> nicknames = asList("dragon reborn", "lews therin"); nicknames.[0] = "dragon reborn",
nicknames.[1] = "lews therin"
Simple Map Map<String, String> atts = asMap({"eye-color", "grey"}, {"hair-color", "black"} ...) atts.[eye-color] = "grey",
atts.[hair-color] = "black",
...
Complex List List<Address> addresses = asList(new Address("Seoul…​ addresses.[0].city = "Seoul",
addresses.[1].city = "..."
Complex Map Map<String, Address> addresses = asMap({"home", new Address("Seoul…​ addresses.[home].city = "Seoul",
addresses.[work].city = "…​

참고: https://docs.spring.io/spring-data/redis/reference/redis/redis-repositories/mapping.html#mapping-conversion

다음은 Map 타입의 필드가 저장되는 예시다.

@RedisHash("books")
public class Book {
    @Id
    private UUID bookId;   
    ...
    private Map<String, String> attr;
}

===========================================

Book book = Book.builder()
            .bookId( bookId )
            ...
            .attr( Map.ofEntries(
                    Map.entry( "publisher", "dream world" ),
                    Map.entry( "publish date", "2024-01-01")) )
            .build();
bookRepository.save(book);

===========================================

book 엔티티의 attr 속성은 Redis에 다음과 같이 저장된다.

bookId 50db3c9b-3399-4e16-a7dd-48907307702a
127.0.0.1:6379> hgetall books:50db3c9b-3399-4e16-a7dd-48907307702a
...
 3) "attr.[publish date]"
 4) "2024-01-01"
 5) "attr.[publisher]"
 6) "dream world"
...

 

Repository를 사용하는 기본적인 방법

Repository를 사용하는 방법은 매우 간단하다. 기본으로 제공되는 인터페이스를 확장한 인터페이스를 정의하면 기본적으로 식별자를 이용한 CRUD가 가능해진다.

public interface BookRepository extends KeyValueRepository<Book, UUID> {}
private final BookRepository bookRepository;
...
...
public Book findBook(UUID id) {
    ...
    //id 는 Book 엔티티의 @Id로 지정된 필드의 값이다.
    //Repository에서는 <keyspace>:@Id 형태로 키를 생성하여 조회한다.
    Optional<Book> optional = bookRepository.findById(id);
    return optional.orElse(null);
    ...
}

 

주요 어노테이션

Repository를 사용할 때 관련된 어노테이션은 다음과 같다.

@RedisHash 클래스가 Redis 해시임을 나타내는데 사용한다. Java 객체를 Redis 해시 데이터 구조에 매핑하는 방법을 제공하여 Redis와 상호작용을 할 수 있도록 한다.
다음과 같은 속성을 지정할 수 있다.

value
Redis Hash의 keyspace 이름을 지정한다. 지정된 keyspace를 통해서 redis에 저장되는 key에 prefix를 붙일 수 있다. 생성되는 key 형태는 다음과 같다.
<keyspace>:@Id
Repository는 <keyspace>:@Id 형태의 키를 자동으로 조합하여 데이터를 조회한다.

timeToLive
데이터의 만료 시간을 초 단위로 지정한다. 기본값은 -1로써 만료되지 않는다.
@Id @Id 어노테이션이 지정된 필드에 대해서 식별자로 사용한다.
@Indexed @Indexed 어노테이션이 지정된 필드에 대해서 secondary index를 생성한다.
secondary index(보조인덱스) 로 지정된 필드의 값에 대해서 별도의 key set을 생성하여 해당 필드로 조회가 가능하게 한다.
@Reference @Reference로 지정된 객체 필드의 key를 참조로 저장한다.

 

@RedisHash 사용 예시

@RedisHash(value="books")
public class Book {
    @Id
    private UUID bookId;
    ...
}
  • Book를 BookRepository를 통해서 저장하면 Redis에 생성되는 key는 다음과 같다.
bookId 3ee49122-eb61-49a3-842a-edac807e18b9

 

"books:3ee49122-eb61-49a3-842a-edac807e18b9"

 

 

@Indexed 사용 예시

@RedisHash("books")
public class Book {
    @Id
    private UUID bookId;
    @Indexed
    private String title;
    ...
}
  • title 멤버 변수에 @Indexed 어노테이션을 지정하였다.
  • title 멤버 변수 값에 대해서 secondary index가 생성되는데 다음과 같다.
bookId e11813dc-8487-4b18-a5a2-b1e5b778daeb
title first book
"books:e11813dc-8487-4b18-a5a2-b1e5b778daeb"
"books:e11813dc-8487-4b18-a5a2-b1e5b778daeb:idx"
"books:title:first book"
  • "books:title:first book"과  "books:e11813dc-8487-4b18-a5a2-b1e5b778daeb:idx" 키가 추가로 생성된다.
127.0.0.1:6379> type "books:title:first book"
set
127.0.0.1:6379> type "books:e11813dc-8487-4b18-a5a2-b1e5b778daeb:idx"
set
127.0.0.1:6379> smembers "books:title:first book"
1) "e11813dc-8487-4b18-a5a2-b1e5b778daeb"

127.0.0.1:6379> smembers "books:e11813dc-8487-4b18-a5a2-b1e5b778daeb:idx"
1) "books:title:first book"
  • "books:title:first book"과 "books:e11813 dc-8487-4b18-a5 a2-b1 e5 b778 daeb:idx"에 저장된 값을 보면 서로 상호적으로 키를 가지고 있음을 알 수 있다.
Repository에서 @Id로 지정된 필드를 제외한 다른 필드에 대해서 조회를 할 때는 위와 같이 secondary index 가 지정된 필드에 대해서 조회가 가능하다.

 

@Reference 사용 예시

public interface PersonRepository extends KeyValueRepository<Person, UUID> {
}

================================

public interface BookRepository extends KeyValueRepository<Book, UUID> {
}

================================

@RedisHash("books")
public class Book {
    @Id
    private UUID bookId;
    @Indexed
    private String title;
    @Reference
    private Person author;
    ...
}

================================

Person author = Person.builder()
                .id( authorId )
                .personName( "author" )
                .personAge( 75 )
                .personNation( "korea" )
                .comment( "my first book" )
                .build();
personRepository.save( author );

Book book = Book.builder()
        .bookId( bookId )
        .title( "first book" )
        .pages( 100 )
        .author( author )
        .attr( Map.ofEntries(
                Map.entry( "publisher", "dream world" ),
                Map.entry( "publish date", "2024-01-01")) )
        .build();

bookRepository.save( book );

================================
  • Redis에 저장된 Person 데이터를 Book 클래스의 author 필드에 저장한다.
  • author 필드에는 @Reference 어노테이션이 지정되어 있어서 참조 형태로 다음과 같이 저장된다.
bookId 85535857-d86d-43b5-acd4-1540e7cff3fd
authorId b0bef0e3-7347-44c1-80f7-b3f3ea1465b4
127.0.0.1:6379> hgetall books:85535857-d86d-43b5-acd4-1540e7cff3fd
 1) "_class"
 2) "com.example.spring.redis.model.Book"
 3) "attr.[publish date]"
 4) "2024-01-01"
 5) "attr.[publisher]"
 6) "dream world"
 7) "author"
 8) "persons:b0bef0e3-7347-44c1-80f7-b3f3ea1465b4"
 9) "bookId"
10) "85535857-d86d-43b5-acd4-1540e7cff3fd"
11) "pages"
12) "100"
13) "title"
14) "first book"
  • author 필드에 persons:b0 bef0e3-7347-44c1-80f7-b3 f3 ea1465 b4 와 같이 키가 참조로 저장된다.
  • BookRepository를 통해서 Book 데이터를 조회하면 Book 엔티티의 author 필드에는 persons:b0bef0e3-7347-44c1-80f7-b3f3ea1465b4 키에 저장된 Person 엔티티가 저장된다.

참조로 지정된 필드의 객체를 가져오는 화면
참조필드에 셋팅된 데이터

 

Partial Update

  • Hash 타입으로 저장된 엔티티의 특정 필드만을 바로 업데이트할 수 있는데 이는 RedisKeyValueAdapter를 통해서 가능하다.
  • RedisKeyValueAdapter는 Redis Repository의 내부 동작을 제어하는데 중요한 역할을 한다.
  • Partial Update는 Spring Data Redis에서 레코드의 특정 필드만 업데이트하는 데 사용되는 기능이다.
  • 이를 통해 Redis에 저장된 전체 엔티티를 가져와서 수정할 필요 없이 부분적으로 즉시 업데이트 할 수 있어 적절한 데이터 변경에 유용하다.

사용법은 다음과 같다.

@Configration
@EnableRedisRepositories
public class RedisConfiguration {
    ....
}
Book book = Book.builder()
                .bookId( bookId )
                .title( "first book" )
                .pages( 100 )
                .author( author )
                .attr( Map.ofEntries(
                        Map.entry( "publisher", "dream world" ),
                        Map.entry( "publish date", "2024-01-01")) )
                .build();
//RedisKeyValueAdapter 빈 주입
@Autowired
private RedisKeyValueAdapter redisKeyValueAdapter;

...

//Book 엔티티의 title, pages, attr.[publisher] 필드 부분 업데이트
PartialUpdate<Book> bookPartialUpdate = new PartialUpdate<>( bookId, Book.class )
                .set( "title", "updated first book" )
                .set( "pages", 1000 )
                .set( "attr.[publisher]", "partial updated dream world" );
redisKeyValueAdapter.update( bookPartialUpdate );

book 엔티티의 title, pages, attr.[publisher] 를 atomic 하게 수정할 수 있다.

Redis 저장 결과는 다음과 같다.

bookId a8aa097d-9260-4893-bf83-95bd1e729dd0
127.0.0.1:6379> hgetall books:a8aa097d-9260-4893-bf83-95bd1e729dd0
 1) "_class"
 2) "com.example.spring.redis.model.Book"
 3) "attr.[publish date]"
 4) "2024-01-01"
 5) "attr.[publisher]"
 6) "partial updated dream world"
 7) "author"
 8) "persons:bbde9b97-d687-462b-b612-09795332c851"
 9) "bookId"
10) "a8aa097d-9260-4893-bf83-95bd1e729dd0"
11) "pages"
12) "1000"
13) "title"
14) "updated first book"

title, pages, attr.[publisher] 필드가 수정된 것을 확인할 수 있다.

 

Keyspace

  • Keyspace는 Redis 해시에 대한 실제 키를 생성하는 데 사용되는 접두사를 정의한다.
  • @RedisHash에 별도의 value 속성을 지정하지 않으면 기본적으로 해당 엔티티 클래스의 getClass().getName()을 keyspace로 사용한다.
  • Keyspace는 @RedisHash 어노테이션의 value 속성을 통해서 지정하는 방법과 Configuration을 통해서 지정하는 방법이 있다.
    • 어노테이션 기반으로 설정을 한 경우에는 엔티티 클래스마다 keyspace를 지정하지만 손쉽게 keyspace를 정의할 수 있다.
    • Configuration을 통해서 지정하는 방식은 별도의 keyspace를 생성하기 위한 클래스를 지정하지만 모든 엔티티 클래스에 대해서 일괄적으로 keyspace를 정의할 수 있다.

Configuration을 통해서 keyspace를 지정하는 방식은 2가지로 나눌 수 있는데 다음과 같다.

@EnableRedisRepositories의 keyspaceConfiguration 속성 지정

@Configuration
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository",
    keyspaceConfiguration = RedisConfiguration.CustomKeyspaceConfiguration.class)
public class RedisConfiguration {
    ...
    ...
    ...
    //keyspace 생성을 위한 KeyspaceConfiguration 확장 클래스 정의
    public static class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
        @Override
        protected Iterable<KeyspaceSettings> initialConfiguration() {
            return Set.of(
                    new KeyspaceSettings( Person.class, "persons" ),
                    new KeyspaceSettings( Book.class, "books" )
            );
        }
    }
}

KeyspaceConfiguration을 확장한 클래스에서 initialConfiguration을 재정의 하여 keyspace를 정의할 수 있다.

127.0.0.1:6379> scan 0
1) "0"
2) 1) "books:4a3b2082-44ea-41a8-88a0-5fd048ed8c09"
   2) "persons:f74f4f26-8105-42c6-a8c5-bb3f0a284289"
   3) "books"
   4) "persons"
   ...

 

RedisMappingContext, RedisKeyValueAdapter, RedisKeyValueTemplate 빈을 통해서 설정

@Configuration
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository")
public class RedisConfiguration {
    ...
    ...
    @Bean
    public RedisMappingContext mappingContext() {
        //이 곳에 사용자 정의 IndexConfiguration, KeyspaceConfiguration을 셋팅할 수 있다.
        return new RedisMappingContext(
                new MappingConfiguration( new IndexConfiguration(), new CustomKeyspaceConfiguration() )
        );
    }


    //for PartialUpdate 사용을 위한 RedisKeyValueAdapter 빈 생성
    @Bean
    public RedisKeyValueAdapter redisKeyValueAdapter(RedisTemplate<String, Object> redisTemplate,
                                                     RedisMappingContext mappingContext) {
        //mappingRedisConverter 인스턴스를 전달해도 무방하다.
        //MappingRedisConverter mappingRedisConverter = new MappingRedisConverter( mappingContext );
        //return new RedisKeyValueAdapter(redisTemplate, mappingRedisConverter);                                                     
        return new RedisKeyValueAdapter(redisTemplate, mappingContext);
    }
    
    //RedisKeyValueTemplate 빈 생성
    //SimpleKeyValueRepository에서 사용되는 RedisKeyValueTemplate 에도 MappingContext가 전달되어야
    //데이터 조회시 keyspace를 찾는다.
    @Bean
    public RedisKeyValueTemplate redisKeyValueTemplate( RedisKeyValueAdapter redisKeyValueAdapter,
                                                        RedisMappingContext mappingContext ) {
        return new RedisKeyValueTemplate( redisKeyValueAdapter, mappingContext );
    }
    
    //keyspace 생성을 위한 KeyspaceConfiguration 확장 클래스 정의
    public static class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
        @Override
        protected Iterable<KeyspaceSettings> initialConfiguration() {
            return Set.of(
                    new KeyspaceSettings( Person.class, "persons" ),
                    new KeyspaceSettings( Book.class, "books" )
            );
        }
    }
}
  • RedisMappingContext 빈에 직접 정의한 CustomKeyspaceConfiguration 인스턴스를 주입한다.
  • RedisKeyValueAdapter 빈에 RedisTemplate 빈과 RedisMappingContext 빈을 주입한다.
  • RedisKeyValueAdapter 빈은 KeyValueTemplate 빈에 주입이 된다.
  • KeyValueTemplate 빈은 SimpleKeyValueRepository 빈에 주입이 된다.

동작은 다음과 같다.

  • CRUD 작업 시에 SimpleKeyValueRepository 빈이 동작하며 내부적으로 KeyValueTemplate 인스턴스가 사용된다.
  • KeyValueTemplate 인스턴스는 내부적으로 RedisKeyValueAdapter 인스턴스를 사용한다.
  • RedisKeyValueAdapter 인스턴스는 keyspace를 RedisMappingContext 빈에 주입된 CustomKeyspaceConfiguration으로부터 찾는다.
@RedisHash의 value 속성으로 keyspace가 지정되어 있고 Configuration에 의해서도 keyspace가 지정되어 있다면 Configuration에서 정의한 keyspace가 적용된다.

 

 

Secondary Index

  • Secondary Index (보조인덱스)는 Redis 구조를 기반으로 조회 작업을 활성화하는 데 사용된다.
    • Repository를 통해서 @Id로 지정된 필드 이외에 조회하고자 하는 필드가 있을 때 해당 필드는 secondary index로 지정되어야 조회가 가능하다.
  • secondary index로 지정된 필드에 값을 저장할 때마다 해당 index에 기록되며 엔티티가 삭제되거나 만료되면 제거된다.
  • secondary index는 필드에 @Indexed 어노테이션을 지정하는 방법과 Configuration을 통해서 지정하는 방법이 있다.
    • 어노테이션 기반으로 설정을 한 경우에는 손쉽게 secondary index를 지정할 수 있지만 Map이나 List 타입의 특정 필드에 대해서 secondary index를 지정하기 어렵다.
    • Configuration을 통해서 지정하는 방식은 별도의 secondary index를 생성하기 위한 클래스를 지정하지만 secondary index를 지정할 필드를 일괄로 등록할 수 있고 Map이나 List 타입의 특정 필드에 대해서 secondary index를 지정할 수 있다.

@EnableRedisRepositories의 IndexConfiguration 속성 지정

@RedisHash("persons")
public class Person {}

@RedisHash("books")
public class Books {}

 

@Configuration
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository",
    indexConfiguration = RedisConfiguration.SecondaryIndexConfiguration.class)
public class RedisConfiguration {
    ...
    ...
    ...
    //secondary index 생성을 위한 IndexConfiguration 확장 클래스 정의
    public static class SecondaryIndexConfiguration extends IndexConfiguration {
        @Override
        protected Iterable<? extends IndexDefinition> initialConfiguration() {
            return Set.of(
                    new SimpleIndexDefinition( "persons", "personName" ),
                    new SimpleIndexDefinition( "books", "title" ),
                    new SimpleIndexDefinition( "books", "attr.publisher" )
            );
        }
    }
}

IndexConfiguration을 확장한 클래스에서 initialConfiguration을 재정의 하여 secondary index를 정의할 수 있다.

127.0.0.1:6379> scan 0
1) "0"
2) 1) "persons:82cc22f1-1e5f-4fba-b0ef-abf33619cbbc:idx"
   2) "books:5456f196-a911-491f-bc85-efd4b0adc0fd:idx"
   3) "books:attr.publisher:dream world"
   4) "books:title:first book"
   5) "persons:personName:author"
   ...

books와 persons keyspace에 지정된 secondary 인덱스 키 정보만 발췌한 것이다. attr Map의 [publisher] 키에 대해서만 secondary index를 생성할 수 있다.

 

 

RedisMappingContext, RedisKeyValueAdapter 빈을 통해서 설정

Keyspace의 Configuration 방식과 동일한 방식으로 정의할 수 있다.

@Configuration
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository")
public class RedisConfiguration {
    ...
    ...
    //keyspace, secondary index 설정
    @Bean
    public RedisMappingContext mappingContext() {
        return new RedisMappingContext(
                new MappingConfiguration( new SecondaryIndexConfiguration(), new KeyspaceConfiguration() )
        );
    }

    //RedisKeyValueAdapter 빈 생성
    @Bean
    public RedisKeyValueAdapter redisKeyValueAdapter( RedisTemplate<String, Object> redisTemplate,
                                                      RedisMappingContext mappingContext) {
        //mappingRedisConverter 인스턴스를 전달해도 무방하다.
        //MappingRedisConverter mappingRedisConverter = new MappingRedisConverter( mappingContext );
        //return new RedisKeyValueAdapter(redisTemplate, mappingRedisConverter);
        return new RedisKeyValueAdapter(redisTemplate, mappingContext);
    }
    
    //secondary index 생성을 위한 IndexConfiguration 확장 클래스 정의
    public static class SecondaryIndexConfiguration extends IndexConfiguration {
        @Override
        protected Iterable<? extends IndexDefinition> initialConfiguration() {
            return Set.of(
                    new SimpleIndexDefinition( "persons", "personName" ),
                    new SimpleIndexDefinition( "books", "title" ),
                    new SimpleIndexDefinition( "books", "attr.publisher" )
            );
        }
    }
}

각각의 빈이 주입되는 방식과 CRUD에 대한 동작 방식은 Keyspace 섹션에서 설명한 방식과 동일하다.

 

최종적으로 Keyspace, Secondary Index 모두 Configuration 방식으로 설정할 수 있다.

1. @EnableRedisRepositories를 이용한 방식

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository",
    keyspaceConfiguration = RedisConfiguration.CustomKeyspaceConfiguration.class,
    indexConfiguration = RedisConfiguration.SecondaryIndexConfiguration.class )
public class RedisConfiguration {
    ...
    ...
    ...
    //secondary index 생성을 위한 IndexConfiguration 확장 클래스 정의
    public static class SecondaryIndexConfiguration extends IndexConfiguration {
        @Override
        protected Iterable<? extends IndexDefinition> initialConfiguration() {
            return Set.of(
                    new SimpleIndexDefinition( "persons", "personName" ),
                    new SimpleIndexDefinition( "books", "title" ),
                    new SimpleIndexDefinition( "books", "attr.publisher" )
            );
        }
    }

    //keyspace 생성을 위한 KeyspaceConfiguration 확장 클래스 정의
    public static class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
        @Override
        protected Iterable<KeyspaceSettings> initialConfiguration() {
            return Set.of(
                    new KeyspaceSettings( Person.class, "persons" ),
                    new KeyspaceSettings( Book.class, "books" )
            );
        }
    }
}

 

2. RedisMappingContext, RedisKeyValueAdapter 빈을 통해서 설정

@Configuration
@EnableRedisRepositories(basePackages = "com.example.spring.redis.repository")
public class RedisConfiguration {
    ...
    ...
    //keyspace, secondary index 설정
    @Bean
    public RedisMappingContext mappingContext() {
        return new RedisMappingContext(
                new MappingConfiguration( 
                    new SecondaryIndexConfiguration(), 
                    new CustomKeyspaceConfiguration() 
                )
        );
    }

    //RedisKeyValueAdapter 빈 생성
    @Bean
    public RedisKeyValueAdapter redisKeyValueAdapter( RedisTemplate<String, Object> redisTemplate,
                                                      RedisMappingContext mappingContext) {
        //mappingRedisConverter 인스턴스를 전달해도 무방하다.
        //MappingRedisConverter mappingRedisConverter = new MappingRedisConverter( mappingContext );
        //return new RedisKeyValueAdapter(redisTemplate, mappingRedisConverter);
        return new RedisKeyValueAdapter(redisTemplate, mappingContext);
    }

    //secondary index 생성을 위한 IndexConfiguration 확장 클래스 정의
    public static class SecondaryIndexConfiguration extends IndexConfiguration {
        @Override
        protected Iterable<? extends IndexDefinition> initialConfiguration() {
            return Set.of(
                    new SimpleIndexDefinition( "persons", "personName" ),
                    new SimpleIndexDefinition( "books", "title" ),
                    new SimpleIndexDefinition( "books", "attr.publisher" )
            );
        }
    }

    //keyspace 생성을 위한 KeyspaceConfiguration 확장 클래스 정의
    public static class CustomKeyspaceConfiguration extends KeyspaceConfiguration {
        @Override
        protected Iterable<KeyspaceSettings> initialConfiguration() {
            return Set.of(
                    new KeyspaceSettings( Person.class, "persons" ),
                    new KeyspaceSettings( Book.class, "books" )
            );
        }
    }
}

 

Redis에 저장된 결과는 다음과 같다.

127.0.0.1:6379> scan 0
1) "0"
2) 1) "books:attr.publisher:dream world"
   2) "books:01ac274c-469a-4ef9-bc99-522a071f1446:idx"
   3) "persons:21ce0e9b-e1b2-4557-b8ea-517e6af55da6"
   4) "books"
   5) "books:title:first book"
   6) "books:01ac274c-469a-4ef9-bc99-522a071f1446"
   7) "persons"
   8) "persons:21ce0e9b-e1b2-4557-b8ea-517e6af55da6:idx"
   9) "persons:personName:author"

 

개인적으로는 @EnableRedisRepositories의 indexConfiguration, keyspaceConfiguration 속성을 이용하는 방법이 간단하고 좋은 것 같다.

 

샘플 코드는 GITLAB링크에서 확인할 수 있다.

끝.