Redis(Remote Dictionary Server)는 Java의 Map과 같이 Key-Value 구조의 NoSQL입니다.
💡 NoSQL?
NoSQL은 SQL을 사용하는 관계형 데이터베이스에 대응하는 비관계형 데이터베이스를 의미합니다. Not only SQL을 줄여서 NoSQL 입니다. 테이블에 필드와 레코드로 데이터를 표현하는 RDB(Relational Database)와는 달리 NoSQL은 SQL을 사용하지 않으며 자유로운 형식을 가집니다. RDB의 약점인 확장성, 유영성, 성능에 대한 문제를 해결하기 위해 사용되는 경우가 많습니다.
Redis 역시 NoSQL이며 동시에 인메모리 데이터베이스(In-memory database)입니다. 인메모리 DB는 이름 그대로 저장 장치에 데이터를 저장하지 않으며 컴퓨터의 메인 메모리, 즉 RAM에 데이터를 저장하는 DB입니다. 매우 빠른 속도를 가진 대신 영속성이 없다는 특징이 있습니다.
Redis에는 String, List, Set, Sorted Set, Hash로 총 5가지의 기본 데이터 타입이 있습니다. 이 데이터 타입들을 알아봅시다.
가장 기본적인 자료형이며, Redis가 Java의 Map<String, String> 처럼 동작한다고 생각하면 됩니다. 저장할 수 있는 최대 크기는 512MB입니다. String에는 다음과 같은 명령어들이 있습니다.
SET <key> <value>: key에 value 문자열 데이터를 저장, "으로 공백 구분GET <key>: key에 저장된 문자열 반환만약 저장된 데이터가 정수 데이터라면 데이터를 바로 증가, 감소가 가능합니다.
INCR key: key에 저장된 데이터를 1 증가DECR key: key에 저장된 데이터를 1 감소여러 Key - Value를 한번에 다루고 싶다면 MSET, MGET을 활용할 수 있습니다.
MSET key value [key value …]: key value의 형태로 주어진 인자들을 각 key에 value를 저장MGET key [key]: 주어진 모든 key에 해당하는 데이터를 반환.문자열은 결국 바이트 배열입니다. 그래서 이미지, 음성, 영상, 파일등도 보관이 가능합니다.
여러 문자열 데이터를 Linked List 형태로 보관하는 데이터 타입입니다. 스택이나 큐로 활용이 가능합니다.
LPUSH key value: key에 저장된 리스트의 앞쪽에 value를 저장RPUSH key value: key에 저장된 리스트의 뒤쪽에 value를 저장LPOP key: key에 저장된 리스트의 앞쪽에서 값을 반환 및 제거RPOP key: key에 저장된 리스트의 뒤쪽에서 값을 반환 및 제거Redis는 Key - Value 구조의 DB입니다. 즉 Map<String, List<String>>의 형태로 사용한다고 생각하면 됩니다.
리스트를 사용하면서 흔히 사용하는 길이 구하기, 범위 내 원소 반환하기 등의 기능도 제공합니다.
LLEN key: key에 저장된 리스트의 길이를 반환LRANGE key start end: key의 start부터 end까지 원소들을 반환LRANGE의 경우 end가 List의 길이를 넘더라도 오류가 발생하지 않고 end 까지의 데이터를 반환합니다. start > end의 경에는 빈 결과를 반환합니다. 음수의 경우 파이썬의 리스트와 같이 인덱스의 역순을 결과로 반환합니다.
문자열의 집합입니다. 중복값을 허용하지 않으며 순서가 존재하지 않습니다.
SADD key value: key에 저장된 집합에 value를 추가SREM key value: key에 저장된 집합의 value를 제거SMEMBERS key: key에 저장된 집합의 모든 원소를 반환SISMEMBER key value: key에 저장된 집합에 value가 존재하는지 반환SCARD key: key에 저장된 집합의 크기를 반환여러개의 Set에 교집합, 합집합 등의 기능도 제공합니다.
SINTER key1 key2: key1과 key2에 저장된 집합들의 교집합의 원소들을 반환SUNION key1 key2: key1과 key2에 저장된 집합들의 합집합의 원소들을 반환SINTERCARD number key1 [key2 ...]: number개의 key에 저장된 집합들의 교집합의 크기를 반환SISMEMBER 의 경우 O(1)의 시간복잡도를 가지고 있어서 인증 토큰 블랙리스트 등을 구현할 때 활용하기 좋습니다.
이름 그대로 정렬된 집합입니다. 기본적으로는 Set과 같이 중복되지 않는 값들을 다루지만 여기에 score라는 실수를 함께 보관하여 score를 바탕으로 값들을 정렬합니다.
ZADD key score member [score member ...]: key의 Sorted Set에 score를 점수로 가진 member를 추가, 이미 있는 member의 경우 새로운 score를 설정ZRANK key member: key의 Sorted Set의 member의 순위를 오름차순 기준으로 0에서 부터 세서 반환ZRANGE key start stop: key의 Sorted Set의 member들을 start 부터 stop 순위까지 오름차순 기준으로 반환ZREVRANK key member: key의 Sorted Set의 member의 순위를 내림차순 기준으로 0에서 부터 세서 반환ZREVRANGE key start stop: key의 Sorted Set의 member들을 start 부터 stop 순위까지 내림차순 기준으로 반환ZINCRBY key increment member: key의 Sorted Set의 member의 score를 increment 만큼 증가 (음수를 전달하면 감소)대표적으로 리더보드나 Rate Limiter(API 등을 제공할 때 짧은 시간에 지나치게 많은 요청을 막기 위한 기능) 등의 기능을 만드는데 사용합니다.
Field - Value 쌍으로 이루어진 데이터 타입입니다. 즉, Map<String, Map<String, String>>의 형식이라고 생각할 수 있습니다.
HSET key field value [field value]: key의 Hash에 field에 value를 넣는다. 한번에 여러 field - value 쌍을 넣어줄 수 있다.HGET key field: key에 저장된 Hash의 field에 저장된 value를 반환. 없는 field의 경우 null.HMGET key field [field]: key에 저장된 Hash에서 복수의 field에 저장된 value를 반환.HGETALL key: key에 저장된 Hash에 저장된 field - value를 전부 반환.HKEYS key: key에 저장된 Hash에 저장된 field를 전부 반환HLEN key: key에 저장된 Hash에 저장된 field의 갯수를 반환Hash는 하나의 키에 복잡한 데이터를 저장하는 용도로 활용되고 공식 문서에서도 여러 Key에 걸쳐 데이터를 표현하기 보다는 Hash를 자주 활용할 것을 권장하고 있습니다. Redis를 Spring과 연계하여 사용할 때 Key에 user의 id, Value에 user의 Entity를 저장하는 식으로 활용할 수 있습니다.
DEL key: key(와 저장된 데이터)를 제거EXPIRE key seconds: key의 TTL(유효시각)을 seconds로 설정, seconds초가 지나면 key 제거EXPIRETIME key: key가 만료되는 시각을 Unix Timestamp로 반환KEYS *: 모든 Key를 확인FLUSHDB: 모든 Key를 제거Spring에서 Redis를 사용 할 때는 Spring Data Repository를 이용하는 방식, RedisTemplate를 이용하는 방식이 있습니다.
Spring Data Repository를 이용하는 방식은 간단한 CRUD 작업에 적합하여 JPA를 사용할 때와 같이 매우 편리하게 Redis를 사용할 수 있습니다.
@RedisHash("item")
public class Item implements Serializable {
@Id
private Long id;
private String name;
private String description;
private Integer price;
}
JPA에서 @Entity를 붙여 테이블을 정의하던 것 처럼 @RedisHash를 붙여 Redis에 저장할 데이터를 정의할 수 있습니다. 주어지는 문자열인 item은, Redis에서 Key를 namespace:key와 같이 좀 더 계층적으로 표현할 수 있는데 이 때 namespace 해당하는 값이라고 보면 됩니다. 만약에 id가 1이라면 Key는 item:1이 됩니다.
Redis에서 객체의 데이터는 결국 문자열로 직렬화 되어 저장됩니다 그렇기에 implements Serializable를 붙여주어 직렬화가 가능한 클래스임으 명시해 주어야 합니다.
public interface ItemRepository extends CrudRepository<Item, Long> {}
JPA에서 JpaRepository 인터페이스를 상속하여 Repository를 만들던 것 처럼 CrudRepository를 상속하여 Redis Repository를 만들 수 있습니다. CrudRepository는 JpaRepository 또한 상속하고 있는 인터페이스로, CRUD에 관련된 메소드들을 가지고 있습니다. JpaRepository와 마찬가지로 제네릭에는 <데이터 타입, 키 타입>이 주어집니다.
JpaRepository의 CRUD와 동일하게 사용하면 됩니다.
Item item = Item.builder()
.id(1L)
.name("keyboard")
.description("Mechanical Keyboard Expensive 😢")
.build();
itemRepository.save(item);
Item item = itemRepository.findById(1L)
.orElseThrow();
item.setDescription("On Sale!!!");
itemRepository.save(item);
Item item = itemRepository.findById(1L)
itemRepository.deleteById(1L);
RedisTemplate를 이용하는 방식은 사용이 약간 더 복잡하지만 훨씬 자유롭게 Redis의 거의 대부분의 기능을 활용할 수 있습니다.
만약 간단히 문자열만 저장을 하고 싶다면 StringRedisTemplate를 사용하는 것이 적합합니다. 이는 Key, Value를 전부 Java 문자열이라고 가정하고 생성되어있는 Spring Bean입니다.
@SpringBootTest
public class RestTemplateTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void stringOpsTesT() {
// 지금 RedisTemplate에 설정된 타입을 바탕으로 Redis 문자열 조작을 할거다.
// 문자열 조작을 위한 클래스
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("simpleKey", "simpleValue");
System.out.println(ops.get("simpleKey"));
// 집합을 조작하기 위한 클래스
SetOperations<String, String> setOps = stringRedisTemplate.opsForSet();
setOps.add("hobbies", "codding", "alcohol", "games");
System.out.println(setOps.size("hobbies"));
stringRedisTemplate.expire("hobbies", 10, TimeUnit.SECONDS);
stringRedisTemplate.delete("simpleKey");
}
}
Bean이게 @Autowired로 주입이 가능합니다. 인스턴스에서 opsForValue()로 내가 조작하고자 하는 Redis 데이터 타입에 대한 클래스를 얻어올 수 있습니다. ValueOperations는 String 데이터 타입을 위한 클래스로 제네릭으로 Key, Value 타입을 String으로 지정해주면 됩니다.
여기서 여러군데에서 동일하게 String이라는 단어를 쓰기에 헷갈릴 수 있습니다. 예를 들어 StringRedisTemplate도 String이고, ValueOperations도 Redis String을 사용한다고 하고, 제네릭으로 주어지는 타입도 <String, String>입니다.
String이 여기저기서 사용되다보니 많이 헷갈릴 수 있는데 정리를 해보면,
이 됩니다. 헷갈리지 않게 각 String이 무엇을 의미하는지 잘 기억해야 합니다.
마찬가지로 SetOperations 클래스를 가져오게 되면 이는 Redis의 Set 타입을 위한 클래스입니다.
앞선 StringRedisTemplate은 간단히, 문자열만 사용할 때 적합한 방식이고 value에 여러 타입의 데이터를 저장하고 싶으면 직접 RedisTemaplte을 정의하여 사용해야 합니다.
우선 Value로 사용하기 위한 객체를 하나 정의해줍시다.
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemDto implements Serializable {
private String name;
private String description;
private Integer price;
}
RedisTemplate를 필요한 곳에서 초기화를 하여 사용하는 것 보다 Bean으로 IoC 컨테이너에 등록한 후 DI하여 사용하는 것이 Spring에서는 더 올바른 방법입니다.
다음과 같이 사용할 RedisTemplate를 Bean으로 등록해줍니다.
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ItemDto> itemRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, ItemDto> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
{
그 후 다음과 같이 @Autowired로 주입 받아 사용하면 됩니다.
@SpringBootTest
public class RestTemplateTests {
@Autowired
private RedisTemplate<String, ItemDto> itemRedisTemplate;
@Test
public void itemRedisTemplateTest() {
ValueOperations<String, ItemDto> ops = itemRedisTemplate.opsForValue();
ops.set("my:keyboard", ItemDto.builder()
.name("Mechanical Keyboard")
.price(250000)
.description("OMG")
.build());
System.out.println(ops.get("my:keyboard"));
ops.set("my:mouse", ItemDto.builder()
.name("mouse mice")
.price(100000)
.description("OMG")
.build());
System.out.println(ops.get("my:mouse"));
}
}