개발자를 위한 레디스 튜토리얼 02

레디스, 제대로 알고 쓰나요? — 레디스 제대로 활용하는 법

GARIMOO
garimoo
9 min readJan 30, 2020

--

지난 이야기를 읽고 오시면 더 재미있을 거에요👇🏻

캐시로서의 레디스

이번에는 지난번 살짝 훑어봤던 레디스 자료구조의 실제 활용 사례를 소개하기에 앞서, 캐시로서의 레디스에 대해 짚고 넘어가고자 합니다. 대부분의 서비스에서는 레디스를 단순 캐시용도로 사용하길 원할 것이라 생각합니다. 이때 레디스를 어떻게 배치하는지가 시스템의 성능에 큰 영향을 미칠 수 있습니다. 캐싱 전략은 캐싱되는 데이터의 유형과 해당 데이터에 대한 액세스 패턴에 따라 달라지기 때문에, 이 설명이 모든 서비스에 적절하지는 않다는 점 참고 부탁드립니다.

Look Aside (= Lazy Loading)

이름에서 알 수 있듯이 이 구조는 캐시를 옆에 두고 필요할 때만 데이터를 캐시에 로드하는 캐싱 전략입니다. 캐시는 데이터베이스와 어플리케이션 사이에 위치하여 단순 key-value 형태를 저장합니다. 어플리케이션에서 데이터를 가져올 때 레디스에 항상 먼저 요청하고, 데이터가 캐시에 있을 때에는 레디스에서 데이터를 반환합니다. 데이터가 캐시에 없을 경우 어플리케이션에서 데이터베이스에 데이터를 요청하고, 어플리케이션은 이 데이터를 다시 레디스에 저장합니다. 아래 그림은 이 프로세스를 나타내고 있습니다.

위 구조를 사용하면 실제로 사용되는 데이터만 캐시할 수 있고, 레디스의 장애가 어플리케이션에 치명적인 영향을 주지 않는다는 장점을 가지고 있습니다.하지만 캐시에 없는 데이터를 쿼리할 때 더 오랜 시간이 걸린다는 단점과 함께, 캐시가 최신 데이터를 가지고 있다는 것을 보장하지 못하는 단점이 있습니다. 캐시에 해당 key 값이 존재하지 않을 때만 캐시에 대한 업데이트가 일어나기 때문에 데이터베이스에서 데이터가 변경될 때에는 해당 값을 캐시가 알지 못하기 때문입니다.

Write-Through

Write-Through 구조는 데이터베이스에 데이터를 작성할 때마다 캐시에 데이터를 추가하거나 업데이트합니다. 이로 인해 캐시의 데이터는 항상 최신 상태로 유지할 수 있지만, 데이터 입력 시 두 번의 과정을 거쳐야 하므로 지연 시간이 증가한다는 단점이 존재합니다. 또한 사용되지 않을 수도 있는 데이터도 일단 캐시에 저장하기 때문에 리소스 낭비가 발생합니다. 이를 해결하기 위해 데이터 입력 시 TTL을 꼭 사용하여 사용되지 않는 데이터를 삭제하는 것을 권장합니다.

레디스 활용 사례

좋아요👍🏻 처리하기

게시물에 달린 댓글에 좋아요 를 표현할 수 있는 기능이 없을 때,레디스를 사용해서 간단하게 구현하는 방법에 대해 알아보겠습니다.

가장 중요한 것은 한 사용자가 하나의 댓글에 한번 만 좋아요 를 할 수 있도록 제한하는 것입니다. RDBMS에서는 유니크 조건을 생성해서 처리할 수 있습니다. 하지만 만약 많은 입력이 발생하는 환경에서 RDBMS을 이용한다면 insert와 update에 의한 성능 저하가 필연적으로 발생하게 됩니다.

레디스의 set을 이용하면 이 기능을 간단하게 구현할 수 있으며, 빠른 시간 안에 처리할 수 있습니다. set은 순서가 없고, 중복을 허용하지 않는 집합입니다. 댓글의 번호를 사용해서 key를 생성하고, 해당 댓글에 좋아요를 누른 사용자의 ID를 아이템으로 추가하면 동일한 ID값을 저장할 수 없으므로 한 명의 사용자는 하나의 댓글에 한번 만 좋아요를 누를 수 있게 됩니다.

제디스(java의 redis 라이브러리) 를 통한 파이프라인을 사용하여 이 기능을 구현한다고 가정했을 때, 초당 약 16만 건의 커맨드를 처리할 수 있습니다. RDBMS와 비교했을 때 확연히 빠른 속도입니다.

게임 서비스🕹에서 일일 순 방문자수(Unique Visitor) 구하기

순 방문자수(UV)는 서비스에 사용자가 하루에 여러번 방문했다 하더라도 한번 만 카운팅 되는 값입니다. 즉 중복 방문을 제거한 방문자의 지표라고 생각할 수 있습니다. 많은 서비스에서 이 수치를 이용해 사용자의 동향을 파악하고, 마케팅을 위한 자료로 활용하기도 합니다. 실제 서비스에서는 이를 구하기 위해서 대표적으로 세 가지 방법을 사용합니다. 첫 번째로 액세스 로그(access log)를 분석하는 방법, 두 번째로 외부 서비스(ex. Google Analytics)의 도움을 받는 방법, 세 번째로는 접속 정보를 로그 파일로 작성하여 배치 프로그램으로 돌리는 방법입니다. 이 세 가지 방법 중 GA를 제외하고는 정보를 실시간으로 조회할 수 없습니다.

그렇다면 이제 레디스의 비트 연산을 활용하여 간단하게 실시간 순 방문자를 저장하고 조회하는 방법을 알아보겠습니다. 게임의 유저는 천만 명이라 가정하고, 일일 방문자 횟수를 집계하며 이 값은 0시를 기준으로 초기화됩니다.

사용자 ID는 0부터 순차적으로 증가된다고 가정하고, string의 각 bit를 하나의 사용자로 생각할 수 있습니다. 사용자가 서비스에 방문할 때 사용자 ID에 해당하는 bit를 1로 설정합니다. 1개의 bit가 1명을 의미하므로, 천만 명의 유저는 천만 개의 bit로 표현할 수 있고, 이는 곧 1.2MB정도의 크기입니다. 레디스 string의 최대 길이는 512MB이므로 천만 명의 사용자를 나타내는 건 충분합니다.

2020년 1월 29일에 ID가 7인 사용자가 방문했다면 위 그림처럼 일곱 번째 인덱스를 1로 설정합니다. 이날에 서비스에 방문한 총방문자 수를 조회하기 위해서는 이 문자열에서 1로 설정된 bit의 개수를 구하는 BITCOUNT 연산을 사용하여 간단히 구할 수 있습니다.

만약 출석 이벤트 등을 진행하기 위해 정해진 기간 동안 매일 방문한 사용자를 구하고 싶을 수 있습니다. 이때는 레디스의 BITOP 커맨드를 사용하면 간단합니다. 레디스 서버에서 바로 AND, OR, XOR, NOT 연산을 할 수 있으므로, 레디스에서 개별 비트를 가져와서 서버에서 처리하는 번거로움을 줄여줍니다.

2020년 1월 29일부터 31일까지 매일 접속한 사용자는 id가 7인 사용자와 11인 사용자라는 것을 BITOP을 이용한 AND 연산을 통해 쉽게 구할 수 있습니다.

최근 검색 목록🔎 표시하기

사내 협업도구의 프로젝트에서 담당자를 선택할 때 매번 멤버를 검색해서 입력해야 하는데, 최근 검색했던 담당자가 보여지면 좋을 것 같다는 개선 의견을 본 적이 있습니다.

이 기능을 관계형 데이터베이스를 이용해 구현하려면 아래와 비슷한 쿼리문이 필요합니다.

select * from KEYWORD where ID = 123 order by reg_date desc limit 5;

이 쿼리는 사용자가 최근에 검색했던 테이블에서 최근 5개의 데이터를 조회합니다. 하지만 이렇게 RDBMS의 테이블을 이용해서 데이터를 저장한다면 중복 제거도 해야 하고, 멤버별로 저장된 데이터의 개수를 확인하고, 오래된 검색어는 삭제하는 작업까지 이루어져야 합니다.

따라서 애초에 중복을 허용하지 않고, 정렬되어 저장되는 레디스의 sorted set을 사용하면 간단하게 구현할 수 있습니다. sorted set은 가중치를 기준으로 오름차순으로 정렬되기 때문에, 가중치로 시간을 사용한다면 이 값이 가장 큰, 나중에 입력된 아이템이 맨 마지막 인덱스에 저장됩니다.

멤버 ID가 123인 사람이 최근 검색한 사람은 위 그림처럼 정렬되어 저장됩니다. 이때 가중치는 입력 순간의 나노세컨드이고, 가장 처음 검색한 사람의 ID는 46, 가장 마지막 검색한 사람은 50입니다. 이때 ID가 51인 사람을 검색하면 아래처럼 마지막에 데이터가 추가됩니다.

항상 다섯 명만 저장하기 위해서는 인덱스가 0인 아이템을 지우면 됩니다. 하지만 아이템 개수가 6보다 작을 때에는 0번째 인덱스를 삭제하면 안 되기 때문에 매번 아이템의 수를 먼저 확인해야 하는 번거로움이 있습니다. 이때 sorted set의 음수 인덱스를 사용한다면 더 간단해집니다. 음수 인덱스는 인덱스의 마지막부터 큰 값부터 작은 값으로 매겨지는데요, 아래 그림과 같습니다.

데이터에 멤버를 추가한 뒤, 항상 -6번째 아이템을 지운다면 특정 개수 이상의 데이터가 저장되는 것을 방지 할 수 있게 됩니다. 인덱스로 아이템을 지우려면 ZREMRANGEBYRANK 커맨드를 사용하면 간단합니다. 이렇게 레디스의 sorted set을 이용하면 많은 공수를 들이지 않고도 최근 검색한 담당자를 보여줄 수 있는 기능을 구현할 수 있게 됩니다. 참 쉽죠?😄

출처

--

--