[ Spring Data Redis 적용기 ] : 2차 인덱스 이슈, expire, redis의 원자성과 spring data의 업데이트 메커니즘
[ Backtony blog ] Spring - Redis 연동하기 (@DataRedisTest 내용도 존재)
Redis supportConnecting to RedisRedisConnection, RedisConnectionFactoryLettuceConnectionFactory ConnectionPool 설정RedisTemplateRedisOperationsUsageRedisRepositoryGeneral RecommendationEmbedded Redis for testObject-to-Hash Mapping참조 객체를 영속화하기PipelineningSerializationredis-cliRedis에서 지원하는 타입별 조회방법@CacheableTroubleShootingqueryMethod 로 sort, limit 이용 시 테스트 오류 발생함
Redis support
Connecting to Redis
- Spring Data Redis는
Lettuce
와Jedis
(자바 오픈소스 라이브러리 for Redis) 를 사용함
- 대부분의 작업에서는 고수준의 추상화와 지원되는 서비스가 최적의 선택이지만, 때때로 저수준의 connection 을 이용한 직접 상호작용이 필요할 수도 있음
- 어떠한 라이브러리(
Lettuce
,Jedis
)를 사용하더라도RedisConnection,
RedisConnectionFactory
를 사용하면 됨
RedisConnection, RedisConnectionFactory
- IoC container에 의해 저장소와 연결될 때 사용됨
RedisConnectionFactory
에 의해 trasnparent exception translation 이 수행됨 (factory acts asPersistenceExceptionTranslator
)
@Configuration public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) { RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration( redisProperties.getHost(), redisProperties.getPort()); configuration.setPassword(redisProperties.getPassword()); configuration.setDatabase(redisProperties.getDatabase()); LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration .builder().useSsl().build(); // startTls() 메서드도 있는데 이건 제외했음 // redis 가 tls-auth-clients no 설정이 되어 있어서 redis-cli --tls 로만으로 접속가능해서 return new LettuceConnectionFactory(configuration, clientConfiguration); } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new StringRedisSerializer()); return template; } }
LettuceConnectionFactory ConnectionPool 설정
// Connection Pool 사용하지 않을 경우 LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() .clientName(clientName) .build(); // Connection Pool 사용할 경우 GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig(); genericObjectPoolConfig.setMaxTotal(10000); genericObjectPoolConfig.setMaxIdle(10000); genericObjectPoolConfig.setMinIdle(10); LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration .builder() .clientName(clientName) .poolConfig(genericObjectPoolConfig) .build();
- connectionFactory.setShareNativeConnection(true/false) 설정: 기본값은 true입니다.
- 얼마나 많은 커넥션이 만들어져서 풀링이 되던 상관없이 연산들이 non-blocking이고 non-transactional operation이라면 하나의 커넥션만을 계속해서 이용
RedisTemplate
RedisConnection
은 레디스와 상호작용할 수 있는 저수준의 메서드를 제공하는 반면,RedisTemplate
은 고수준의 메서드를 제공함
- Helper class that simplifies Redis data access code.
- 참고 RedisCommands : serialize를 직접 정의해주어야함
- 참고 RedisOperations : serialize default 이용해서 알아서 수행해줌
RedisTemplate은 serialization 과 연결 관리를 수행함
enableDefaultSerializer
를false
로 둠으로써 serializer를 null로 둘 수도 있음
- DefaultSerializer :
JdkSerializationRedisSerializer
- Java based serializer를 대부분의 연산에서 사용하게 됨 → template에 의해 읽히거나 쓰이는 모든 object는 java에 의해 직렬화/역직렬화 되는 것

RedisTemplate XXXOperations에서 쓰이는 역할별 Serializer (AbstractOperations
class 참고)

@Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new StringRedisSerializer()); return template; } @Test void name() { Room room = RoomFixture.createRoom(1L, "메타버스1팀", 10, 50); HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash(); Map<String, String> map = Map.of("test", "value"); hashOperations.putAll("room:1", map); }
keySerializer
는 “room:1”을 serialize
hashKeySerializer
는 “test”를 serialize
hashValueSerializer
는 “value”를 serialize
RedisOperations
HashOperations (map specific operations working on a hash)
@Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } private final HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash(); @Test void save() { Room room = RoomFixture.createRoom(1L, "메타버스1팀", 10, 50); Map<String, String> map = objectMapper.convertValue(room, Map.class); map.replaceAll((k, v) -> String.valueOf(v)); hashOperations.putAll("room:1", map); } @Test void find() { Map<String, String> hashes = hashOperations.entries( "room:%d".formatted(roomId)); Room room = objectMapper.convertValue(hashes, Room.class); } @Test void delete() { redisTemplate.delete("room:1"); // Operations 쪽에는 hash key 자체를 한번에 날리는 // 메서드를 찾지 못함 }
SetOperations (Set 값에 대한 operation)
private final SetOperations<String, String> setOperations = redisTemplate.opsForSet(); @Test void readValues() { String setHashKey = "room:channelServer:2"; Set<String> members = setOperations.members(setHashKey); } @Test void addValue() { setOperations.add("room:channelServer:2", "2"); } @Test void removeValue() { setOperations.remove("room:channelServer:2", "2"); }
ZSetOperations (SortedSet 에 대한 operation)
Usage
spring: datasource: url: jdbc:sqlserver://localhost:1433;databaseName=virtual_office;encrypt=true;trustServerCertificate=true username: sa password: MyPassword@ redis: port: 6379 host: localhost
RedisProperties
에서spring.redis
prefix로 autoconfigure 해줌
RedisRepository
@Configuration public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) { return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); } @Bean public RedisTemplate<byte[], byte[]> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<byte[], byte[]> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } }
@RedisHash("people") public class Person { @Id String id; String firstname; String lastname; Address address; }
- entity에 @RedisHash 어노테이션 + @Id 어노테이션 ⇒ 두 개의 어노테이션이 hash를 영속화하기 위해 사용되는 실제 key 생성을 가능케 해줌
public interface PersonRepository extends CrudRepository<Person, String> { }
QueryMethod — find 메서드에서 이용되는 프로퍼티는 model 에서 @Indexed 어노테이션 붙여주어야 함
public interface PersonRepository extends CrudRepository<Person, String> { List<Person> findByFirstname(String firstname); }
- Please make sure properties used in finder methods are set up for indexing.
Sorting Query Method
- 레디스 자체로는 sorting을 지원하지 않기 때문에 Redis Repository는
Comparator
를 구성하여 내부적으로 정렬을 하고 List를 반환함
interface PersonRepository extends RedisRepository<Person, String> { // Static sorting derived from method name. List<Person> findByFirstnameOrderByAgeDesc(String firstname); // Dynamic sorting using a method argument. List<Person> findByFirstname(String firstname, Sort sort); }
General Recommendation
- Immutable Object로 만들어라! (
@PersistenceCreator
어노테이션 활용해서 가능함)
package com.uplus.virtualoffice.model; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; import lombok.Getter; @Getter @RedisHash("room") public class Room { @Id private final Long id; @Indexed private final Long roomNo; @Indexed private final Long channelId; private final String roomName; private final int userCnt; private final int maxUserCnt; public Room(Long id, Long roomNo, Long channelId, String roomName, int userCnt, int maxUserCnt) { this.id = id; this.roomNo = roomNo; this.roomName = roomName; this.channelId = channelId; this.userCnt = userCnt; this.maxUserCnt = maxUserCnt; } }
Embedded Redis for test
Embedded redis 의존성 추가
testImplementation 'it.ozimov:embedded-redis:0.7.2'
Embedded Redis Server Configuration 정의
package com.uplus.virtualoffice.repository; import java.io.IOException; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.test.context.ActiveProfiles; import redis.embedded.RedisExecProvider; import redis.embedded.RedisServer; @TestConfiguration @ActiveProfiles("test") public class TestRedisConfiguration { private final RedisServer redisServer; public TestRedisConfiguration(RedisProperties redisProperties) throws IOException { RedisExecProvider provider = RedisExecProvider.defaultProvider(); this.redisServer = new RedisServer(provider, redisProperties.getPort()); } @PostConstruct public void postConstruct() { this.redisServer.start(); } @PreDestroy public void preDestroy() { this.redisServer.stop(); } }
사용하려는 테스트에서 해당 Configuration을 Component로서 ApplicationContext에 추가
@SpringBootTest(classes= TestRedisConfiguration.class) class ChannelServerRepositoryTest { @Autowired private ChannelServerRepository repository; ... }
발생한 이슈 : embedded redis가 안 뜨는 것같다. (github action에서)
- 그래서 log를 보려고 하니, ./gradlew test 에서는 로그가 안떠서, build.gradle 수정하여 로그 출력되도록 함
test { testLogging.showStandardStreams = true }
- Embedded redis랑 전혀 관련 없는 문제로,
@SpringBootTest
어노테이션으로 applicationContext 만들어서 application 실행 시, test 시에 사용하는 스키마(virtual-office-test
)가 아닌 일반 스키마(virtual-office
)로 connection 요청을 하여서Failed to load application context
에러가 발생했던 것
Object-to-Hash Mapping
Entity를 Hash로 바꿀 때, 아래와 같이 데이터가 저장되는데 이 매핑되는 방식을 Customizing이 가능함(Converter
인터페이스를 구현함으로써)
_class = org.example.Person (1) id = e2c7dcee-b8cd-4424-883e-736ce564363e firstname = rand (2) lastname = al’thor address.city = emond's field (3) address.country = andor
- Object를 hash 형태로(field:value 의 맵 형태)로 serialize 하는 방법
- RedisRepository 이용하면 default로 클래스를 hash 형태로 serialize
ObjectHashMapper 를 사용하여 클래스를 hash 형태로 serialize
public class HashMapping { @Autowired HashOperations<String, byte[], byte[]> hashOperations; HashMapper<Object, byte[], byte[]> mapper = new ObjectHashMapper(); public void writeHash(String key, Person person) { Map<byte[], byte[]> mappedHash = mapper.toHash(person); hashOperations.putAll(key, mappedHash); } public Person loadHash(String key) { Map<byte[], byte[]> loadedHash = hashOperations.entries("key"); return (Person) mapper.fromHash(loadedHash); } }
Object를 map으로 바꾼뒤 HashOperations를 이용하여 hash 형태로 serialize
참조 객체를 영속화하기
_class = org.example.Person id = e2c7dcee-b8cd-4424-883e-736ce564363e firstname = rand lastname = al’thor mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 (1)
- 다른 객체에 대한 Reference를 위와 같이 저장할 수도 있음(JPA에서 다른 entity를 현재 entity에서 참조하듯이)
Pipelinening
- 다수의 커맨드를 서버에 한번에 보낸 뒤(응답을 기다리지 않고) 그 후, 응답을 한번에 다 받는 방식으로 진행됨
//pop a specified number of items from a queue List<Object> results = stringRedisTemplate.executePipelined( new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisConnection stringRedisConn = (StringRedisConnection)connection; for(int i=0; i< batchSize; i++) { stringRedisConn.rPop("myqueue"); } return null; } });
- RedisCallaback에서 반환되는 값은 null 이어야 함. 이 값은 버려지고, pipeline command의 결과값이 반환됨
Serialization

TIL/Spring/Redis/redis serializer/serializer.md at master · binghe819/TIL
📚 Today I Learned. 기록하자. Contribute to binghe819/TIL development by creating an account on GitHub.
Jackson2JsonRedisSerializer
127.0.0.1:6379> hgetall job:1 1) "id" 2) "\"1\"" 3) "createTime" 4) "1455778716799" 5) "submitterName" 6) "\"Jon Snow\"" 7) "jobDef" 8) "{\"def\":\"nightwatch\"}"
GenericJackson2JsonRedisSerializer
127.0.0.1:6379> hgetall job:1 1) "id" 2) "\"1\"" ... 7) "jobDef" 8) "{\"@class\":\"java.util.LinkedHashMap\",\"def\":\"nightwatch\"}"
DecoratingStringHashMapper
127.0.0.1:6379> hgetall job:1 1) "id" 2) "1" 3) "createTime" 4) "1455780810643" 5) "submitterName" 6) "Jon Snow" 7) "jobDef" 8) "{def=nightwatch}"
redis-cli
Redis에서 지원하는 타입별 조회방법
[ Redis Command ]

@Cacheable
TroubleShooting
queryMethod 로 sort, limit 이용 시 테스트 오류 발생함
public interface ChannelServerRepository extends CrudRepository<ChannelServer, Long> { Optional<ChannelServer> findTopByOrderByCurrentConnectionAsc(); List<ChannelServer> findAllByOrderByCurrentConnectionAsc(); }
- 위의
findTopByOrderByCurrentConnectionAsc()
에서 connection 갯수가 가장 작은 것을 반환해야 하는데, 어떤 경우는 가장 작은 것을 반환하고 어떤 경우는 가장 큰 값을 반환함(sort의 asc, desc 이것이 한번씩 바뀌는거같음;). 이유를 알 수가 없음 - redis 내부적으로는 sort를 지원하지 않아서 결과를 반환하기 전에 Comparator 를 구성해서 적용하여 List로 반환을 함 [ Spring data redis Docs 참고 ]
- Therefore, Redis repository query methods construct a
Comparator
that is applied to the result before returning results asList
. Let’s take a look at the following example:
- ⇒ List 를 반환하는 경우는 sort 이상이 없어서 이걸 사용함