프로젝트/기술적 선택

[기술적 선택] Top-10 인기 검색어 구현을 위한 레디스 도입 이유

배발자 2023. 7. 23.
반응형

 

 

필자가 진행한 프로젝트에서 Top-10 인기 검색어 서비스를 구현하기 위해서 어떠한 방식을 사용할지 고민을 했다. 사실 해당 서비스를 구현하기 위해 레디스에 대한 개념이 잡혀있지 않아서 데이터베이스에 접근을 해서 매번 카운팅 업데이트 쿼리문을 날려야 하는건가 생각을 했었다. 왜냐하면 지금까지 프로젝트를 진행하면서 여러 기술들을 사용해봤지만 메모리 기반의 분산 저장소를 직접 활용해본 경험이 없었기 때문이다. 

 

"Top-10 인기 검색어" 서비스는 개발자 커뮤니티에서 사용자가 요청한 검색 키워드를 이용해 타사용자들에게 실시간으로 인기 검색어를 시각적으로 제공해준다. 그렇다면 매번 사용자가 검색할 때마다 디스크에 접근하여 해당 키워드에 대한 카운팅 작업으로 갱신한다면 리소스 낭비가 매우 심할 것이라고 생각했다. 

 

이를 해결하기 위해 좋은 방법이 무엇이 있을지 리서치를 통해 레디스를 도입하기로 하였다. 이번 포스팅은 필자가 왜 레디스를 선택을 하였고, 레디스의 어떠한 자료구조를 활용하여 인기 검색어 기능을 구현하였는지 정리하려고 한다. 

 

Redis (Remote Dictionary System) 

서비스에서 DB의 데이터를 불러오는 작업은 시간이 많이 걸리는 일이다. 쉬운 예로 들자면, 학창시절 교실에 있는 사물함을 많이 이용해봤을 거다. 만약 국어 수업이 있는 날에는 국어책을 사물함에 미리 넣어놓는다. 만약 사물함에 없다면? 집에 가서 들고와야지 뭐... 사물함에 넣어놓은 친구들은 10초, 국어책이 집에 있으면 20분이 걸릴 것이다. 

 

집이 디스크(DB)고, 사물함이 캐시(레디스)라고 생각하자. 캐시(레디스)는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소인 것이다. 

 

 

위의 그림처럼 처리율 제한 미들웨어에서 요청 처리에 대한 카운팅을 해주기 위해 레디스를 활용한다고 한다. 위의 그림에 대한 개념은 필자가 포스팅 한 아래의 글을 참고하자. 

 

 

[대규모 시스템 설계] 2-1 처리율 제한 장치

필자는 처리율 제한 장치와 관련된 처리를 해본 경험이 없다. 프로젝트를 진행하면서 필수적으로 진행해야하는 것은 API 기능 명세에 작성된 주요 서비스 구현이 1순위이다. 그러다보니 트래픽

baebalja.tistory.com

 

 

인메모리(In-memory)

레디스는 인메모리 데이터 구조 저장소로 캐시의 역할을 사용한다. 여기서 인메모리가 정확히 무엇인지 구분이 안갈 수 있다. 위에서 국어책 가져오기를 예로 들었지만 좀 더 구체적으로 설명하려고한다. 

 

인메모리란 컴퓨터의 메인 메모리 RAM에 데이터를 올려서 사용하는 방법을 말한다. 이유는 명확하게 속도 때문이다. 

SSD, HDD 같은 저장공간에 데이터를 가져오는 것보다 RAM에 올려진 데이터를 가져오는 것이 수백배(HDD 기준) 이상 빠르다. 즉, Redis는 빠른 검색 속도를 가진다. 

 

하지만 빠른 속도를 가지는만큼 단점도 존재한다. 기본적으로 노트북 사양을 고를 때 RAM 용량이 8기가, 16기가 32기가로 많이 상용되고 있는데 램이 커질수록 비용이 증가하는 것을 경험해본 적이 있을 것이다. 또한 Key-Value 형태의 NoSQL이라는 점에서 다양한 형태의 데이터 구조를 지원하기는 하지만 복잡한 데이터를 저장하는 데이터베이스로 사용하기에는 어려움이 있다. 

 

이러한 단점을 고려하여 캐시 서버로 활용하는 것이 적합하다는 판단이다. 인기 검색어 서비스는 복잡한 데이터 구조가 필요없이 인기 검색어를 가지는 키워드와 검색 횟수를 의미하는 score 데이터 값만 가지고 있으면 될 뿐더러 개발자 커뮤니티는 일반 소셜 커뮤니티보다 검색하는 키워드는 한정적이라는 생각이다. 또한, 매번 데이터베이스에 랭킹 정보를 갱신하고 order by로 불러오는 것은 리소스 낭비가 클 것이라고 생각하였고 캐시에 상위 10개의 인기 검색어를 정렬되어 저장되어있다면 좋은 해결책이 될 수 있다. 

 

 

Redis와 Memcached 비교

  Redis Memcached
데이터 분할 O O
다양한 데이터 구조 지원 O X
Thread  Single Thread Multi Thread
데이터 저장(persistence/snapshot) O X
데이터 복제 (replication) O X
트랜잭션 지원 O X
Publisher / Subscriber O X

redis를 검색하면 Memcached와 비교를 많이 한다. 둘 모두 사용하기 쉽지만 어떤 캐시를 사용할지는 장단점을 고려해야한다. 먼저 이 둘의 공통점은 1ms 이하의 응답 대기시간을 가진다데이터를 메모리에 저장하기 때문에, 디스크 기반의 데이터베이스보다 빠르게 데이터를 읽을 수 있기때문이다. 그리고 문법적으로 사용하기 쉽고, 개발 코드 양 또한 적기때문에 개발의 용이성을 가진다. 

 

 

 

주요 차이점 

 

1. 확장성

Memcashed의 확장성은 Scale up(vertical)을 통해서 얻을 수 있는 반면, Redis는 Scale out(horizontal)을 통해 얻을 수 있다. 

 

2. 데이터 축출 전략 

캐시는 디스크보다 용량이 작은 유한한 리소스를 지니므로, 메모리에 자리잡은 자원을 언젠가 교체해야할 순간들이 올것이다. Memcashed의 경우, LRU(Least Recently Used) 알고리즘만을 채택하고 있지만 Redis의 경우 보다 다양하고 미세한 방법을 제공한다. 

  • No Eviction : 메모리가 부족하면 에러 반환 
  • All Keys LRU : LRU에 근거하여 eviction 
  • Volatile LRU = LRU를 따르되, 만료 시점이 지정된 것들에 한해서 eviction 을 수행 
  • All Keys Random = 랜덤하게 키 삭제 
  • Volatile Random = 랜덤하게 키 삭제하되, 그 대상은 만료 시점이 지정된 것들로 한정 
  • Volatile TTL = TTL 값을 기반으로, 만료 시점이 빨리 도래하는 순서대로 삭제 

 

3. 다양한 데이터 타입 지원 

Redis는 다양한 자료구조를 제공한다. 인기 검색어 Top-10 기능을 구현하기 위해 레디스를 선택한 가장 큰 이유이기도 하다. Redis는 String, Hash, Sorted Set, List, Set과 같은 다양한 자료 구조를 제공한다. 최대 512MB의 key와 512MB의 value까지 지원한다. 반면 Memcached는 별도의 데이터 타입이 없이, 문자열을 저장할 뿐이다. Memcached는 최대 250B의 Key와 1MB의 value를 지원한다. 

 

4. 영속성 

데이터를 저장할 수 있는지의 여부는 매우 중요하다. Redis는 RDB 혹은 AOF 기반으로 데이터를 저장할 수 있는 기능이 있는 반면, Memcached는 들고 있는 데이터를 저장할 수 있는 기능이 없다. 따라서, Memcached는 캐시로서의 기능을 수행할 수 밖에 없는 반면, Redis는 캐시로도 쓰일 수 있고, Data Store로서도 쓰일 수있다. 

 

AOF는 "Append-Only File"의 약자로, Redis에서 사용되는 데이터 지속성 방법 중 하나이다. AOF는 일련의 명령어를 파일에 연속적으로 기록하는 방식으로 동작한다. Redis는 각 명령어를 수행할 때마다 해당 명령어를 AOF 파일에 기록한다. 따라서 Redis의 상태를 복원하는 데에는 모든 명령어를 순서대로 실행하면 된다.

 

  • 데이터 손실이 적음: 명령어를 AOF 파일에 기록하므로 데이터를 잃는 경우가 거의 없다. 
  • 복구가 용이함: AOF 파일에 기록된 모든 명령어를 순서대로 재실행하여 데이터를 복구할 수 있다. 
  • 데이터의 지속성 보장: Redis는 AOF 파일을 사용하여 부팅 후 자동으로 데이터를 복구한다.

 

하지만 AOF 방식은 RDB 방식보다 파일 크기가 크고, RDB보다는 약간 느린 성능을 보일 수 있다. 

 

5. 트랜잭션

레디스는 멀티 명령어를 원자적으로 실행할 수 있는 트랜잭션을 지원한다. 멀티 명령어는 하나의 논리적인 그룹으로 묶여 있으며, 이 그룹 안의 모든 명령어는 원자적으로 실행되어야 한다. 즉, 트랜잭션 내의 명령어 중 하나라도 실패할 경우, 해당 트랜잭션의 모든 변경 사항이 롤백되어 이전 상태로 복원된다. 

 

  • MULTI: 트랜잭션 시작을 알리는 명령어로, 이후의 명령어들이 트랜잭션에 속함을 나타낸다.
  • 여러 개의 명령어: 트랜잭션에 속하는 여러 개의 명령어들을 실행한다.
  • EXEC: 트랜잭션을 실행한다. 이때 트랜잭션 내의 모든 명령어들이 원자적으로 실행된다.
  • DISCARD: 트랜잭션을 취소하고 모든 변경 사항을 롤백한다.

 

6. Pub / Sub messaging

Publish(발행)과 Sub(구독)방식의 메시지를 패턴 검색이 가능하다. 따라서 높은 성능을 요구하는 채팅, 실시간 스트리밍, SNS 피드 그리고 서버상호통신에 사용할 수 있다.

 

7. 스레드 

Memcached는 멀티스레드를 지원하기 때문에, 멀티프로세스코어를 사용할 수 있다. 따라서, 스케일업을 통하여 더욱 많은 작업처리를 할 수 있다. redis는 싱글 스레드로 처리되기 때문에 원자성은 보장되지만 여러 개의 명령어는 처리 못한다. 따라서 시간 복잡도를 고려하며 명령어를 사용해야 한다. 즉, O(N) 명령어는 주의해서 사용한다.

Redis는 싱글 스레드로 동작하기 때문에 동시에 처리할 수 있는 명령어의 갯수는 한번에 하나이다.

예를 들어, CPU 성능에 따라 차이가 있지만 가장 간단한 get/set 과 같은 명령어 기준 1초에 약 10만건의 명령어를 처리할 수 있다. 만약 동시에 10만건의 명령이 전달되었을 때 첫번째 명령에서 3초간 걸린다면 나머지 99,999건의 명령이 3초를 기다리게 되는 상황이 되는 것이다. 

 

# 실제 명령어 동작은 packet 단위로 전달되며 packet이 쌓여 명령 command가 완성되면 명령을 실행한다. 

 

Sorted Set 

Redis는 다양한 형태의 데이터 구조를 사용할 수 있다는 것은 큰 장점이다. Top-10 인기 검색어 정보를 담기 위해 redis가 제공하는 Sorted-Set을 이용하여 (랭킹점수 순으로 sort) 편리하게 랭킹 데이터를 관리하였다. Sorted-Set은 각 원소에 "score"라는 값이 부여되어 있으며, 원소들은 해당 score를 기준으로 정렬된다. 

 

1. Sorted Set에 원소 추가 및 갱신

특정 멤버를 Sorted Set에 추가할 때, 해당 멤버와 그에 대응하는 score 값을 함께 전달한다. 개발자 커뮤니티 사이트에서 사용자가 찾고자 하는 게시물을 찾고자 할 때 검색을 하게 된다. 이때 검색어로 사용한 키워드가 특정멤버가 되며 이미 존재하는 멤버라면 기존 score에서 +1로 갱신된다. 

 

2. 원소 점수에 따른 정렬 

Sorted Set의 원소들은 score에 따라 정렬되어 저장된다. score가 낮은 멤버부터 높은 멤버 순으로 정렬된다. 필자는 내림차순 정렬을 활용하였다. 

 

 

어떤 캐시를 사용해야 할까?

결론적으로 Redis를 사용하였다. 우리 프로젝트에서 토큰 저장 용도로 캐시를 사용하기도 하였지만, Redis에서 제공하는 Sorted-Set 자료구조를 활용하면 인기 검색어 Top-10을 쉽게 구현할 수 있다고 판단하였다. 실시간 랭킹 서버를 구현할 때 관계형 DBMS를 이용한다면 DB 데이터를 저장하고 SCORE 값으로 정렬하여 다시 읽어오는 과정이 필요하고 이러한 흐름은 리소스 낭비 문제가 발생한다. Redis에서 제공하는 간편한 자료구조를 활용하여 빠른 속도로 사용자들에게 서비스를 제공할 수 있다. 또한, Memcached의 대부분의 기능은 Redis로도 커버가 가능하다. Memcached는 In-Memory Key-Value 저장소라 한다면, Redis는 단순한 key-Value 저장소를 넘어 일종의 데이터 구조 스토어라고 할 수 있다. 

반응형

댓글