Redis는 빠르고, 메모리 기반으로 동작하는 데이터 저장소로, 많은 프로젝트에서 유용하게 쓰이고 있다. Spring Data Redis를 사용하면 이런 Redis를 쉽게 통합할 수 있고, @RedisHash
는 자바 객체를 Redis 해시 구조에 매핑하는 걸 아주 쉽게 만들어 줘서 데이터를 직관적으로 다룰 수 있다. 특히, Spring Data의 CrudRepository 인터페이스와 함께 사용하면, 기본적인 CRUD 기능을 쉽게 구현할 수 있다.
하지만 예상하지 못한 명령어가 실행되거나 인덱싱을 위해 존재하는 삭제되지 않아 데이터가 무제한으로 쌓일 수 있다. 아래 예제로 주의해야하는 부분을 살펴보자.
@Getter
@RedisHash(value = "person")
public class Person {
@Id
private String id;
private String name;
@Indexed
private Integer age;
@TimeToLive
private long ttl = 60;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
Redis는 Primary Key 접근만 지원하여 구조가 간단하고 성능이 뛰어나지만, 특정 필드 검색 기능이 제한적이다. Spring Data Redis Repository는 이러한 한계를 Set을 사용해 극복한다.
데이터 저장 시 별도의 Set 에 key와 ID를 저장하고, 조회 시 해당 Set에서 데이터를 빠르게 검색한다. MySQL 인덱스를 떠올리면 될 것 같다.
@Test
void test() {
var personId = personRedisRepository.save(new Person("ho", 20)).getId();
personRedisRepository.findById(personId);
personRedisRepository.findAllByAge(20);
}
Hash 자료구조 저장 및 조회를 위해 HMSET
, HGETALL
이 수행될 것 같다. 인덱싱을 위한 Set 구조 저장 및 조회를 위해 SADD
, SINTER
도 수행이 될 것 같다.
$ redis-cli monitor
위 명령어를 통해서 수행되는 명령어 확인이 가능하다.
# save operation
"SISMEMBER" "person" "aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"DEL" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"HMSET" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60" "age" "20" "id" "aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60" "name" "ho" "ttl" "60"
"SADD" "person" "aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"EXPIRE" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60" "60"
"SADD" "person:age:20" "aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"SADD" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60:idx" "person:age:20"
# findById operation
"HGETALL" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"TTL" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
# findAllByAge operation
"SINTER" "person:age:20"
"HGETALL" "person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60"
"HGETALL" "person:38034ca4-039b-468a-9da9-8083237b74ca"
person:aa53cd7f-8a4e-4247-b6f3-ab43a8d7ad60:idx
)에 값(person:age:20
)을 추가한다.데이터를 저장하기 전에도 무조건 SISMEMBER, DEL 명령어가 수행된 후에 HMSET 이 수행되는 등의 불필요한 쿼리 발생으로 이어질 수 있으며, 성능에 영향을 줄 수 있다.
또한 인덱싱 기능을 사용하면, 저장과 조회 과정에서 SADD 등의 추가적인 명령어가 발생할 수 있다.
Hash 데이터는 TTL 이 만료되어 데이터가 삭제되었지만, SET 데이터는 삭제가 되지 않는 것을 알 수 있다.
Redis에서 기본적으로 TTL(Time-To-Live) 설정은 키 전체에 적용되며, 개별 필드에 TTL을 적용할 수는 없다.
TTL 설정이 전체 키에만 적용되기 때문에, Set에 속한 개별 요소들은 TTL이 적용되지 않는다. 즉, Set에 포함된 요소들은 Set 자체가 삭제되지 않는 한 계속해서 남아 있게 된다. 이로 인해 데이터가 계속 쌓여서 문제가 될 수 있다. 참고로 인덱싱을 위한 Set 의 TTL 설정은 만료기한이 없는 -1 이다.
이러한 문제를 해결하기 위해서는 TTL이 만료된 후에도 인덱스 Set을 정리할 수 있는 메커니즘이 필요하다. 이를 위해 Redis의 Keyspace Notifications를 활용할 수 있다. Keyspace Notifications를 통해 TTL 만료 이벤트를 수신하고, 해당 이벤트가 발생했을 때 인덱스 Set에서 만료된 객체의 ID를 제거하는 작업을 수행할 수 있다.
애플리케이션이 시작되면, Spring Data Redis는 PSUBSCRIBE 명령을 사용하여 keyspace 이벤트 패턴을 구독하고, Redis에서 키가 만료되거나 삭제될 때, 해당 이벤트가 발생하면 이를 Pub/Sub 메시지로 발행한다. Spring Data Redis는 Pub/Sub을 통해 이벤트를 수신하고, 수신된 이벤트를 기반으로 애플리케이션에서 만료된 키에 대한 인덱스 데이터를 제거한다.
$ redis-cli config set notify-keyspace-events Ex
Redis 에서 만료 이벤트가 발생할 때 이를 클라이언트에 알릴 수 있도록 설정하자.
@Configuration
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class RedisConfig {
}
Redis의 Keyspace Notifications 설정을 추가하자.
설정 모드 | 설명 |
---|---|
ON_STARTUP | 애플리케이션이 시작될 때 Redis keyspace 이벤트를 활성화한다. |
ON_DEMAND | Redis에서 첫 번째 키가 TTL(Time To Live)로 설정될 때까지 Redis keyspace 이벤트를 활성화하지 않는다. |
OFF | Redis keyspace 이벤트를 비활성화한다. |
"PSUBSCRIBE" "__keyevent@*__:expired"
"HGETALL" "person:d772a2b9-6d0c-4551-afcc-73f25997d372:phantom"
"DEL" "person:d772a2b9-6d0c-4551-afcc-73f25997d372:phantom"
"SREM" "person" "d772a2b9-6d0c-4551-afcc-73f25997d372"
"SMEMBERS" "person:d772a2b9-6d0c-4551-afcc-73f25997d372:idx"
"TYPE" "person:age:20"
"SREM" "person:age:20" "d772a2b9-6d0c-4551-afcc-73f25997d372"
"DEL" "person:d772a2b9-6d0c-4551-afcc-73f25997d372:idx"
person:d772a2b9-6d0c-4551-afcc-73f25997d372:phantom
, person:d772a2b9-6d0c-4551-afcc-73f25997d372:idx
키로 인덱스를 삭제person:d772a2b9-6d0c-4551-afcc-73f25997d372:idx
형식의 키는 조회에 사용되지도 않으면서 생성이 되는 이유가 궁금했는데, 인덱스 정리를 위해 필요한 부분이었다.
설정 모드 | 설명 |
---|---|
ShadowCopy.DEFAULT | 기본값으로, 섀도우 복사본을 사용하여 만료 이벤트를 처리해. 원본 데이터가 저장될 때 복사본을 생성하고, 복사본의 TTL을 원본 TTL보다 약간 더 길게(+5분) 설정한다. 이는 이벤트 처리를 안정적으로 하기 위함이다. |
ShadowCopy.OFF | 섀도우 복사본을 저장하지 않아. 저장 공간을 절약할 수 있지만, 이벤트 처리의 신뢰성이 떨어질 수 있다. |
나는 RedisTemplate으로 전환하여 해결하기로 했는데, 이유는 다음과 같다.
마스터/레플리카 구성을 사용하여 제공된 주소를 통해 RedisConnection을 설정하는 데 사용되는 RedisConnectionFactory 설정 클래스입니다. 예를 들어, AWS ElastiCache에 읽기 레플리카를 사용하여 연결할 때 적용됩니다.
마스터/레플리카 연결은 Pub/Sub 작업에 사용할 수 없다는 점에 유의하십시오.
fyi; spring data redis 공식문서
@RedisHash
정말 편하지만, 키스페이스 이벤트 비활성화 시에는 데이터가 계속 쌓일 수 있다.@RedisHash
CRUD Repository 메서드 실행 시에 어떤 명령어가 실행되는지 확인할 필요가 있다.