🫠 실패에 대한 짧은 결론 : 레디스는 인메모리 DB로써 빠른 IO를 요구하는 작업에 최적화 되어있다. 예를 들면 세션 클러스터링이나 캐싱에 많이 사용된다. 그러나 Entity를 계층 구조로 만들고 깊은 내부의 있는 필드에 Document나 RDB 하듯 LIKE 쿼리를 구현하는 것은 바람직하지 않아보인다. 물론 단순한 구조의 Entity에 id필드를 지정하고 id를 가지고 직렬화/역직렬화 하는 것은 가능하다. 결론적으로 key로 value에 바로 접근할 수 있는 Set이나 Hash구조 일 때 의미가 있어보인다.
하려고 했던 것
웹 기반의 채팅 프로그램을 설계 하면서 publisher가 던지는 채팅을 일정시간 저장할 용도의 NoSQL을 물색하고 있었다. 단순히 Redis가 생각나서 RDB를 생각하며 접근했다. 복잡한 쿼리는 RedisTemplate으로 던지고 간단한 CRUD는 Spring Data Redis로 해결하려 했다.
당연히 메모리 기반인것도 알고있고 원하면 디스크에 저장할 수 있는 것도 사전에 알고 있었다. 바로 프로젝트에 구현하기 전에 간단한 CRUD와 LIKE 검색 정도는 구현해보고 진행하려 했다. 예제는 상품 CRUD였고 상품명으로 LIKE검색할 수 있는 RestController를 만들려고 했다. 문제는 LIKE 검색이였는데 여기서 Redis 자료구조에 대한 이해가 없어 삽질을 상당히 오랜시간 했다.
스프링 부트에 도커로 Redis 로컬을 올리고 진행했다.
어디까지 가봤니
동시에 여러가지를 찾느라 순서는 의미없지만 자의적인 해석을 담아 적어왔다. value를 대상으로 검색하는 것은 hscan
을 사용 한다고 한다. hscan
의 기본 쿼리는 아래와 같다
hscan [key] [cursor] [match] [pattern]
LIKE 쿼리 시 %검색%
하듯 pattern
에 *검색*
을 넣으면 된다고 한다.
Spring Data Redis
CrudRepository
에서 @Id
로 key를 지정해 Entity를 save()
하면 기본적으로 Set 타입으로 저장된다. 직렬화 되는 방식은 여러가지가 있지만 역직렬화 하기 전엔 값을 알 수 없는 것이 일반적이다. 그렇다면 Set 타입에서 hscan
을 사용 할 수 없나? Redis의 대표적인 자료구조 4개 – String, List, Set, Hash – 를 알고 나서 RedisTemplate으로 선회하여 Hash 타입으로 직접 저정하기로 한다.
RedisTemplate
opsForHash()
호출 후 Entity를 저장해본다. 이때부터 조회되지 않는 Redis를 탓하며 redis-cli
에 들어가 keys와 scan만 죽어라 조회한다. 직렬화 된 데이터에서 hscan의 match가 성사될리가 없다. @Indexed
를 추가해보지만 이렇게 하니 Entity의 Hash와 인덱싱된 상품명의 Hash가 둘 다 DB에 추가된다. 이러면 상품의 Entity 전체를 저장하는 의미가 없다. 여기서 오기가 생겼다. (여기서 멈췄어야 했다. 🤦♂️)
@Repository
public class ProductRedisTemplateRepository {
private final HashOperations<String, String, Product> hashOps;
//...
public List<Product> findByNameLike(String pattern) {
String key = "product";
List<Product> results = hashOps.scan(key, ScanOptions.scanOptions().match("*" + pattern + "*").build())
.stream().map(Map.Entry::getValue)
.collect(Collectors.toList());
return results;
}
//...
}
Redis의 자료구조
- String : 기본적인 1:1 k-v 구조의 타입이다.
- List : k-v가 1:N으로 구성되어있다.
- Set : List와 마찬가지로 1:N 구조지만, value가 중복되지 않는다.
- Hash : 하나의 key에 여러개의 field와 value로 구성된다.
Redis의 대표적인 scan
- scan : 데이터 일괄 조회. 커서를 두고 limit offset 같이 검색
- sscan : key를 기반으로 match검색이 가능한 조회.
- hscan : field를 기반으로 match검색이 가능한 조회.
실패 인정하기
슬슬 이상하게 돌아간다는걸 머리로는 알았지만 그동안 삽질한 노력을 가슴은 포기할 수 없다 하여 온갖 Redis 쿼리를 찾아보기 시작한다. 이때쯤 되서 Redis에는 자료구조라는게 있다는걸 알았고 위에서 시도한 것들을 N번씩 반복하기 시작한다. Redis prune – Entity 수정 – RedisTemplate 수정 – 테스트 코드 실행 – redis-cli 들어가서 직접 조회 … 의 무한 반복이였다.
머릿말에 적당히 결론을 적어놨지만 일단 직렬화 자체도 다른방식으로 시도했었다. json으로도 넣어보고 entity맴버 자체로 field-value 방식으로도 넣어봤다. 그러나 Entity내의 필드 자체가 k-v의 1:1구조가 되지 않는 이상은 불가능한 것으로 판단했다.
용도에 맞게 쓰기
마치 스포츠카로 험준한 산맥에서 오프로드를 하려고 하고, 슈퍼스트렛 기타로 하와이 해변가에서 트로피칼 레게를 연주하려고 했다. 약간의 미련이 남은 상태에서 MongoDB를 사용해 Redis에 들인 노력과 시간의 1/10 수준으로 CRUD와 LIKE검색에 성공하고 그대로 성불해버렸다.
그래서 MongoDB를 쓰기로 했는데 아무래도 디스크 기반 DB다 보니 인덱싱을 어떻게 할지 고민하고 HA 클러스터링에 대해 좀 더 알아봐야겠다. 잠깐 찾아보니 복제와 샤딩에 대해 지원하는 것 같긴 한데..