Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 38b6f17

Browse files
committed
feat : add a redis-is-faster-than-mysql.md
1 parent 7b99634 commit 38b6f17

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: Redis는 MySQL보다 항상 빠른가요? 아니요.
3+
categories: [Computer engineering, Backend engineering]
4+
tags: [backend, spring, java, redis, MySQL, autocorrect, autocomplete, refactoring, performance issue, 백엔드, 스프링, 자바, 레디스, 자동 완성, 성능 개선, 성능 이슈]
5+
---
6+
7+
이번 포스팅에서는 이전 포스팅에서 계속 다뤘던 검색어 자동 완성 구현을 위해 Redis와 MySQL을 각각 사용했을 때 성능을 비교하면서 어떤 것을 사용하는 것이 더 좋은지 알아보겠습니다.
8+
9+
## Redis와 MySQL의 성능 비교를 위한 상황
10+
**우선 Redis가 좋을지 MySQL이 좋을지 고민하게 된 배경에 대한 소개**를 드리겠습니다. 이전 세 개의 포스팅에 걸쳐 검색어 자동 완성 기능 구현 과정에 대해 공유드렸었는데, 갑작스럽게 요구 사항이 바뀌는 바람에 "어 이러면 Redis 쓰는 의미가 없지 않나?"라는 의구심이 들어 직접 성능 비교를 해보게 되었습니다.
11+
12+
원래 기존의 검색어 자동 완성 기능에 대한 요구 사항은 사용자가 검색한 검색어를 활용해서 DB에서 맨 앞 글자부터 사용자의 검색어와 매칭되는지 여부를 검사하여 필터링하는 방식으로 구현했었는데, 이번에 요구 사항이 바뀌면서 맨 앞 글자가 아니더라도 중간에 사용자의 검색어가 포함되어 있어도 자동 완성의 대상이 되도록 변경해야만 했습니다. 즉 간단히 말하면 String의 Contains()와 같은 로직으로 변경이 됐습니다.
13+
14+
기존에 열심히 Redis 적용시키고 Refactoring 시켰던 것이 아쉽기도해서 Redis를 사용해 Contains()와 같은 로직을 쉽게 구현할 수 있을지 검색해보다가 일단 Redis에는 scan이라는 기능이 있었는데 이를 통해 정규표현식처럼 String 매칭이 가능하다는 것을 알게 되었습니다. 여기까지만 보면 굉장히 희망적이었는데 이때 **1차 고비**가 한번 찾아옵니다.
15+
16+
**그것은 바로 대소문자 구분 여부**입니다. 저희 프로젝트 요구 사항 상 대소문자 구분이 안되어야 하는데, 즉 **case insensitive해야 하는데 Redis 내부적으로 case insensitive와 관련된 기능 제공을 하고 있지 않다**는 것을 알게됩니다. 그래서 **어떻게 이걸 구현할 수 있을지 고민하다가 생각해낸 방법**이 다음 로직입니다.
17+
18+
1. 검색 대상이 되는 모든 가게명을 모두 대문자 혹은 소문자로 변경하고 해당 값을 Redis내 Hash 자료구조의 Key로 저장
19+
2. 그리고 대문자 혹은 소문자로 변경하기 전 원래 가게명은 해당 Key와 매핑되는 Value로 저장
20+
3. 그 후 Hash Key값에 대한 scan을 통해 Contains 로직 구현
21+
4. 조건에 맞는 Hash Key값들은 매핑 되어있는 Value(원래 가게명)을 가져와 List에 저장
22+
23+
위 로직 그대로 Redis의 Hash 자료 구조를 이용해 구현한 코드를 보며 설명드리겠습니다.
24+
25+
## Redis Hash를 이용한 구현
26+
```java
27+
@Service
28+
public class RedisHashService {
29+
private final HashOperations<String, String, String> hashOperations;
30+
private final RedisTemplate<String, String> redisTemplate;
31+
32+
public RedisHashService(RedisTemplate<String, String> redisTemplate) {
33+
this.hashOperations = redisTemplate.opsForHash();
34+
this.redisTemplate = redisTemplate;
35+
}
36+
37+
private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터
38+
39+
//Hash에 field-value 쌍을 추가하는 메서드
40+
public void addToHash(String field, String value) {
41+
hashOperations.put(key, field, value);
42+
}
43+
44+
public Set<String> findAllValuesContainingSearchKeyword(String searchKeyword) {
45+
//Redis에서는 case insensitive한 검색을 지원하는 내장 모듈이 없으므로 searchKeyword는 모두 소문자로 통일하여 검색하도록 구현
46+
//당연히 초기 Redis에 field를 저장할 때도 모두 소문자로 변형하여 저장했고 원본 문자열은 value에 저장!
47+
Set<String> result = new HashSet<>(); //searchKeyword를 포함하는 원래 가게 이름들의 리스트. 최대 maxSize개까지 저장. 중복 허용하지 않고, 자동 사전순 정렬하기 위해 사용
48+
final int maxSize = 10; //최대 검색어 자동 완성 개수
49+
50+
ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + searchKeyword + "*").build(); //searchKeyword를 포함하는지를 검사하기 위한 scanOption
51+
Cursor<Map.Entry<String, String>> cursor = hashOperations.scan(key, scanOptions); //기존 Redis Keys 로직의 성능 이슈를 해결하기 위해 10개 단위로 끊어서 조회하는 Scan 기능 사용
52+
53+
while (cursor.hasNext()) { //끊어서 조회하다보니 while loop로 조회
54+
Map.Entry<String, String> entry = cursor.next();
55+
result.add(entry.getValue());
56+
57+
if(result.size() >= maxSize) //maxSize에 도달하면 scan 중단
58+
break;
59+
}
60+
cursor.close();
61+
return result;
62+
}
63+
64+
public void removeAllOfHash() {
65+
redisTemplate.delete(key);
66+
}
67+
}
68+
```
69+
70+
우선 위 코드가 **Redis의 Hash 자료 구조를 사용**하는 서비스 로직입니다. 여기서 메인 로직은 **Case insensitive하게 Redis에서 조회해주는 findAllValuesContainingSearchKeyword 메서드**입니다. 앞서 설명드린 로직과 그대로 구현을 했고 조금 자세히 보면 좋을 부분이, 원래 **Keys라는 Redis 기능을 통해서 Key에 대한 조회를 할 수가 있는데 Redis가 싱글 스레드이다 보니 대규모 데이터에 대해서 Keys로 조회를 하게되면 해당 처리를 하는 동안 계속 Redis를 점유하고 있는 문제가 있어서 Scan이라는 기능이 추가**되었다고 합니다. **Scan은 사용자가 지정한 단위대로 Pagination해서 처리**합니다. **Default 값은 10개**로 되어있습니다.
71+
72+
```java
73+
public List<String> autocorrect(String keyword) { //검색어 자동 완성 로직
74+
Set<String> allValuesContainingSearchKeyword = redisHashService.findAllValuesContainingSearchKeyword(keyword); //case insensitive하게 serachKeyword를 포함하는 가게 이름 최대 10개 반환
75+
if(allValuesContainingSearchKeyword.isEmpty())
76+
return new ArrayList<>();
77+
else
78+
return new ArrayList<>(allValuesContainingSearchKeyword); //자동 완성 결과가 존재하면 ArrayList로 변환하여 리턴
79+
}
80+
```
81+
82+
다음 로직은 **자동 완성 로직에 대한 구현이 담긴 Service 단**의 코드입니다. 단순히 방금 전까지 살펴본 findAllValuesContainingSearchKeyword() 메서드를 통해 얻어진 최대 10개의 자동 완성 키워드를 반환해주는 기능을 합니다. 이렇게 했을 때 **약 51200개의 가게를 가지고 있는 저희 데이터에 대해 어느정도의 Response time이 나오는지 확인**해봤습니다.
83+
84+
![1](/assets/img/redis-is-faster-than-mysql/1.png){: w="1000" h="800" style="border:1px solid #eaeaea; border-radius: 7px; padding: 0px;"}
85+
86+
위 사진처럼 보시다시피 **Redis를 Full scan할 수 있는 searchKeyword로 검색했을 때 약 2.5초**가 나왔습니다. 여기서 **Full scan할 수 있는 searchKeyword란, 제가 자동 완성 키워드가 10개 만들어지면 조회 loop를 도중에 break하도록 구현해 놓았기 때문에 일부러 10개까지 조회되지 않는 검색어**로 검색하였습니다.
87+
88+
그럼 이번에는 **성능 비교를 위해 MySQL의 like 쿼리를 사용해서 조회**해보겠습니다.
89+
90+
## MySQL LIKE를 이용한 구현
91+
```java
92+
public List<String> findAllBySearchKeyword(String searchKeyword) {
93+
return em.createQuery("select s.displayName from Store s where s.displayName like concat('%', :searchKeyword, '%')", String.class)
94+
.setParameter("searchKeyword", searchKeyword)
95+
.setMaxResults(10)
96+
.getResultList();
97+
}
98+
```
99+
100+
이번에는 서비스쪽 로직은 그냥 Query 결과를 반환해주는 거 밖에 없을 정도로 간단해서 생략하고 **메인 로직인 Repository 단**의 코드를 가져왔습니다. 쿼리도 별로 어려울 것 없이 **LIKE를 활용해서 사용자의 검색어가 포함되는 가게명들을 조회하였고 like를 활용한 pattern matching을 편리하게 하기 위해 concat을 사용해서 검색어 앞뒤에 %를 붙여줬습니다.**
101+
102+
사실 이 쿼리만 봐도 누가 빠를지 대충 예상이 되긴합니다. **MySQL에는 인덱싱을 적용할 수 있기도하고 현재 Repository 단에서부터 애초에 최대 10개로 Limit를 걸고 filtering해서 가지고 오기 때문에 추후 Service 로직에서 그 많은 모든 데이터에 대해 인스턴스화해서 가지고 있을 필요가 없습니다. 이에 따른 메모리나 성능적인 이점 또한 매우 클 것입니다. 그리고 무엇보다 직접 대문자 혹은 소문자화 해가면서 Case insensitive하게 만드는 로직을 거칠 필요도 없습니다. 왜냐하면 MySQL 쿼리 내 like 절은 애초에 Case insensitive하게 동작하기 때문**입니다.
103+
104+
![2](/assets/img/redis-is-faster-than-mysql/2.png){: w="1000" h="800" style="border:1px solid #eaeaea; border-radius: 7px; padding: 0px;"}
105+
106+
실제로 Response time을 체크해보면 위 사진과 같이 **약 0.5초** 밖에 걸리지 않습니다. **무려 Redis를 사용해서 걸렸던 시간의 20%** 밖에 안걸립니다.
107+
108+
## Redis VS MySQL 결론
109+
지금까지 살펴본 성능 분석 결과를 정리해보면 다음과 같습니다.
110+
111+
**Case insensitive하게 특정 문자열이 포함되는 값들을 Full scan(전수 조사)해야 되는 상황**이 있었습니다. 이때, **Redis에서는 이러한 기능을 내장하여 제공하고 있지 않기 때문에 별도의 추가 로직 필요하다는 점** 그리고 **Redis의 경우 데이터를 Filtering해서 사이즈를 대폭 줄여줄 수 있는 limit 로직을 Service 단보다 더 이른 시점인 Repository 단에서 걸어줄 수 없다는 점들이 성능 저하의 요인**이 되었습니다.
112+
113+
**특히 대용량 데이터의 Filtering 시점이 늦어짐에 따라 대량의 Repository 조회 결과를 고스란히 인스턴스화시켜서 Service 단까지 가져오고 이를 전체 조회해야 된다는 점이 성능 저하의 주된 요소**로 보여집니다.
114+
115+
그래서 **결론은, "인메모리 DB이므로 Redis는 항상 빠를 것이다."라는 생각은 굉장히 잘못된 일반화의 오류이며 Redis의 특징인 Key-Value 자료 구조의 장점이 잘 부각될 수 있도록 캐싱이나 O(1) 조회 등이 필요한 상황에만 적용해서 사용하는 것이 바람직**할 것입니다.
116+
117+
118+
154 KB
Loading
152 KB
Loading

0 commit comments

Comments
 (0)