1. Redis connection
SpringBoot 로 작성한 어플리케이션에서 redis 와 통신하기 위해 사용할 수 있는 방법은 크게 2가지가 있다.
1. RedisTemplate
2. RedisRepository
1번은 spring data redis 에서 redis client 를 사용하여 java 객체들이 redis 와 연결을 맺고 데이터를 받아올 수 있는 통로같은 객체이다.
1번은 redis 가 제공하는 crud, 캐싱, pub/sub 등 다양한 기능을 사용하기 위해 redis cli 를 java 함수적으로 접근할 수 있게 구현한 것이며, 공식문서에서는 "자바 기반의 serializer 로 데이터를 읽어 직렬화 또는 역직렬화를 한다." 라고 설명하고 있다. redis server 에 대한 정보를 가지며 어플리케이션과 상호작용하기 위해 사용하는 객체이다.
2번은 단순한 crud 를, 어플리케이션에서 으레 사용하던대로 사용하기 위해 쿼리메소드를 제공하는 방식이라고 한다.
나는 처음 사용할 때부터 RedisTemplate 를 사용하기도 했고, 여기에서 제공하는 CRUD 함수들에 대해 정리하고자 RedisTemplate 을 중심으로 설명하고자 한다.
2. 다양한 자료구조
CRUD 의 코드적 구현을 설명하기 이전에, Redis가 구현하는 다양한 자료구조를 톺아보고 가자. 단순히 테이블로 데이터를 저장하는 RDB와 다르게, Redis 는 NoSQL 로서 다양한 자료구조로 데이터를 저장하는데, 때문에 상황에 맞게 데이터를 효율적으로 모델링할 수 있고 접근 속도도 빨라진다는 NoSQL 로서의 장점을 충분히 구현하고 있다.
Redis 에서 제공하는 자료구조는 String, List, Set, Hash, Sorted Set 이 있다.

1) ArrayList 와 LinkedList 의 차이 : ArrayList 보다 LinkedList 가 삽입이 쉬운데, 실제 디스크에 저장될 때, 전자는 자료들을 연속적으로 저장하고, 후자는 각 요소들을 불연속적으로 저장하여 저장된 요소들을 연결하는 형태로 구현되었다. LinkedList 자체로 사용되기 보다는, 연결을 맺고 끊는 데 수월한 장점을 사용해 deque 를 구현하는 데 쓰인다고 한다.
2) Sorted Set 은 뭔가? : set 은 list 와 다르게 중복을 허용하지 않고 인덱스가 없다. Sorted Set 은 요소들을 트리 형태로 구현하여 순서를 갖는 set 을 만드는 것이다. 실질적으로 쓰임이 다른데, list 는 삽입된 순서가 중요하다면, Sorted Set 은 자료 내부에서의 순서가 중요하다. 따라서 list 는 로깅에 적합하고, Sorted Set 은 실시간 순위를 매기는 것 등에 자주 사용된다.
* 추가적으로, set 은 해당 요소의 유무를 단시간에 찾을 수 있다는 특징이 있는데, 해시 테이블을 사용해서 항목의 존재 여부를 파악할 수 있다. 따라서 HashSet 이 기본 구현체다.
3) LinkedList 와 Tree 의 차이는? : 둘 모두 '순서'를 구현하는 방법이지만, linked list 는 탐색이 느린 대신 삽입 삭제가 빠르다면, tree 는 탐색이 빠른 대신 삽입 삭제가 조금 느린 편이라고 할 수 있다. 빠른 조회가 필요하다면 Tree 형태가 좋을 수 있다.
4) Map 이 해시테이블로 저장되는 방식 : set 의 hash 저장 value 와 다르게, map 에서는 필드-값 쌍을 저장한다. map 에서는 필드 이름을 해싱하고 유무 판별도 필드 이름으로 하게 된다.
3. Redis Template 의 CRUD
자료구조에 따라 다른 명령어가 사용된다고 한다. 더 많은 자료구조와 명령어가 있지만, 간단하게 대표적인 것 몇 가지만 정리하겠다.
1) String
key - String value (하나의 키에 하나의 문자열을 저장할 수 있는 형태) 특정 사용자의 세션 데이터를 저장하고 조회할 때 자주 사용된다.
// create/update
set user:1 "iammindy"
// read
get user:1
// delete
del user:1
set, get, del 연산자를 사용하며, user 는 데이터베이스이며 1은 키다.
ValueOperations<String, String> valueOps = redisTemplate.opsForValue(); // 각 타입에 대한 메서드를 정의한다.
valueOps.set('key", "value"); // 저장
String value = valueOps.get("key"); // 조회
데이터베이스는 RedisConnectionFactory 에서 db 번호를 지정하며 RedisTemplate 은 명령과 해당 데이터베이스의 직렬화/역직렬화를 담당하는 객체이다.
캐싱하면 좋을 긴 문자열을 저장해도 좋고, 나의 경우 프로젝트에서 만료된 jwt 의 블랙리스트로 사용했다.
2) Set
set 자료구조는 순서가 없으며 중복도 없다.
sadd, smembers, sismember, spop 등의 연산이 있다. 요소들을 'member' 라고 한다.
SetOperations<String, String> setOps = redisTemplate.opsForSet();
setOps.add("mySet","value1","value2"); // mySet 이라는 키로 value1, value2 를 저장한다.
// 집합 myset 의 모든 요소를 가져온다.
Set<String> members = setOps.members("mySet"); // key 에 해당하는 모든 멤버 가져오기
setOps.remove("mySet", "value1"); // 요소 삭제
boolean = isMember = setOps.isMember("mySet", "value1"); // member 여부 확인
이것을 활용하는 방법으로, 좋아요와 같이 단순하지만 빠른 조회가 요구되는 상황에서,
순서가 필요 없이 누가 post 에 좋아요를 눌렀는지 여부를 확인하면 되므로 set 을 사용해서 구현할 수 있다!
3) Sorted Set
Tree 형태로 구현되어 있어 순서가 있는 set 자료형이다. tree 를 만들기 위해 score 라는 가중치를 같이 넣으며, score 의 오름차순으로 내부 정렬이 된다고 한다. set 이기 때문에 value는 중복될 수 없지만, score는 가능하다.
cli 명령어로 ZADD, ZRANGE, ZPOPMIN, ZPOPMAX, ZREM(삭제) 등의 연산이 있다.
ZSetOperations<String, String> zsetOps = redisTemplate.opsForZSet();
zsetOps.add("myZSet", "value1", 1.0) // myZSet key를 가진 sorted set 자료구조에서 "value1" 멤버에 1.0 가중치를 부여해 add 한다
Set<String> range = zsetOps.range("myZSet",0,-1); // 정렬된 key 집합 의 모든 요소를 가져온다.
// 범위 내의 요소를 가져온다.
Set<String> rangeByScore = zsetOps.rangeByScore("myZSet", 0.0, 10.0); // ['value1']
zsetOps.remove("myZSet", "value1") // 삭제
실시간 검색어, 게임 순위와 같이 실시간으로 변하는 순위를 매겨야 하는 상황에서 rdbms 로 구현하면 매우 부하가 커지고, 삽입된 순서 자체는 상관이 없지만 내부적으로 score 를 통해 등수를 내야 하는 상황에서 sorted set 자료구조를 이용할 수 있다.
4) Hash
hash 테이블을 사용하여 내부에 또 field-value 구조의 데이터를 저장한다. python으로 말하면 dictionary 구조이다. key는 중복해서 사용할 수 없는 용어이므로 'field' 라고 하며, 이론적으로는 메모리 제한이 없다고 한다.
hset, hdel, hget, hlen(필드 개수 조회), hvals(key에 속한 모든 value를 조회) 등의 명령어가 있다.
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash(); // 해시 이름, 필드 이름, 값
hashOps.put("myHash", "field1", "value1"); // myHash 라는 키에, 필드와 value를 넣는다.
String value = hashOps.get("myHash", "field1"); // 키와 필드로 value 조회
Map<String, String> entries = hashOps.entries("myHash") // dic 형태의 hash 가 나온다.
hashOpse.delete("myHash", "field1"); // 삭제
레디스를 캐싱으로 사용할 수 있는 좋은 이유 중 하나는, 객체의 데이터를 redis 에서 hash 자료형으로 넣게 되면, redis를 먼저 조회하고, 없으면 rdbms 에서 찾는 캐싱 기능을 사용할 수 있다. entity class 에 @RedisHash를 붙여 해당 class 이름을 기반으로 객체의 필드를 결정하고, 객체의 데이터를 직렬화하여 하나의 value로 저장하고 조회해올 수 있다.