스프링부트

Spring Data Redis - RedisTemplate의 ValueOperations

알쓸개잡 2024. 4. 24. 17:13

Spring Data Redis의 중심 클래스인 RedisTemplate의 여러 Operations 중에서 가장 기본적인 ValueOperations에 대해서 정리해보고자 한다. ValueOperations는 개발자가 Redis 문자열 값과 상호 작용하는 방식을 단순화하여 일반적인 작업에 대한 간단한 방법을 제공한다. 이번 포스팅에서는 Spring Data Redis에서 제공하는 RedisTemplate의 ValueOperations의 각 메서드에 대해서 테스트 코드와 함께 알아보고자 한다.

 

먼저 Spring Data Redis를 사용하기 위해서 Redis 사용을 위한 AutoConfiguration에 대해서 알아두면 좋다. 아래 포스팅에 정리해 두었다.

2024.03.24 - [스프링부트] - Spring Data Redis - Auto Configuration을 이용한 Redis 연결 설정

 

또한 샘플 코드 제시를 위해서 사용한 코드 프로젝트는 spring boot 3.1부터 지원되는 docker compose를 사용하였다. spring boot docker compose에 대한 내용은 아래 포스팅을 참고하기 바란다.

2023.10.22 - [스프링부트] - spring boot 3.1 docker compose support

 

Spring Data Redis 사용을 위한 Configuration에 대해서는 Spring Data Redis - Auto Configuration을 이용한 Redis 연결 설정 포스팅에 정리하였으므로 생략하도록 하겠다.

 

ValueOperations의 사용 사례

ValueOperations는 주로 Redis의 간단한 값(또는 문자열)과 관련된 작업을 처리하는 데 사용된다.

  • 캐싱
    • Redis는 자주 액세스하는 데이터를 메모리에 저장하여 데이터베이스 부하를 줄이기 위한 캐싱 솔루션으로 자주 사용된다. ValueOperations를 사용하면 이러한 캐시 된 값을 신속하게 검색하고 업데이트할 수 있으므로 성능이 중요하고 데이터 읽기가 많은 애플리케이션에 적합하다.
  • 세션 저장소
    • 웹 애플리케이션에서 ValueOperations를 사용하여 세션 데이터를 Redis에 저장할 수 있다. 이를 통해 기존 세션관리(로컬 또는 관계형 데이터베이스)가 너무 느리거나 번거로울 수 있는 분산 환경에서 사용자 상태를 유지하는데 필수적인 빠른 세션 검색 및 업데이트가 가능하다.
  • 카운터
    • Redis는 좋아요, 조회수 또는 동시 업데이트가 필요한 기타 측정항목과 같은 카운터를 처리하는데 자주 사용된다. ValueOperations는 이러한 사용 사례에 필수적인 증가 및 감소와 같은 원자적 연산을 제공한다.
  • 실시간 데이터
    • 채팅 애플리케이션이나 라이브 이벤트 모니터링과 같이 실시간 데이터 처리가 필요한 애플리케이션의 경우 ValueOperations는 메시지 또는 이벤트 데이터를 빠르게 저장하고 검색하는데 도움이 된다.
  • 임시 데이터 저장소
    • OTP(일회용 비밀번호) 또는 임시 액세스 토큰과 같이 데이터를 장기간 유지할 필요가 없는 시나리오의 경우 ValueOperations를 사용하여 이러한 임시 데이터를 효율적으로 관리할 수 있다. Redis에 내장된 만료 기능을 사용하면 이러한 키에 TTL 설정이 간단해진다.

 

ValueOperations 테스트 코드

Spring Data Redis에서 제공하는 ValueOperations의 여러 메서드에 대해서 테스트 코드와 함께 살펴보겠다.

 

테스트 코드에 docker compose 적용을 위한 설정

spring boot에서 지원하는 docker compose를 테스트 코드에 적용하기 위해서는 다음과 같은 설정이 필요하다.

build.gradle에 다음과 같은 dependency가 필요하다.

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
testAndDevelopmentOnly 'org.springframework.boot:spring-boot-docker-compose'

 

application.yml

spring:
  application:
    name: spring-redis
  docker:
    compose:
      enabled: true
      lifecycle-management: start_and_stop
      stop:
        command: down
        timeout: 30s
      skip:
        in-tests: off

spring.docker.skip.in-tests 설정을 off로 지정해야 한다.

 

RedisTemplate Bean 설정

@Bean
public RedisTemplate<String, Object> redisTemplate( LettuceConnectionFactory lettuceConnectionFactory ) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory( lettuceConnectionFactory );

    // set key serializer
    // template.setKeySerializer( new StringRedisSerializer(StandardCharsets.UTF_8) ); 와 동일함
    template.setKeySerializer( RedisSerializer.string() );
    template.setHashKeySerializer( RedisSerializer.string() );

    // set value serializer
    // template.setDefaultSerializer( new GenericJackson2JsonRedisSerializer() ); 와 동일함
    template.setDefaultSerializer( RedisSerializer.json() );
    template.setValueSerializer( RedisSerializer.json() );
    template.setHashValueSerializer( RedisSerializer.json() );

    return template;
}

//String 타입 value 전용 RedisTemplate 빈 생성
@Bean
public StringRedisTemplate stringRedisTemplate( LettuceConnectionFactory lettuceConnectionFactory ) {
    return new StringRedisTemplate(lettuceConnectionFactory);
}

//Integer 타입 value 전용 RedisTemplate 빈 생성
@Bean
public RedisTemplate<String, Integer> integerRedisTemplate( LettuceConnectionFactory lettuceConnectionFactory ) {
    RedisTemplate<String, Integer> template = new RedisTemplate<>();
    template.setConnectionFactory( lettuceConnectionFactory );
    // set key serializer
    // template.setKeySerializer( new StringRedisSerializer(StandardCharsets.UTF_8) ); 와 동일함
    template.setKeySerializer( RedisSerializer.string() );

    // set value serializer
    // template.setDefaultSerializer( new GenericJackson2JsonRedisSerializer() ); 와 동일함
    template.setValueSerializer( RedisSerializer.json() );
    return template;
}

Object 타입을 값으로 저장하는 RedisTemplate 빈과 String 타입을 값으로 저장하는 StringRedisTemplate을 빈으로 등록하였다.

 

Base Test 클래스

@ExtendWith( SpringExtension.class )
@ContextConfiguration( classes = { RedisConfiguration.class } )
@DataRedisTest
@EnableAutoConfiguration
public class BaseTest {
    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
    @Autowired
    RedisTemplate<String, Integer> integerRedisTemplate;
}

RedisTemplate빈과 StringRedisTemplate빈을 주입받는 기본 클래스를 정의하였다.

ValueOperations 기능을 테스트하기 위한 테스트 클래스는 BaseTest 클래스를 상속받아 사용할 것이다.

public class ValueOperationTest extends BaseTest {
...
}

ValueOperationTest 클래스는 BaseTest 클래스를 상속하므로 redisTemplate, stringRedisTemplate, integerRedisTemplate 빈을 사용할 수 있다.

ValueOperations 메서드 기능 확인을 위한 테스트 코드는 ValueOperationTest 클래스 내에 작성하였다.

 

ValueOperations 메서드

이제 본격적으로 ValueOperations에서 제공하는 메서드를 확인해 보자.

 

set() 과 get()

ValueOperations 메서드 중에 가장 기본적인 메서드라고 할 수 있다.

void set(K key, V value) Key에 Value 값을 저장한다.
void set(K key, V value, logn offset) Key에 Value 값을 저장하되 offset 위치에 Value 값을 replace 한다.
void set(K key, V value, long timeout, TimeUnit unit) Key에 Value 값을 저장하고 만료 시간을 지정한다.
default void set(K key, V value, Duration timeout) Key에 Value 값을 저장하고 만료 시간을 지정한다.
V get(Object key) Key에 해당하는 Value 값을 리턴한다.
String get(K key, long start, long end) start 위치부터 end로 지정된 개수의 문자열을 리턴한다.
리턴 타입이 String 이므로 StringRedisTemplate에만 적용된다.
V getAndDelete(K key) Key에 해당하는 값을 리턴하면서 값을 삭제한다.
V getAndExpire(K key, long timeout, TimeUnit unit) Key에 해당하는 값을 리턴하면서 만료 시간을 지정한다.
V getAndExpire(K key, Duration timeout) Key에 해당하는 값을 리턴하면서 만료 시간을 지정한다.

테스트 코드는 다음과 같다.

@Test
@DisplayName( "set and get 테스트" )
void set_and_get_test() {
    final UUID key = UUID.randomUUID();
    final UUID id = UUID.randomUUID();
    Person person = Person.builder()
            .id( id )
            .personName( "test-name" )
            .personAge( 100 )
            .personNation( "korea" )
            .comment( "first commit data" )
            .build();

    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    valueOperations.set( key.toString(), person );

    // find data
    Object object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isInstanceOf( Person.class );
    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( id.toString() );
    }

    //void set(K key, V value, long offset) 테스트
    final UUID key2 = UUID.randomUUID();
    final String value = "test data";
    ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
    stringValueOperations.set( key2.toString(), value, 0L );
    stringValueOperations.set( key2.toString(), " my data", 4 );
    String getValue = stringValueOperations.get( key2.toString() );
    Assertions.assertThat( getValue ).isEqualTo( "test my data" );
    getValue = stringValueOperations.get( key2.toString(), 0, 6 );
    Assertions.assertThat( getValue ).isEqualTo( "test my" );
}

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get 8db1d273-171a-461a-b74d-19ad86585ea9
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"8c41db1a-7a57-4db1-aac6-318e209da38e\",\"personName\":\"test-name\",\"personAge\":100,\"personNation\":\"korea\",\"comment\":\"first commit data\"}"
127.0.0.1:6379> get df7797cc-6cc4-4c2a-844a-04e7045b7627
"test my data"

 

저장되는 데이터에 만료 시간 지정

@Test
@DisplayName( "set and get expiration 테스트" )
void set_and_expiration_test() throws InterruptedException {
    final UUID key = UUID.randomUUID();
    final UUID id = UUID.randomUUID();
    Person person = Person.builder()
            .id( id )
            .personName( "test-name" )
            .personAge( 100 )
            .personNation( "korea" )
            .comment( "first commit data" )
            .build();

    //5초 후에 데이터 만료 - 5초 후에 Redis에 저장된 데이터는 자동 삭제된다.
    final long timeout = 5000L;
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    //아래 메서드를 호출해도 동일하다.
    //valueOperations.set( key.toString(), person, Duration.ofMillis(timeout) );
    valueOperations.set( key.toString(), person, timeout, TimeUnit.MILLISECONDS );

    // find data
    Object object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( object ).isInstanceOf( Person.class );
    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( id.toString() );
    }

    //5초가 지난뒤에는 데이터가 삭제됨
    Thread.sleep( timeout );
    object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNull();

    //getAndExpire 테스트
    final UUID key2 = UUID.randomUUID();
    final String value = "test value";
    ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
    stringValueOperations.set( key2.toString(), value );
    String getValue = stringValueOperations.getAndExpire( key2.toString(), Duration.ofMillis( timeout ) );
    Assertions.assertThat( getValue ).isEqualTo( value );
    Thread.sleep( timeout );
    getValue = stringValueOperations.get( key2.toString() );
    Assertions.assertThat( getValue ).isNull();
}

위 테스트 코드는 만료 시간을 5초로 지정하였다. 5초 후에 저장된 데이터는 삭제된다.

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get fb38165e-372f-46e6-b2b5-e96b9c1820d6
(nil)
127.0.0.1:6379> get 0b9d4dc1-cea7-4653-ac43-f918f538b95b
(nil)

 

getAndSet()

V getAndSet(Key key, V value) key에 value를 저장하면서 이전 value 값을 리턴한다.
@Test
@DisplayName( "getAndSet 테스트" )
void getAndSet_test() {
    final UUID key = UUID.randomUUID();
    final UUID oldPersonId = UUID.randomUUID();
    Person person = Person.builder()
            .id( oldPersonId )
            .personName( "test-name" )
            .personAge( 100 )
            .personNation( "korea" )
            .comment( "first commit data" )
            .build();

    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    //현재 value에는 데이터가 없으므로 object는 null 이고 person이 value에 저장됨.
    Object object = valueOperations.getAndSet( key.toString(), person );
    Assertions.assertThat( object ).isNull();

    //person 저장 검증
    object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( object ).isInstanceOf( Person.class );
    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( oldPersonId.toString() );
    }

    UUID newPersonId = UUID.randomUUID();
    person = Person.builder()
            .id( newPersonId )
            .personName( "new-name" )
            .personAge( 101 )
            .personNation( "korea" )
            .comment( "new commit data" )
            .build();
    //새로운 person 인스턴스를 저장하고 이전에 저장된 person 객체를 리턴한다.
    object = valueOperations.getAndSet( key.toString(), person );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( object ).isInstanceOf( Person.class );
    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( oldPersonId.toString() );
    }
}

 

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get 935d490f-ba15-4709-8ced-86606d0dd274
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"3430cf9b-a00d-4ed9-bd53-454cc70d2c9e\",\"personName\":\"new-name\",\"personAge\":101,\"personNation\":\"korea\",\"comment\":\"new commit data\"}"

 

setIfAbsent()

Boolean setIfAbsent(K key, V value) key가 없는 경우에 key와 함께 value를 저장한다.
key가 이미 존재하는 경우 false를 리턴한다.
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) key가 없는 경우에 key와 함께 value를 저장하고 만료시간을 지정한다.
key가 이미 존재하는 경우 false를 리턴한다.
default Boolean setIfAbsent(K key, V value, Duration timeout) key가 없는 경우에 key와 함께 value를 저장하고 만료시간을 지정한다.
key가 이미 존재하는 경우 false를 리턴한다.

 

@Test
@DisplayName( "setIfAbsent 테스트" )
void setIfAbsent_test() {
    final UUID key = UUID.randomUUID();
    final UUID personId = UUID.randomUUID();
    Person person = Person.builder()
            .id( personId )
            .personName( "test-name" )
            .personAge( 100 )
            .personNation( "korea" )
            .comment( "first commit data" )
            .build();

    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    //현재 key가 저장되지 않은 상태이므로 true를 리턴함.
    //setIfAbsent(key, value, timeout, timeunit) 만료시간을 지정할 수도 있다.
    //setIfAbsent(key, value, Duration.ofMillis(timeout)) 만료시간을 지정할 수도 있다.
    Boolean result = valueOperations.setIfAbsent( key.toString(), person );
    Assertions.assertThat( result ).isTrue();

    //person 저장 검증
    Object object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( object ).isInstanceOf( Person.class );
    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( personId.toString() );
    }

    UUID newPersonId = UUID.randomUUID();
    Person newPerson = Person.builder()
            .id( newPersonId )
            .personName( "new-name" )
            .personAge( 101 )
            .personNation( "korea" )
            .comment( "new commit data" )
            .build();
    //key가 이미 존재하므로 setIfAbsent()의 결과는 false가 됨.
    result = valueOperations.setIfAbsent( key.toString(), newPerson );
    Assertions.assertThat( result ).isFalse();
}

 

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get f1e1aed0-c6d5-4fc6-836e-492a81b1d09b
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"68be9061-ab4a-45af-a713-b762f32b65ed\",\"personName\":\"test-name\",\"personAge\":100,\"personNation\":\"korea\",\"comment\":\"first commit data\"}"

최초 person 인스턴스를 저장한 뒤에 동일 key에 newPerson 인스턴스를 setIfAbsent 메서드로 저장하면 이미 동일 키가 존재하므로 newPerson은 저장되지 않고 false를 리턴한다.

 

setIfPresent()

setIfAbsent() 메서드와 대조적인 역할을 한다. key가 존재하는 경우에만 데이터를 저장한다.

Boolean setIfPresent(K key, V value) key가 존재하는 경우에 value를 저장한다.
key 존재하지 않는 경우 false를 리턴한다.
Boolean setIfPresent(K key, V value, long timeout, TimeUnit unit) key가 존재하는 경우에 value를 저장하고 만료시간을 지정한다.
key가 존재하지 않는 경우 false를 리턴한다.
default Boolean setIfPresent(K key, V value, Duration timeout) key가 존재하는 경우에 value를 저장하고 만료시간을 지정한다.
key가 존재하지 않는 경우 false를 리턴한다.

 

@Test
@DisplayName( "setIfPresent 테스트" )
void setIfPresent_test() {
    final UUID key = UUID.randomUUID();
    final UUID personId = UUID.randomUUID();
    Person person = Person.builder()
            .id( personId )
            .personName( "test-name" )
            .personAge( 100 )
            .personNation( "korea" )
            .comment( "first commit data" )
            .build();

    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    //현재 key가 존재하지 않은 상태이므로 false를 리턴함. person 인스턴스는 저장되지 않음.
    Boolean result = valueOperations.setIfPresent( key.toString(), person );
    Assertions.assertThat( result ).isFalse();

    //key에 person 인스턴스를 저장
    valueOperations.set( key.toString(), person );

    //person 저장 검증
    Object object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( object ).isInstanceOf( Person.class );

    if ( object instanceof Person personInstance ) {
        Assertions.assertThat( personInstance.id().toString() ).isEqualTo( personId.toString() );
        Person modifiedPerson = personInstance.modifyPersonName( "modified-name" );
        result = valueOperations.setIfPresent( key.toString(), modifiedPerson );
        Assertions.assertThat( result ).isTrue();

        object = valueOperations.get( key.toString() );
        Assertions.assertThat( object ).isNotNull();
        Assertions.assertThat( object ).isInstanceOf( Person.class );

        Assertions.assertThat( ((Person) object).personName() ).isEqualTo( "modified-name" );
    }
}

 

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get e22ba5de-c280-4b52-ae0d-7b4c118c7c9e
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"b6ae745c-aead-4996-b57b-5ccf84ac4ff2\",\"personName\":\"modified-name\",\"personAge\":100,\"personNation\":\"korea\",\"comment\":\"first commit data\"}"

최초 person 인스턴스를 저장할 때 setIfPresent() 메서드를 통해서 저장하는 경우 key 값이 존재하지 않기 때문에 저장은 실패한다.

set() 메서드로 person 인스턴스를 저장한 뒤에 동일 키로 modifiedPerson 인스턴스를 setIfPresent()로 저장할 때는 키가 존재하므로 성공한다.

 

increment(), decrement()

정수형 혹은 부동 소수형 타입의 값을 원자적으로 증가 및 감소를 하는 메서드다.

Long increment(K key) key에 저장된 정수형 value의 값을 1 증가시킨다.
1을 증가시킨 최종값을 리턴한다.
Long increment(K key, long delta) key에 저장된 정수형 value의 값을 delta만큼 증가시킨다.
delta 만큼 증가시킨 최종값을 리턴한다.
Double increment(K key, double delta) key에 저장된 부동소수형 타입의 value 값을 delta만큼 증가시킨다.
delta 만큼 증가시킨 최종값을 리턴한다.
Long decrement(K key) key에 저장된 정수형 value 값을 1 감소시킨다.
1을 감소시킨 최종값을 리턴한다.
Long decrement(K key, long delta) key에 저장된 정수형 value 값을 delta 만큼 감소시킨다.
delta 만큼 감소시킨 최종값을 리턴한다.

 

@Test
@DisplayName( "increment 테스트" )
void increment_test() {
    final UUID key = UUID.randomUUID();
    final int value = 100;

    ValueOperations<String, Integer> valueOperations = integerRedisTemplate.opsForValue();
    valueOperations.set( key.toString(), value );
    Integer object = valueOperations.get( key.toString() );
    Assertions.assertThat( object ).isNotNull();
    Assertions.assertThat( value ).isEqualTo( object );

    final int delta = 10;
    //redis에 저장된 값을 10만큼 증가 시킴
    Long result = valueOperations.increment( key.toString(), delta );
    Integer incrementObject = valueOperations.get( key.toString() );
    Assertions.assertThat( incrementObject ).isNotNull();

    Assertions.assertThat( value + delta ).isEqualTo( incrementObject );
    Assertions.assertThat( result.intValue() ).isEqualTo( incrementObject );
}

Integer 타입의 값 처리를 위해서 Integer 타입 값 전용 integerRedisTemplate 빈을 사용하였다.

 

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get a874b65a-01e4-4373-950d-0e86969225a6
"110"

 

append(), size()

append()와 size() 메서드는 String 타입의 value 처리를 위해서 주로 사용된다.

Integer append(K key, String value) key에 저장된 값에 value 문자열을 추가한다.
추가된 문자열을 포함한 value의 길이를 리턴한다.
Long size(K key) key에 저장된 값의 크기를 리턴한다.

 

@Test
@DisplayName( "append and size 테스트" )
void append_test() {
    final String key = UUID.randomUUID().toString();
    final String value = "string value";
    final String append = ", appended data";

    ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
    valueOperations.set( key, value );
    String getValue = valueOperations.get( key );
    Assertions.assertThat( getValue ).isEqualTo( value );

    //문자열을 추가한다.
    //appendedSize는 추가된 문자열을 포함한 길이를 리턴한다.
    Integer appendedSize = valueOperations.append( key, append );
    getValue = valueOperations.get( key );
    Assertions.assertThat( getValue ).isEqualTo( "%s%s", value, append );

    Long size = valueOperations.size( key );
    Assertions.assertThat( size ).isEqualTo( String.format( "%s%s", value, append ).length() );
    Assertions.assertThat( size.intValue() ).isEqualTo( appendedSize );
}

String 타입의 값 처리를 위해서 String 타입 값 전용 stringRedisTemplate 빈을 사용하였다.

 

redis 저장 데이터는 다음과 같다.

127.0.0.1:6379> get 71acec45-3b30-4841-b654-dfc70acffa4a
"string value, appended data"

 

multiSet(), multiGet(), multiSetIfAbsent()

여러 개의 key, value를 한 번에 저장하고 검색하는 메서드다.

void multiSet(Map<? extends K, ? extends V> map) Map으로 전달된 key, value 데이터를 일괄 저장한다.
List<V> multiGet(Collection <K> keys) keys에 저장된 key list에 해당하는 값들을 List로 리턴한다.
key list 중에서 존재하지 않는 key에 대응되는 값은 null 로 셋팅된다.
Boolean multiSetIfAbsent(Map<? extends K, ? extends V> map) Map으로 전달된 key, value 데이터를 일괄 저장하지만 Map의 모든 key가 redis에 없어야 한다.
Map의 key list 중에서 이미 저장된 key가 존재하는 경우 저장은 실패하고 false를 리턴한다.

 

다음은 multiSet(), multiGet() 동작 확인을 위한 테스트 코드다.

@Test
@DisplayName( "multiSet and multiGet 테스트" )
void multiSet_and_multiGet_test() {
    List<String> keyList = List.of(
            UUID.randomUUID().toString(), 
            UUID.randomUUID().toString(), 
            UUID.randomUUID().toString()
    );

    List<UUID> personIdList = List.of(
            UUID.randomUUID(), 
            UUID.randomUUID()
    );

    Map<String, Person> personMap = Map.ofEntries(
            Map.entry( keyList.get( 0 ),
                    Person.builder()
                            .id( personIdList.getFirst() )
                            .personName( "test-name1" )
                            .personAge( 10 )
                            .personNation( "korea" )
                            .comment( "commit data1" )
                            .build() ),
            Map.entry( keyList.get( 1 ),
                    Person.builder()
                            .id( personIdList.getLast() )
                            .personName( "test-name2" )
                            .personAge( 11 )
                            .personNation( "America" )
                            .comment( "commit data2" )
                            .build() )
    );

    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    valueOperations.multiSet( personMap );

    //multiGet 메서드에 전달된 keyList에 있는 키 개수는 3개다.
    //multiSet 메서드를 통해서 저장된 personMap 은 2개다.
    //personList 크기는 keyList 수와 동일하지만 redis에 저장되지 않은 keyList에 있는 하나의 key에 대한 결과는 null 이다.
    List<Object> personList = valueOperations.multiGet( keyList );

    Assertions.assertThat( personList ).isNotNull();
    //존재하지 않는 key가 keyList에 포함되어 있는 경우 해당 value는 null로 출력된다.
    Assertions.assertThat( personList.size() ).isEqualTo( keyList.size() );
    Assertions.assertThat( personList ).contains( ( Object )null );

    personList.stream()
            .filter( Objects::nonNull )
            .forEach( person -> {
                Assertions.assertThat( person ).isInstanceOf( Person.class );
                Assertions.assertThat( personIdList.contains( ((Person) person).id() ) ).isTrue();
            } );
}

 

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

127.0.0.1:6379> get bf1e2f68-7e2c-417a-a972-ac1b52f532aa
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"68e5bef7-5e1d-483c-b164-2e9f78923c7f\",\"personName\":\"test-name2\",\"personAge\":11,\"personNation\":\"America\",\"comment\":\"commit data2\"}"
127.0.0.1:6379> get 72f664b0-6031-4c0d-9eb6-2e60aba594d5
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"96e1959c-5661-472d-809f-3a929f073940\",\"personName\":\"test-name1\",\"personAge\":10,\"personNation\":\"korea\",\"comment\":\"commit data1\"}"

 

다음은 multiSetIfAbsent() 동작 확인을 위한 테스트 코드다.

@Test
@DisplayName( "multiSetIfAbsent 테스트" )
void multiSetIfAbsent_test() {
    List<String> keyList = List.of(
            UUID.randomUUID().toString(), 
            UUID.randomUUID().toString()
    );
    
    List<UUID> personIdList = List.of(
            UUID.randomUUID(), 
            UUID.randomUUID()
    );

    //multiSetIfAbsent 호출시 이미 존재하는 key가 있는 경우 동작 확인을 위해 firstPerson 객체를 우선 저장한다.
    Person firstPerson = Person.builder()
            .id( personIdList.getFirst() )
            .personName( "test-name1" )
            .personAge( 10 )
            .personNation( "korea" )
            .comment( "commit data1" )
            .build();

    Person secondPerson = Person.builder()
            .id( personIdList.getLast() )
            .personName( "test-name2" )
            .personAge( 11 )
            .personNation( "America" )
            .comment( "commit data2" )
            .build();
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
    valueOperations.set( keyList.getFirst(), firstPerson );

    Map<String, Person> personMap = Map.ofEntries(
            Map.entry( keyList.getFirst(), firstPerson ),
            Map.entry( keyList.getLast(), secondPerson )
    );

    //multiSetIfAbsent로 전달되는 Map의 모든 Key가 존재하지 않아야 결과는 성공한다.
    Boolean result = valueOperations.multiSetIfAbsent( personMap );
    Assertions.assertThat( result ).isFalse();

    //multiSetIfAbsent 결과가 실패했으므로 keyList 에 대응하는 List 결과는 크기는 2 이지만 secondPerson은 저장되지 않았으므로
    //하나는 이미 저장되었던 firstPerson 인스턴스이고 나머지 하나는 null 이다.
    List<Object> personList = valueOperations.multiGet( keyList );

    Assertions.assertThat( personList ).isNotNull();
    Assertions.assertThat( personList.size() ).isEqualTo( keyList.size() );
    Assertions.assertThat( personList ).contains( ( Object )null );

    personList.stream()
            .filter( Objects::nonNull )
            .forEach( person -> {
                Assertions.assertThat( person ).isInstanceOf( Person.class );
                Assertions.assertThat( personIdList.contains( ((Person) person).id() ) ).isTrue();
            } );
}

 

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

127.0.0.1:6379> get 5bd042ed-86d5-4ec8-acb1-e4cc9bbf3bb1
"{\"@class\":\"com.example.spring.redis.model.Person\",\"id\":\"cef2701d-090f-4718-8373-b815b64afa3a\",\"personName\":\"test-name1\",\"personAge\":10,\"personNation\":\"korea\",\"comment\":\"commit data1\"}"

multiSetIfAbsent() 메서드 실행이 실패했기 때문에 처음에 저장된 firstPerson 인스턴스만 저장되었다.

 

setBit(), getBit()

setBit(), getBit() 메서드를 통해서 bit 배열 또는 boolean 플래그를 효율적으로 관리할 수 있다. 메모리 효율성이 중요하고 비트 집합에 대한 작업이 자주 발생하는 시나리오에서 주로 사용할 수 있다.

Boolean getBit(K key, long offset) key의 value에서 지정된 offset 위치의 bit의 boolean (0 또는 1) 값을 리턴한다.
Boolean setBit(K key, long offset, boolean value) key의 value에서 지정된 offset 위치의 bit에 value 값을 설정한다.
key가 존재하지 않으면 새 문자열 값이 생성된다.
문자열은 (offset * 8) + 1 byte 길이만큼 생성되며 초기 bit는 0으로 설정된다. offset 값은 0보다 크거나 같아야 하며 2^32 보다 작아야 한다. (비트맵은 512MB로 제한된다.)
offset 에 지정된 이전 bit의 값을 리턴한다.
만약 offset 값이 2^32 -1 값을 설정한 경우 key가 존재하지 않거나 key에 저장된 문자열이 작은 경우 Redis는 모든 중간 메모리를 할당해야 하므로 서버가 일정 시간 동안 차단될 수 있다. 2010MacBook Pro 기준 비트 번호 2^32-1(512MB 할당)을 설정하는데 ~300ms, 비트 번호 2^30 - 1(128MB 할당)을 설정하는데 ~80ms, 비트 번호 2^28 - 1(32MB 할당)을 설정하는데 ~30ms, 비트 번호 2^26 - 1(8MB 할당)을 설정하는데 ~8ms 가 소요된다.

 

byte 배열을 이용하여 setBit(), getBit() 동작을 확인하는 테스트 코드다.

//키에 저장된 문자열 값의 오프셋 비트를 설정하거나 지운다.
//Redis의 문자열은 바이너리 값에 안전하기 때문에 비트맵은 바이트 스트림으로 간단히 인코딩된다.
//문자열의 첫번째 바이트는 비트맵의 오프셋 0..7, 두번째 바이트는 8..15 범위 등에 해당된다.
@Test
@DisplayName( "setBit and getBit 테스트" )
void setBit_and_getBit_test() {
    ///////////////////////////////////////////////////////////////////////////////
    // "42"값을 가진 byte 배열을 저장 후 setBit() 메서드를 통해서 "44"값으로 변경하는 테스트
    ///////////////////////////////////////////////////////////////////////////////
    final String key = UUID.randomUUID().toString();
    String value = "42"; //0011_0100('4') 0011_0010('2')

    long setBitPosition = 13;
    long unsetBitPosition = 14;
    ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
    valueOperations.set( key, value );
    valueOperations.setBit( key, setBitPosition, true );
    valueOperations.setBit( key, unsetBitPosition, false );

    String getValue = valueOperations.get( key );
    Assertions.assertThat( getValue ).isEqualTo( "44" );


    ///////////////////////////////////////////////////////////////////////////////
    // 2개의 길이를 가진 byte배열(16bit)를 초기화 후 5번째 bit를 1로 변경 후 확인 테스트
    ///////////////////////////////////////////////////////////////////////////////
    //16개의 bit를 가지는 bit set을 정의. 초기 값은 모두 0.
    final String key2 = UUID.randomUUID().toString();
    //2byte(16bit) 의 공간을 0으로 초기화 하기 위함.
    byte[] bitArray = new byte[2];
    String bitString = new String( bitArray );

    setBitPosition = 5;
    valueOperations.set( key2, bitString );

    getValue = valueOperations.get( key2 );
    Assertions.assertThat( getValue ).isEqualTo( bitString );

    byte[] bytes = getValue.getBytes();
    Assertions.assertThat( bytes[0] ).isEqualTo( (byte) 0b0000_0000 );
    Assertions.assertThat( bytes[1] ).isEqualTo( (byte) 0b0000_0000 );

    Boolean bit = valueOperations.getBit( key2, setBitPosition );
    Assertions.assertThat( bit ).isEqualTo( false );

    valueOperations.setBit( key2, setBitPosition, true );
    bit = valueOperations.getBit( key2, setBitPosition );
    Assertions.assertThat( bit ).isEqualTo( true );

    //bit position이 5이므로 0..7 범위의 첫번째 바이트의 5번째 bit가 1로 변경된 것이다.
    getValue = valueOperations.get( key2 );
    bytes = getValue.getBytes();
    Assertions.assertThat( bytes[0] ).isEqualTo( (byte) 0b0000_0100 );
}

 

setBit()을 통해서 직접 bit를 설정하여 동작을 확인하는 테스트 코드다.

@Test
@DisplayName( "setBit and getBit 테스트" )
void setBit_and_getBit_test2() {
    final String key = UUID.randomUUID().toString();

    //초기 bitset의 크기는 (index / 8) + 1 byte로 (14 / 8) + 1 = 2 byte로 가장 큰 index 기준으로 동적으로 할당된다.
    ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
    //0011_0100('4') 0011_0010('2')
    valueOperations.setBit( key, 2, true );
    valueOperations.setBit( key, 3, true );
    valueOperations.setBit( key, 5, true );
    valueOperations.setBit( key, 10, true );
    valueOperations.setBit( key, 11, true );
    valueOperations.setBit( key, 14, true );

    String getValue = valueOperations.get( key );
    Assertions.assertThat( getValue ).isEqualTo( "42" );

    //2번째 bit가 true로 지정되었기 때문에 결과는 true.
    Boolean result = valueOperations.getBit( key, 2 );
    Assertions.assertThat( result ).isTrue();

    //4번째 bit는 true로 지정되지 않았기 때문에 결과는 false.
    result = valueOperations.getBit( key, 4 );
    Assertions.assertThat( result ).isFalse();
}

 

지금까지 Spring Data Redis 에서 지원하는 RedisTemplate의 ValueOperations 메서드에 대해서 알아보았다.

ValueOperations에 제공되는 메서드 목록은 아래 링크를 참고하기 바란다.

https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/core/ValueOperations.html

링크 페이지의 각 메서드 설명 항목에 보면 Redis Documentation 링크가 있는데 해당 링크를 참고하면 Redis Command를 이해하는데 많은 도움이 될 것이다.

 

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

끝.