Redis 특징
- Key-Value 방식으로 저장된다.
- 컬렉션을 지원해서 다양한 타입으로 저장 가능하다.
- Pub/Sub 기능을 지원한다. 이 방식은 채팅이나 이벤트 소싱 방식에서 사용할 수 있다.
- 디스크 저장이 가능하다. 메모리에 데이터를 저장하여 휘발성메모리라고 하지만 AOF/RDB 방식을 사용해서 영구적인 저장이 가능하다. 하지만 성능적인 측면에서 저하된다거나 중간에 데이터가 유실될 수도 있다는 단점이 있다.
- 복제가 가능하다. Master/Slave 구조를 지원한다. 또한 샤딩을 사용해서 수평적인 확장이 가능하다.
- 메모리를 통해 데이터를 조회하기 때문에 빠른 속도로 데이터를 처리한다.
Redis의 타입 정리
Redis 기본 개념, 자료구조
Redis는 가장 자주 기본적으로 사용하는 구조는 다음과 같다. 각 타입의 특성을 알아보고 다시 한 번 위 포스팅에서 내용을 가져와 정리하는 시간을 가지려고 한다. 위 자료는 번역하거나 데이터를 추가하면서 순서가 꼬인 부분이 많다...!
String
- 키와 연결할 수 있는 가장 간단한 유형이다.
- 모든 종류의 문자열을 저장할 수 있으며 JPEG 이미지, HTML fragment를 캐시하는 용도로 자주 사용한다.
- 최대 저장 사이즈는 512MB이다.
List
- list는 일반적인 LinkedLIst 특징을 갖는다.
- 수 백만개의 아이템이 있더라도 head, tail에 값을 추가할 동안 동일한 시간이 소요된다. 특정 값을 인덱스로 데이터를 찾거나 삭제할 수 있다.
- 여러 작업에 list가 쓰이지만 대표적으로 Pub/Sub 패턴에 사용된다. 프로세스간 통신 방법에서 생산자가 아이템을 만들어서 list에 넣으면 소비자가 꺼내와서 수행하는 식으로 동작된다.
- 일시적으로 list를 blocking하는 기능도 유용하게 사용할 수 있다. Pub/Sub 상황에서 list가 비어있을 때 pop을 시도하면 대부분 null을 사용하게 되는데 이 경우 소비자는 일정 시간을 기다린 후 다시 pop을 시도한다(polling 방식)
- BRPOP를 사용하면 새로운 아이템이 리스트에 추가되었을 때에만 응답하므로 polling 프로세스 수를 줄일 수 있다.
BRPOP
사용법은 brpop key timeout 이다.
리스트에 데이터가 이미 있을 경우에는 RPOP와 같다. 데이터가 없을 경우에는 timeout(초) 만큼 기다린다.
timeout이 0일때, 데이터가 입력될때까지 기다린다. 데이터가 들어오면 pop을 하고 key, data, 시간(초)를 표시한다.
출처 - redisGate
Hash
- field-value쌍을 이용한 일반적인 해시 타입
- key에 대한 field의 개수는 제한이 없다.
- field와 value의 구성은 rdb와 다를바 없다. key는 pk, field는 column, value는 value로 볼 수 있다.
hmget user-2 email country 1) "gigantpengsoo@ebs.com" 2) "Antarctica"
- 아래처럼 개별 아이템을 atomic하게 조작할 수 있는 커맨드도 있다.
> hincrby user:1000 birthyear 10 (integer) 1987 > hincrby user:1000 birthyear 10 (integer) 1997 ```
Set
- 정렬되지 않은 문자열의 모음이다. 아이템은 중복될수도 없다.
- 교집합, 합집합, 차집합 연산을 레디스에서 수행할 수 있기에 set은 객체 간의 관계를 표현할 때 좋다.
- 아래처럼 태그기능으로 데이터를 포함할 수 있다.
> sadd project:1000:tags 1 2 5 77 (integer) 4
smembers project:1000:tags
- 5
- 1
- 77
- 2
SortedSet
- set과 마찬가지로 key 하나에 중복되지 않는 여러 멤버들을 저장하지만, 각각의 멤버는 스코어에 연결된다.
- 모든 데이터는 이 스코어를 통해 정렬되고, 같은 값은 멤버값의 사전 순서로 정렬된다.
- 이 score를 사용해서 랭킹 시스템 또는 인기 검색어 기능을 구현할 수도 있다.
- 정렬된 형태로 저장되기 떄문에 인덱스를 이용하여 빠르게 조회할 수 있다. 그래서 인덱스를 이용하여 조회할 일이 많다면 list보다는 sorted set의 사용을 권장한다.
bit/bitmap
- setbit, getbit 등의 커맨드로 일반적인 비트 연산이 가능하다.
- 비트맵을 사용하면 크게 절약할 수 있다는 장점이 있다.
hyperloglogs
- 집합의 카디널리티를 추정하기 위한 데이터 구조이다. 예를 들어 검색 엔진의 하루 검색어 수를 조회할 떄 사용한다.
- 일반적으로 이를 계산하기 위해서는 데이터크기에 비례하는 메모리가 필요하지만, 레디스의 hyperloglogs를 사용하면 같은 데이터를 여러번 계산하지 않도록 과거의 항목을 기억하기 때문에 메모리를 효과적으로 줄일 수 있다.
Geospatial Indexes
- 지구상 두 지점의 경도와 위도를 입력하고, 그 사이의 거리를 구하는데 사용된다.
- 내부적으로는 Sorted Set Data Structure를 사용한다.
Stream
- 레디스 5.0에서 도입된 로그를 처리하기 위해 최적화된 데이터 타입이다.
- 가장 큰 특징은 소비자 그룹을 지정할 수 있다는 것이다.
Redis의 Key
- 레디스의 키는 문자열이기 때문에 알파벳 'abcd'부터 모든 이진 시퀀스를 키로 사용할 수 있다. 빈 문자열도 키가 될 수 있다. string 타입과 마찬가지로 허용되는 최대 키 크기는 512MB이다.
- 키를 조회할 때 비용을 생각하면, 키를 너무 길게 사용하는 것은 권장되지 않는다. 키를 어떻게 생성하느냐에따라 분산이 몰릴수도 있다.
보통user:1000
처럼 object-type:id의 형태를 권장한다. comment:reply.to
,comment:reply-to
와 같이 '.', '-', ':' 드으이 부호를 사용해서 관계를 나타낼 수 있다. 이 부분은 restAPI처럼 명명규칙에 해당하는 것 같다.
Expire기능
- 키와 관련된 중요한 기능인 Expire에 대해서도 알아보면 인메모리 DB인 만큼, 메모리에 저장될 수 있는 데이터는 한정적이다.
- 더이상 메모리에 저장할 수 있는 공간이 없는 경우 가장 먼저 들어온 데이터를 삭제하거나, 최근에 사용되지 않는 데이터를 삭제하거나, 데이터를 받지 못하도록 해야 한다.
- 가장 좋은 방법은 직접 설정하는 것이다. TTL을 설정해서 만료 시간을 정하고, 설정된 timeout이 경과되면 DEL명령어를 호출한것처럼 키가 자동으로 삭제되야 한다.
Redis 사용하는 첫 번째 방법 - @HashHash
- 기존의 JPA를 사용하던 방식과 동일하다 Repository를 통해 Redis 서버에서 데이터를 조회,수정,삭제,삽입하는 방식이다.
- 달라진 점이 있다면 @Entity 라는 어노테이션이 아닌 @RedisHash 방식을 사용한다는 점이다.
- 그리고 Repository에는 JPARepository가 아닌 CRUDRepository를 사용한다.
ItemOrder.Class
아래에서 사용한 어노테이션과 속성을 간단히 설명해보자면 아래와 같다.
- @RedisHash를 사용해서 Hash타입으로 데이터가 저장되도록 한다.
- value를 사용해서 prefix를 설정한다. id가 string이기때문에 order:string지정값 으로 키가 저장된다.
- timeToLive를 사용해서 만료될 시간을 설정해줄 수 있다. 초단위이기 때문에 아래 데이터는 60초 뒤 소멸된다.
- @Indexed
- 이 어노테이션을 사용해서 세컨더리 인덱스를 사용할 수 있다. 데이터 저장소에서 @Id로 설정한 메인 키는 아니지만 필요시 데이터를 조회하기 위해 인덱싱을 이 어노테이션으로 추가할 수 있다.
- 데이터가 생성될때마다 인덱스가 따로 저장이 되고, 삭제될 때 인덱스도 함께 삭제된다.
이 어노테이션을 선언하면 prefix:field_name:value의 형태를 갖도록 저장된다. - 이 때 저장되는 자료형은 SortedSet이다. SortedSet은 가중치에 따라 정렬되어 사용되기 때문에 특정 필드 값에 따른 객체들을 빠르게 조회할 수 있다.
- @TimeToLive
- 별도의 정수형 데이터에 이 어노테이션을 적용하면 Redis에 데이터가 저장되는 만료기한을 설정할 수 있다. 초, 시간등 단위도 지정 가능하다.
- 생성시 지정한 값으로 TTL이 설정된다.
- 이렇게 지정하여 설정할 경우 SeconderyIndex는 삭제하지 못하는 단점이 있어 별도의 EventListener를 따로 구현할 필요가 있다.
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(value = "order", timeToLive = 60)
public class ItemOrder {
@Id
private String id;
@Indexed
private String name;
private Integer count;
private Long totlaPrice;
private String orderStatus;
@TimeToLive(unit = TimeUnit.HOURS)
private int ttl;
}
Repository
아래와같이 CrudRepository를 사용하면 jpa처럼 기본적인 쿼리문을 수행할 수 있다.
public interface OrderRepository extends CrudRepository<ItemOrder, String> {
그렇다면 CrudReopsitory는 어떤 메서드들이 있는지 확인해보자
기본적인 CRUD가 가능하도록 미구현 메서드들이 포함되어 있는것을 볼 수 있다.
최상위 Repository의 하위 구현체로 JPARepository는 추가로 PagingAndSortingRepository를 상속받고 있다. 이런 차이점이 있다는 것만 알고 있으면 더 좋을 것 같다.
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
Redis를 사용하는 두 번째 방법 - RedisTemplate
- RedisTemplate은 Redis 내부에 있는 데이터 엑세스를 도와주는 헬퍼 클래스이다.
- 클라이언트에서 작성해야 하는 레디스 공통로직을 제공해 레디스 이용을 편리하게 할 수 있도록 도와준다.
RedisTemplate 역할
- 커넥션 관리
- 레디스 서버와 연결 시에 커넥션 풀과 리소스를 대신 관리해준다.
- 이로인해 개발자는 직접 커넥션을 열거나 닫을 필요가 없어진다.
- 직렬화와 역직렬화를 지원한다.
- 자바 Serializer를 통해 Redis 스토리지와 데이터를 직렬화/역직렬화 한다.
- 다른 여러 직렬화 기법도 제공하지만 문자열만 사용한다면 StringRedisTemplate을 사용하는 것이 좋다.
- 데이터 타입에 맞는 직렬화 , 역직렬화 방식을 Serializer의 형태로 지원하여 타입에 따른 효율적인 통신이 가능하다.
- 기본값은 JDKSerializationRedisSerializer이고 StringRedisSerializer , Jackson2JsonRedisSerializer 같은 상황에 맞는 Sereializer도 제공한다.
- 직접 커스텀한 Serializer를 구성하는 것도 가능하다.
RedisTemplate - redisConnectionFactory
@Bean
public RedisTemplate<String, ItemDto> itemRedisTemplate(
RedisConnectionFactory connectionFactory //yml파일 기준으로 생성되는 factory
){
RedisTemplate<String,ItemDto> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string()); //key 직렬화 방식
redisTemplate.setValueSerializer(RedisSerializer.json()); //json으로 직렬화, GenericJackson2JsonRedisSerializer을 사용하지만 가끔 문제가생겨 직접 만드는 것도 추천
return redisTemplate;
}
RedisConnectionFactory는 Redis 데이터베이스와의 연결을 설정하는 역할이다.
Redis 서버와 연결을 만들고 관리를 담당하며, LettuceConnectionFactory와 JedisConnectionFactory가 가장 일반적으로 사용된다.
그럼 Spring에서는 LettuceConnectionFactory와 JedisConnectionFactory중 어떤걸 기본값으로 사용할까?
- LettuceConnectionFactory
- 비동기(동기 포함) 및 리액티브 프로그래밍을 지원하여 더 뛰어난 성능과 확장성을 제공한다.
- JedisConnectionFactory
- 쉬운 사용성이 장점이지만, 동기적으로 동작한다.
- 비동기적인 부분을 지원하긴 하지만 멀티 쓰레드 환경에서 동작하도록 JedisPool을 사용하기 때문에 모든 풀이 사용중일 경우 유휴상태에 빠질 수 있다.
- Redis의 클러스터모드를 지원하지 않는다는 단점도 존재한다.
그래서 Spring 2.x 이상에서는 Lettuce를 사용한 LettuceConnectionFactory가 권장되고 기본값으로 설정되어 있다.
외부 라이브러리의존성의 MANIFEST.MF에 들어가면 이러한 내용이 있다.
Manifest-Version: 1.0
Automatic-Module-Name: spring.boot.starter.data.redis
Build-Jdk-Spec: 17
Built-By: Spring
Implementation-Title: Starter for using Redis key-value data store with
Spring Data Redis and the Lettuce client
Implementation-Version: 3.3.1
Spring-Boot-Jar-Type: dependencies-starter
ItemService
여기서 RestTemplate이 연결되는 과정은 다음과 같다.
1. RedisTemplate<String, ItemDto>는 RedisConfig에서 생성한 redisTemplate 빈을 주입받음.
2. redisTemplate은 redisConnectionFactory를 사용하여 Redis와 연결됨.
3. redisConnectionFactory는 Spring Boot에서 기본적으로 Lettuce를 사용하여 Redis 서버에 연결.
4. rankOps = redisTemplate.opsForZSet();을 통해 Sorted Set(ZSet) 연산을 수행하는 객체를 생성.
이를 통해 rankOps.add(), rankOps.rangeByScore() 등의 ZSet 관련 메서드를 사용할 수 있음.
@Slf4j
@Service
public class ItemService {
private final ItemRepository itemRepository;
private final OrderRepository orderRepository;
private final ZSetOperations rankOps;
public ItemService(
ItemRepository itemRepository,
OrderRepository orderRepository,
RedisTemplate<String, ItemDto> redisTemplate
) {
this.itemRepository = itemRepository;
this.orderRepository = orderRepository;
this.rankOps = redisTemplate.opsForZSet();
}
public void purchase(Long id) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
orderRepository.save(ItemOrder.builder()
.item(item)
.count(1)
.build());
rankOps.incrementScore("sodRanks",ItemDto.fromEntity(item),1); //dto가 존재하지 않으면 자동으로 만들어서 증가시켜준다.
}
public List<ItemDto> getMostSold(){
Set<ItemDto> ranks = rankOps.reverseRange("sodRanks", 0 ,9);
if (ranks == null) {
return Collections.emptyList();
}
return ranks.stream().toList();
}
}
해당 메서드를 사용해서 "sodRanks"라는 id를 찾아서 score를 1 증가시킨다.
rankOps.incrementScore("sodRanks", ItemDto.fromEntity(item), 1);
해당 메서드를 사용해서 sodRanks에서 판매량이 가장 높은 상위 10개를 상품을 가져온다. SortedSet구조는 가장 낮은 점수를 기준으로 정렬되는 자료구조이기 땜누에 reverseRange()를 사용하면 높은 점수부터 정렬하여 데이터를 가져올 수 있다.
Set<ItemDto> ranks = rankOps.reverseRange("sodRanks", 0, 9);
결과보기
1번 호출했을 때
3번 호출했을 때
여러 데이터를 저장했을 때 score기준으로 정렬된 모습
세 번째 캐싱 방법 - @Cacheable
- 비교적 간단하게 캐싱을 하는 방법이다. 몇 가지 설정을 통해 어떤 방식으로 캐싱할 것인지를 정하면 어노테이션을 통해 메서드를 쉽게 적용할 수 있다.
- 작동방식은 AOP에 의해 프록시 클래스가 되고, 이 클래스에 의해 CacheInterceptor의 invoke가 호출되는 방식이다. 이 메서든는 실제 캐시 서버에 요청을 보내기 위한 다양한 클래스의 메서드가 호출된다. 내부 동작 원리는 나중에 더 자세히 포스팅할 예정이다.
EnableCaching
@Configuration
public class CacheConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
}
@Bean
public CacheManager cacheManager(
RedisConnectionFactory redisConnectionFactory
) {
//설정 구성을 먼저 진행
//Redis를 이용해서 Spring Cache를 사용할 때
// Redis 관련 설정을 모아두는 클래
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofSeconds(60)) // TTL(Time To Live)
.computePrefixWith(CacheKeyPrefix.simple()) // 캐시를 구분하는 접두사
.serializeValuesWith( //TODO cache 저장할 값을 어떻게 직렬화/역직렬화 할것인 !! 공부해보기
fromSerializer(RedisSerializer.java())
);
return RedisCacheManager.
builder(redisConnectionFactory).
cacheDefaults(redisCacheConfiguration)
// .withInitialCacheConfigurations() //TODO 캐시이름을 넣어 별도의 설정이 가능하다.!! 공부해보기
.build();
}
}
@EnaleCaching
- Spring Cache의 어노테이션을 사용하기 위해 설정 클래스에 이 어노테이션을 달아줘야 한다.
- 이 어노테이션을 달 때 CacheManager Bean을 정의하여 함께 등록해준다. 외부 캐시 서버를 사용하거나 여러 개의 캐시 저장소를 사용하는 등 사용자 정의를 진행할 경우 필요한 작업이다. 따로 설정하지 않으면 ConcurrentHashMap 기반 로컬 캐시 저장소를 사용하게 된다.
@Cacheable
- 메서드의 반환값을 캐시 아이템에 저장한다.
- 캐시 데이터가 존재하면 캐시의 데이터를 반환한다. 만약 데이터가 없다면, 메서드 로직 수행 후 반환값을 캐시 해줘야 한다.
- key, keyGenerator는 함께 쓰일 수 없으며 cacheManager, cacheResolver는 함께 쓰일 수 없다.
- 내부에서 사용할 수 있는 속성을 정리해보았다.
- value : cacheNames Alias
- key : spEL 이라는 표현식에 맞게 작성해 캐시 키를 동적으로 생성한다. (#, methodName 등)
- keyGenerator : 사용할 키제너레이터를 정할 수 있다.
- condition : SpEL 표현식으로 조건을 작성하여 true일 경우에만 캐싱이 적용되도록 한다.
- unless : SpEL 표현식으로 조건을 작성하여 true일 경우에 캐싱이 적용되지 않도록 한다.
- cacheManager : 사용할 캐시매니저를 지정한다.
- cacheResolver : 캐시 리졸버를 지정할 수 있다.
- sync : 여러 스레드가 동일한 키에 대해 값을 로드하려고 할 경우, 기본 메서드의 호출을 동기화한다.
@CacheEvict
- 오래되거나 사용되지 않는 데이터를 제거할 때, 데이터가 변경될 캐시 아이템을 제거하기 위해 활용한다.
- 메서드가 트리거로 동작하므로 반환 값이 무시되어 void 메서드에도 사용될 수 있다.
- 내부 사용 속성을 정리해보았다.
- value : cacheNames Alias
- key : spEL 이라는 표현식에 맞게 작성해 캐시 키를 동적으로 생성한다. (#, methodName 등)
- keyGenerator : 사용할 키제너레이터를 정할 수 있다.
- condition : SpEL 표현식으로 조건을 작성하여 true일 경우에만 캐싱이 적용되도록 한다.
- unless : SpEL 표현식으로 조건을 작성하여 true일 경우에 캐싱이 적용되지 않도록 한다.
- cacheManager : 사용할 캐시매니저를 지정한다.
- cacheResolver : 캐시 리졸버를 지정할 수 있다.
- allEntries : 캐시에 저장된 값을 모두 제거할지 여부
- beforeInvocation : 메서드 수행 전 캐시 데이터 제거 수행 여부
@CachePut
- 캐싱 값을 저장하는 용도로만 사용된다. 항상 메서드 로직을 수행하고 캐시 아이템을 저장한다.
- 내부 사용 속성을 정리해보았다.
- value : cacheNames Alias
- key : spEL 이라는 표현식에 맞게 작성해 캐시 키를 동적으로 생성한다. (#, methodName, args[0] 등)
- keyGenerator : 사용할 키제너레이터를 정할 수 있다.
- condition : SpEL 표현식으로 조건을 작성하여 true일 경우에만 캐싱이 적용되도록 한다.
- unless : SpEL 표현식으로 조건을 작성하여 true일 경우에 캐싱이 적용되지 않도록 한다.
- cacheManager : 사용할 캐시매니저를 지정한다.
- cacheResolver : 캐시 리졸버를 지정할 수 있다.
@Caching
- 동일한 캐시 어노테이션을 여러개 선언하고 싶은 경우 사용한다.
- 조건이나 키 표현 방식에 따라 여러 동작을 수행해야 하는 경우 유용하다.
- 내부 사용 속성을 정리해보았다.
- cacheable : 여러 개의 Cacheable 어노테이션 입력
- evict : 여러 개의 CacheEvict 어노테이션 입력
- put : 여러 개의 CachePut 어노테이션 입력
검색 결과 페이징 캐싱
이 부분은 코드와 함께 설명하겠다.@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
이렇게 속성을 추가하면 페이징 데이터가 DTO형태로 직렬화 된다. 다시 말해 Page가 Redis에 저장될 때 DTO 형태로 변환되어 저장된다.
@SpringBootApplication
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
public class RedisApplication {
}
args[0]은 쿼리 검색어, args[1]은 현재 페이지 번호, args[2]는 페이지 크기를 인자로 받아서 key를 생성한다.
@Cacheable(
cacheNames = "itemSearchCache",
key = "{ args[0], args[1].pageNumber, args[1].pageSize }"
)
public Page<ItemDto> searchByName(String query, Pageable pageable) {
return itemRepository.findAllByNameContains(query, pageable)
.map(ItemDto::fromEntity);
}
실제 레디스에서는 이와 같이 데이터가 JSON형태로 직렬화되어 Redis에 저장된다.
{
"content": [
{ "id": 1, "name": "치킨", "price": 15000 },
{ "id": 2, "name": "치킨세트", "price": 25000 }
],
"pageable": {
"pageNumber": 1,
"pageSize": 10
},
"totalPages": 5,
"totalElements": 50
}
만약 유저가 이와 같이 searchByName("치킨", PageRequest.of(1, 10));
로 요청을 보낸다고 하면 Redis에서 itemSearchCache::{"치킨",1,10} 키를 찾아 반환하고 존재하지 않으면 DB에서 데이터를 조회하여 캐싱 후 반환한다.
'Redis' 카테고리의 다른 글
[Redis] @Cacheable의 작동 원리 (0) | 2025.03.05 |
---|---|
[TIL] Redis 종류 및 전략 및 문서 정리 (0) | 2025.02.27 |