인기검색어 기능에 왜 Redis를 써야 할까?
- 사용자가 검색할 때마다 DB로 UPDATE 쿼리를 날리는 것은 상당히 비용이 크다
- Redis는 In-memory 방식으로 속도가 빠름
- Redis의 Sorted Set을 활용하면 쉽게 인기 검색어를 관리할 수 있음
Redis의 Sorted Set 자료형
우선 인기 검색어를 구현할 때 사용할 sorted set에 대해 알아보자
A Redis sorted set is a collection of unique strings (members) ordered by an associated score. When more than one string has the same score, the strings are ordered lexicographically.
Redis 공식 문서에서는 sorted set을 위와 같이 정의하고 있다.
해석하자면 sorted set의 요소들은 중복되지 않는 유니크한 String으로 구성되고 score이라는 값을 가진다. 이 score 값을 가지고 정렬이 수행된다.
redis-cli에서 Sorted Set 사용해보기
ZADD
help zadd 를 redis-cli에 입력하면 다음과 같이 사용법과 설명이 나온다.
sorted set에서 member의 score 값에 score을 더한다. 만약 member가 없다면 업데이트하고 key가 없다면 생성한다.
ZADD testSet 1 "member1"
현재 testSet이라는 key가 없기 때문에 testSet을 생성한다.
"member1"에 해당하는 멤버 또한 없기 때문에 score를 1로 하는 member1을 업데이트 한다.
ZINCRBY
ZADD와 유사한 명령어로 ZINCRBY 명령어가 있다.
ZADD와 유사해 보이지만 약간의 차이점이 있다.
위와 같이 sorted set에 없는 멤버에 대해 지정한 값으로 멤버를 추가해주는 것은 동일하다.
하지만 이미 존재하는 멤버에 대해 두 명령어를 실행해보면 차이점을 알 수 있다.
먼저 member1에 대해 ZADD를 적용했을 때는 member1의 score가 3으로 바뀌고
member2에 대해 ZINCRBY를 적용했을 때는 member2의 score가 4로 바뀐다.
ZADD 명령어는 이미 존재하는 멤버에 대해서는 해당 멤버의 score를 지정한 값으로 갱신하고
ZINCRBY 명령어는 지정한 값을 더하여 갱신한다.
ZRANGE
ZRANGE 명령어는 sorted set의 요소를 start 부터 stop까지 출력한다.
WITHSCORES 옵션을 통해 연관된 socre와 함께 출력도 가능하다.
Spring boot에서 Redis 사용하기
가장 먼저 build.gradle에 redis 의존성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
application.yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
RedisConfiguration
@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());//key 깨짐 방지
redisTemplate.setValueSerializer(new StringRedisSerializer());//value 깨짐 방지
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
SearchServie
@Service
@RequiredArgsConstructor
@Slf4j
public class SearchService {
private final PostRepository postRepository;
private final RedisTemplate<String, String> redisTemplate;
public List<PostDto> searchTitle(String title) {
List<Posts> results = postRepository.findByTitleLike(title);
List<PostDto> response = new ArrayList<>();
for (Posts result : results) {
response.add(PostDto.builder()
.title(result.getTitle())
.build());
}
try {
redisTemplate.opsForZSet().incrementScore("ranking", title, 1);
} catch (Exception e) {
System.out.println(e.toString());
}
return response;
}
public List<String> getSearchRankList() {
String key = "ranking";
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
Set<String> popularKeywords = zSetOperations.reverseRange(key, 0, 9);
return popularKeywords != null ? new ArrayList<>(popularKeywords) : null;
}
}
searchTitle 매서드는 검색이 수행되었을 때 실행되고, 해당 검색어의 멤버가 redis의 키가 ranking인 sorted set에 없다면 1로 생성하고, 있다면 값을 1 증가시킨다.
getSearchRankList 매서드는 ZREVRANGE를 통해 ranking을 key로하는 sorted set에서 score가 가장 높은 순서의 검색어를 가져온다.
SearchController
@RestController
@Slf4j
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
@GetMapping("/redis-learn/search")
public List<PostDto> search(String title) {
return searchService.searchTitle(title);
}
@GetMapping("/redis-learn/search-rank")
public List<String> getSearchRank() {
return searchService.getSearchRankList();
}
}
hello : 1번
Test 1 : 1번
Test 2 : 2번
Test 3 : 3번
위와 같이 검색 요청이 수행되었고 search-rank를 통해 결과를 확인해본다.
정상적으로 많이 검색된 순서대로 출력되는 것을 볼 수 있다.
참고자료
'server' 카테고리의 다른 글
[server] spring boot + 프로메테우스, 그라파나를 활용한 모니터링 실습 (2/2) (0) | 2024.07.01 |
---|---|
[server] spring boot + 프로메테우스, 그라파나를 활용한 모니터링 실습 (1/2) (0) | 2024.07.01 |
[Redis] Redis overview (0) | 2024.03.31 |