Redis와 캐시(Cache)

KDG: First things first!·2025년 11월 12일


Redis와 캐시(Cache)란 무엇인가

Redis: Redis(Remote Dictionary Server)란 인메모리 데이터 구조의 저장소이다. 쉽게 말해서 데이터를 메모리(RAM)에 저장하고 관리하여 읽고 쓰는 속도가 매우 빠른 저장소이다.

보통 MySQL, Oracle 같은 RDBMS는 데이터를 디스크(하드)에 저장하기 때문에 읽고 쓸 때마다 파일을 매번 뒤져야 하니까 속도가 좀 느리다. 하지만 Redis는 모든 데이터를 메모리(RAM) 에 넣고 다루기 때문에 응답 일반적인 RDB에 비해 속도가 수십 배에서 수천 배 빠르고 이러한 이유 때문에 Redis가 자주 쓰인다. Redis는 여러 기능을 제공하지만 특히 캐싱(Caching) 기능을 구현하는 데에 자주 사용된다.


캐싱(Caching): 자주 사용되는 데이터를 임시 저장소(캐시)에 복사 또는 저장해두고, 원본 저장소보다 훨씬 빠르게 데이터에 접근하는 캐시 방식을 이용한 성능을 향상시키는 기술이다.


일반적으로 서비스는 클라이언트가 요청을 서버에 보내면 서버에서 DB의 데이터를 사용하는 3 layer 구조를 갖는다.


이용자의 수가 적은 소규모 서비스라면 크게 문제없지만 클라이언트의 수가 늘어나고 서버에 보내는 요청의 수가 늘어나면 자연스럽게 DB에 대한 부하도 크게 늘어나 서비스의 성능에 문제가 생긴다.


DB에 대한 부하를 줄이기 위해 캐시를 도입한다면 DB에 대한 부하도 줄이고 데이터를 이용하는 속도도 크게 높일 수 있어 서비스의 성능을 개선할 수 있다.


Redis는 이러한 캐시를 사용하는 데에 매우 적합한 솔루션이다.

우선 데이터를 단순한 key-value 형태로 저장하고 다양한 형식과 자료구조로 데이터를 저장할 수 있어 어떠한 데이터라도 쉽게 저장할 수 있다. 또한 모든 데이터를 RAM에 저장하는 In-memory 데이터 저장소이기 때문에 평균 작업 속도가 1ms 이하라 초당 수백만 건의 작업이 가능하다. 이렇게 빠른 처리 속도를 가지고 있기 때문에 Redis와 같은 캐시를 적절하게 사용하면 지연 시간(latency)도 감소하고 처리량도 획기적으로 늘릴 수 있어 서비스의 성능을 극대화시킬 수 있다.



캐싱 전략(Caching Stratgies)


캐시를 사용하면 일반적으로 Cache HitCache Miss라는 두 가지 상황 중 하나의 상황이 발생한다.

애플리케이션은 보통 데이터를 찾을 때 캐시 저장소를 먼저 확인한다. 만약 캐시에 찾으려고 하는 데이터가 존재한다면 캐시에서 데이터를 가져오는 것을 반복한다.

이렇게 캐시에 찾으려고 하는 데이터가 존재하는 상황을 Cache Hit이라고 한다.


하지만 애플리케이션이 캐시를 조회했는데 찾고자 하는 데이터가 해당 캐시에 존재하지 않아 DB를 직접 조회해야 하는 상황도 존재할 것이다.

이렇게 캐시에 찾고자 하는 데이터가 없다면 DB를 확인하여 직접 데이터를 갖고 온 후 캐시에 데이터를 저장해야 하는데 이렇게 캐시에 찾고자 하는 데이터가 존재하지 않는 상황을 Cache Miss라고 한다.



Redis를 이용하여 캐싱을 구현할 때에 어떠한 전략을 사용하는지가 서비스의 성능에 큰 영향을 끼치기도 한다. 이때 데이터의 유형과 해당 데이터에 접근하는 엑세스 패턴을 잘 고려하여 선택해야 한다.

캐싱 전략에는 여러 전략이 존재하는데 크게 '읽기 전략'과 '쓰기 전략'으로 나뉜다.


읽기 전략

읽기 전략은 애플리케이션에서 데이터를 읽는 상황이 많을 때 사용하는 전략이다.

읽기 전략에는 Look Aside(Lazy Loading) 전략과 Read Through 전략이 존재한다.


읽기 전략1 : Look-Aside(Lazy Loading)

이 전략은 Redis를 캐시로 쓸 때 가장 일반적으로 많이 사용하는 전략이다.

클라이언트가 애플리케이션에 요청을 보내면 애플리케이션이 캐시 스토어를 조회해서 Cache Hit이면 해당 데이터를 바로 가져온다.


하지만 만약 캐시에 데이터가 없어 Cache Miss가 발생했다면 DB(Aside:옆)에서 데이터를 찾고 해당 데이터를 Cache에 저장하고 애플리케이션에 반환한다.

이렇게 캐시에 찾는 데이터가 없을 때에만 입력되기 때문에 Look-Aside 전략을 다른 말로 Lazy Loading이라고도 부른다.


이 구조는 Redis와 같은 캐시가 다운되더라도 바로 시스템의 장애로 이뤄지지 않고 DB에서 데이터를 갖고 올 수 있다는 장점이 존재한다. 하지만 캐시에 붙어있던 커넥션이 많다면 캐시에 붙어있던 커넥션들이 전부 DB에 붙기 때문에 DB에 갑작스러운 과부하가 발생할 수 있다는 단점도 존재한다.

또한 캐시를 새로 투입하거나 DB에만 새로운 데이터를 저장했다면 초기에 수많은 Cache Miss가 발생해서 서비스의 성능에 저하가 발생할 수 있다.


그래서 이럴 때를 대비해서 미리 DB에서 캐시로 데이터를 밀어넣어 주는 작업을 할 수 있는데 이러한 작업을 Cache Warming이라고 한다.


이외에도 Look Aside 전략은 캐시와 DB간의 별다른 커넥션이 존재하지 않아 동기화가 제대로 이루어지지 않아 DB와 캐시간의 데이터가 서로 일치하지 않는 데이터 정합성 문제가 발생하기 쉽다.



읽기 전략2 : Read Through

Read Through 전략은 항상 캐시를 통해서만 데이터를 읽어오는 전략이다.

Cache Hit이라면 그대로 캐시에서 데이터를 가져온다.


하지만 Cache Miss라면 애플리케이션이 아니라 캐시 스토어가 DB에서 직접 데이터를 가져와 애플리케이션에 반환하는 구조이다.


이 전략의 장점은 캐시와 DB간에 커넥션이 존재하기 때문에 동기화가 이루어져 데이터의 정합성이 보장된다는 것이다. 하지만 단점으로는 만약 캐시가 다운된다면 이는 서비스 전체의 장애로 이루어질 수 있다.



쓰기 전략

쓰기 전략에는 Write Around, Write Back, Write Throug라는 3가지 전략이 존재한다.



쓰기 전략1 : Wrtie Around

Write Around 전략은 캐시를 우회해서 직접 쓰는 전략이다.

이 전략은 일반적인 애플리케이션 구조와 같이 Write할 때에는 데이터를 DB에 직접 주입한다.


하지만 데이터를 읽어올 때 캐시를 조회하고 만약 Cache Miss가 발생한다면 DB에서 데이터를 조회한 후 해당 데이터를 반환함과 동시에 캐시에 해당 데이터를 저장한다.


Write Around 전략은 DB에 직접 쓰기 때문에 성능이 좋고 불필요한 데이터는 캐시에 저장하지 않기 때문에 리소스를 절약할 수 있다는 장점이 존재한다.

하지만 캐시 스토어와 DB에 별다른 커넥션이 존재하지 않기 때문에 양측의 데이터가 서로 다를 수 있어 데이터 정합성 유지가 까다롭다는 단점도 존재한다.




쓰기 전략2 : Write Back


이 전략은 DB가 아니라 캐시 스토어에 데이터들을 먼저 저장(write)한 후 scheduing을 통해 주기적으로, 그리고 일괄적으로 캐시에 쌓아둔 새로운 데이터들을 DB에 저장하는 방식이다.


이 전략을 통해 한꺼번에 많은 양의 데이터들을 한 번의 쓰기 요청으로 DB에 저장할 수 있다. 데이터가 저장될 때마다 일일이 insert문을 날려야 하는 기존의 방식과는 한 번의 insert문으로 많은 양의 데이터를 저장할 수 있어 쓰기 비용 횟수를 획기적으로 줄일 수 있다는 성능상의 장점이 존재한다.

하지만 DB로 데이터를 옮기기 전에 캐시 스토어나 캐시 스토어에 저장된 데이터에 문제가 생기거나 하면 데이터가 유실될 수 있다는 단점도 존재한다.




쓰기 전략3 : Write Through

해당 전략은 항상 캐시를 통해서만 쓰기를 진행하는 방식이다.


이 전략은 캐시에 데이터를 저장한 후 캐시에서 DB에 데이터를 바로 저장한다.


해당 방식은 항상 캐시를 거친 후에 DB에 도착하기 때문에 데이터의 정합성이 보장된다는 장점이 존재한다.

하지만 매번 데이터를 write할 때마다 무조건 두 번의 쓰기가 발생하기 때문에 성능적으로 떨어진다는 단점이 존재한다. 또한 재사용되지 않는 데이터들도 전부 캐시에 저장하기 때문에 불필요하게 낭비되는 리소스가 발생하기 쉽다.




Redis의 데이터 타입 종류

Redis는 key-value 형태로 데이터들을 저장하는데 실질적인 데이터가 저장되는 value에 다양한 데이터 타입을 제공하여 다양한 데이터 타입으로 데이터를 저장할 수 있다.


우선 String은 Redis에서 가장 기본적인 데이터 저장 타입이다. set 커맨드를 통해 저장되는 데이터들은 모두 String 형태로 저장된다.


Bitmap은 String의 변형 타입이고 비트 단위의 연산이 가능하다.


데이터를 순서대로 저장하는 ListQueue로 사용하는 데에 적합한 데이터 타입이다.


Hash는 하나의 key 안에 또 여러 개의 key-value가 존재하는 형태이다.


Set은 중복되지 않는 문자열들의 집합이다.


Sorted Set은 Set처럼 중복되지 않는 값을 저장하지만 모든 값을 score라는 특정 필드를 지정해 해당 값의 순서대로 정렬(기본은 오름차순, score 동일하면 사전순)하는 방식이다. 랭킹 같은 서비스를 구현하는 데에 자주 사용된다.


HyperLogLog는 굉장히 많은 데이터를 다룰 때 사용되면 중복되지 않는 값의 개수를 count할 때 자주 사용된다.


Stream은 로그를 저장하기 가장 좋은 자료구조이다.



그렇다면 해당 데이터 타입들을 어떠한 방식으로 사용할 수 있을까??

몇 가지 사용 예시를 살펴보자.




Redis의 데이터 타입 사용 예시

1. Counting 예시

우선 데이터 Counting이 필요한 상황을 생각해보자.

Redis에서 카운팅하기에 가장 쉬운 방법은 key를 만들고 카운팅이 필요한 상황마다 수치를 1씩 증가시키는 방법이다. 이건 StringINCR(Increment) 함수를 사용하면 간단하게 구현이 가능하다.

예제를 통해 살펴보자.

우선 score:a라는 키를 생성하고 해당 키에 10을 저장한 후에 INCR 함수를 사용하면 1이 증가하여 11이 된다.

여기서 INCRBY 함수를 사용하여 값을 4로 지정하면 4가 증가하여 15가 되는 것을 확인할 수 있다.



두 번째 Counting 방식은 Bit 연산을 사용하는 방식이다.

해당 방식을 이용하면 저장 공간을 많이 절약할 수 있다. 예를 들어 서비스에 접속한 유저 수를 카운팅하고 싶다면 날짜 키를 생성하고 유저 ID에 해당하는 비트를 1로 올려준다. 1개의 비트가 1명을 의미하므로 천 만 명 개의 비트는 천 만 명의 접속자를 표현 가능하지만 1.2MB의 매우 작은 저장 공간만 차지한다.

하지만 해당 방식을 사용하려면 모든 데이터를 정수로 표현가능해야 된다는 전제 조건이 존재한다. 유저 ID가 0 이상의 정수 값이라면 해당 방식을 사용할 수 있을 것이다.



마지막 카운팅 방식은 HyperLogLog를 사용하는 방식이다. HyperLogLog는 모든 String 데이터 값을 유니크하게 구별할 수 있다. 이는 Set과 비슷하지만 대량의 데이터를 카운팅할 때에 훨씬 더 적절하다. 그 이유는 HyperLogLog는 저장되는 데이터의 개수와 상관없이 모든 값이 12KB로 고정되어 저장되기 때문이다.

대신에 한 번 저장된 값은 다시 불러올 수는 없는데 경우에 따라 데이터를 보호하기 위한 목적으로 사용할 수 있다. 예를 들어 웹사이트에 방문한 IP가 몇 개나 되는지, 크롤링한 URL의 개수가 몇 개인지, 검색 엔진에서 검색된 단어가 몇 개인지 등의 엄청 크고 유니크한 값을 카운팅할 때 매우 적절하다.

PFADD 커맨드로 데이터를 저장하고 PFCOUNT로 저장된 데이터를 조회할 수 있다.

만약 일별로 데이터를 저장했는데 일주일치를 취합해서 보고 싶다면 PFMERGE로 키들을 merge할 수 있다.




2. Messaging 예시

Redis의 ListMessage(Event) Queue로 사용하기 적절하다.

특히 자체적으로 blocking (어떤 작업을 실행할 때, 해당 작업의 완료를 기다리느라 다른 작업들이 중단되고 대기하는 상태) 기능을 제공하기 때문에 이를 적절히 사용하면 불필요한 polling (하나의 장치가 다른 장치의 상태를 주기적으로 확인하여, 특정 조건이 만족되었을 때 데이터를 처리하는 방식)을 막을 수 있다.


아래 예제를 통해 살펴보자.

클라이언트 A가 BRPOP 커맨드를 통해 myqueue라는 리스트에서 데이터를 꺼내오려고 하지만 현재 리스트 안에 데이터가 없어 대기를 하고 있는 상황이라고 가정해보자.


이 때 클라이언트 B가 "hi"라는 값을 넣어주면 클라이언트 A에서 해당 값을 바로 확인할 수 있다.



LPUSHXRPUSHX 같은 커맨드를 사용하면 키가 있을 때에만 리스트에 데이터를 추가하는데 이를 잘 사용하면 굉장히 유용하다.

키가 이미 있다는 것은 사용된 적이 있는 큐라는 의미이고 사용했던 큐에만 메시지를 넣어줄 수 있기 때문에 비효율적인 데이터의 이동을 방지할 수 있다.

예시로 인스타그램, 페이스북, 트위터 같은 SNS에는 각 유저별로 타임라인이 존재하고 타임라인에 팔로워들의 데이터가 나오는데 트위터에서는 각 유저의 타임라인에 보일 트윗을 캐싱하기 위해 레디스의 리스트를 사용하는데 이 때 RPUSHX 커맨드를 사용한다. 이를 이용하여 트위터를 자주 사용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시해놓을 수 있으며, 자주 사용하지 않는 유저는 캐싱 키가 존재하지 않기 때문에 트위터를 자주 사용하지 않는 유저들의 데이터를 미리 쌓아놓는 비효율적인 리소스 낭비를 방지하고 있다.



Redis에서 메시징하는 마지막 방법은 Stream이다.

Stream은 로그를 저장하기 가장 적절한 자료구조이다. 실제 서버에 로그가 쌓이는 것처럼 모든 데이터는 append-only (모든 쓰기(write) 연산을 파일에 순차적으로 추가하여 저장) 방식으로 저장되며 중간에 데이터가 바뀌지 않는다.

예제에서는 XADD라는 커맨드를 이용하여 mystreams라는 키에 데이터를 저장하고 있다. 키 이름 옆의 *은 ID 값을 의미하며 별표를 사용하면 레디스가 알아서 저장하고 ID 값을 반환해준다.
반환된 값은 데이터가 저장된 시간을 의미한다. 이 값 뒤로는 해시처럼 key-value 형태로 데이터가 저장되는데 예제에서는 sensor ID 값에 1234를, 온도에는 19.8를 저장하였다.

Steram의 데이터를 읽는 방법은 다양한데 ID 값을 이용하여 시간 대역대로 저장된 값을 검색할 수 있고 실제 서버에서 로그를 읽는 것처럼 ail-f (리눅스에서 파일의 마지막 내용을 실시간으로 모니터링하는 명령어)를 사용하는 것처럼 새로 들어오는 데이터만 리스닝할 수 있다.

또한 카프카처럼 소비자 그룹(Consumer Group)이라는 개념이 존재하기 때문에 원하는 소비자만 특정 데이터를 읽게 할 수도 있다.
실제로 Stream은 카프카의 개념을 많이 차용했는데 레디스 공식 문서에서 Stream은 메시지 브로커가 필요할 때 카프카를 대체해서 간단하게 사용할 수 있는 자료구조라고 소개하고 있다.




Redis 데이터 영구 저장(RDB vs AOF)


Redis는 In-memory 데이터 스토어이기 때문에 모든 데이터가 메모리에 저장되어 서버나 레디스 인스턴스가 재시작되면 모든 데이터가 유실된다. 복제 구조를 사용하고 있더라도 코드상의 버그나 사람의 실수가 발생하여 데이터 유실이 발생하면 복제본에도 똑같이 적용되기 때문에 데이터 유실에 완전히 안전하다고는 할 수 없다.

따라서 Redis를 단순 캐시 이외의 용도로 사용하려면 적절한 데이터의 백업은 필수적이다.



Redis에서는 데이터를 영구 저장하는 데에 2가지 방법을 제공하고 있다.

그 중 하나인 AOF(Append Only File)은 데이터를 변경하는 커맨드가 들어오면 커맨드를 그대로 모두 저장한다.

다른 하나인 RDB는 스냅샷 방식으로 작동하기 때문에 저장 당시의 메모리에 있는 데이터를 그대로 사진 찍듯이 찍어서 파일로 저장한다.


AOF에는 key1에 a가 저장되었다가 apple로 변경되고 key2가 들어왔다가 삭제된 기록이 모두 남아있다.

하지만 RDB에는 key1이 apple인 값만 남아있다.

즉, AOF는 Append Only하게 동작하기 때문에 데이터가 추가되기만 하여 대부분의 경우 RDB 파일보다 용량이 크다. 따라서 AOF 파일은 주기적으로 압축하여 재작성되는 과정을 거쳐야 한다.

(참고로 위의 예제 그림은 단순히 설명을 돕기 위한 예시고 AOF 파일은 레디스 프로토콜 형태로 저장되고, RDB 파일은 바이너리 파일 형태로 저장되기 때문에 직접 읽을 수는 없다.)


이제 이 두 가지 파일은 생성하는 방법을 알아보자.


AOF 파일과 RDB 파일 모두 커맨드를 이용하여 파일을 직접 생성할 수 있으며 원하는 시점에 파일이 자동 생성되도록 설정할 수도 있다.

RDB의 경우 시간 단위로 파일을 저장할 수 있고, AOF의 경우 파일의 크기를 기준으로 해서 압축되는 시점을 설정할 수 있다.


그렇다면 2가지 방법 중에 어떠한 방법을 선택해야 할까???(단순히 Redis를 캐시용으로만 사용한다면 영구 저장할 필요는 없다.)



만약 데이터 백업이 필요한데 어느 정도의 데이터 손실은 감수할 수 있다면 RDB만 사용해도 된다. 단, redis.conf 파일에 'SAVE' 옵션을 적절히 사용해야 한다.

위의 예시의 'SAVE 900 1'이라는 커맨드를 통해 900초 동안 1개 이상의 키가 변경되었을 때 RDB 파일을 재작성하라는 의미이다.


하지만 만약 장애 상황 직전까지의 모든 데이터가 보장되어야 하는 경우라면 AOF를 사용해야 한다. 이 때 APPENDFSYNC 옵션이 기본값인 every second의 경우라면 최대 1초 사이의 데이터는 유실될 가능성이 존재한다.


마지막으로 가장 강력한 내구성이 필요하다면 RDB와 AOF를 동시에 사용하라고 Redis 공식 문서에 기재되어 있다.




Redis 아키텍처(Repulication vs Sentinel vs Cluster)


Redis는 간단히 3개의 아키텍처로 나눌 수 있다.


리플리케이션 구성, 즉 복제 구성은 마스터 노드와 마스터를 복제하는 리플리카 노드만 존재하는 가장 간단한 구조이다.

센티널 구성은 마스터와 리플리카 노드 외에 추가로 각 노드들이 제대로 작동하고 있는지 계속 모니터링하는 센티널 노드들이 필요하다.

클러스터 구성에서는 최소 3대의 마스터가 필요하고 샤딩 기능을 제공한다.



리플리케이션 구성은 단순히 복제만 구현된 구조이다. 모든 Redis의 구조에서는 복제는 비동기식으로 동작하여 마스터에서 복제본에 데이터가 잘 전달되었는지 매번 확인하고 기다리지 않는다.

이 구조는 HA 기능(고가용성(High Availability), 하드웨어나 소프트웨어 장애 발생 시에도 서비스가 중단되지 않도록 시스템 이중화를 통해 안정성을 확보하는 기술)이 없기 때문에 마스터에 장애가 발생하면 수동으로 여러 작업들을 변경해줘야 한다.

우선 리플리카 노드에 직접 접속하여 복제를 끊어야 하고, 애플리케이션에서도 연결 설정을 변경하여 배포하는 작업이 필요하다.



센티널 노드는 일반적으로 다른 노드들을 계속 모니터링하는 역할을 담당하는데 그러다 마스터 노드가 죽으면 자동으로 fail over (시스템, 서버, 네트워크 등에서 장애가 발생했을 때 예비 시스템이 자동으로 주 시스템을 대신하여 서비스를 이어받는 기능)을 발생시켜 기존의 리플리카 노드 중 하나가 마스터 노드로 승격된다.

이 때 애플리케이션에서는 연결 정보를 변경할 필요 없이 센티널 노드만 알고 있으면 되고 센티널 측에서 변경된 마스터 노드로 자동으로 설정을 변경해준다.

센티널 구조를 사용하기 위해서는 센티널 프로세스를 추가로 띄워야 하는데 이때 마스터 노드가 fail over되였는지 여부를 센티널들의 과반수 투표를 통해 결정하기 때문에 센티널은 항상 3대 이상의 홀수여야 한다.



클러스터 구성에서는 데이터가 여러 마스터 노드에 분할되어 저장되는 샤딩 기능을 제공한다. 이 구성에서는 모든 노드가 서로를 감시하고 있다가 마스터가 비정상 상태라고 판단되면 자동으로 fail over를 진행한다.

이를 위해서는 최소 3대 이상의 마스터 노드가 필요하며 각 마스터 노드에 리플리카 노드를 하나씩 추가하는 게 일반적인 구조이다.



그렇다면 우리 서비스에는 어떠한 아키텍처를 선택해야 할까??? 간단하게 정리해보자.



먼저 복제 기능이 필요한지, 자동 HA 기능(fail over) 필요한지의 여부를 판단하고 필요하다면 샤딩 기능도 필요한지 판단해보자.

서비스의 scale out이 필요하여 샤딩이 필요하다면 클러스터 구조를 사용하고, 그게 아니라 HA 기능만 필요하다면 센티널 구조를, HA도 필요없고 복제만 필요하다면 리플리카 구조를, 전부 필요없다면 마스터 노드 1개만 사용하는 스탠드 얼론 구조를 선택하자.





자료 출처:
https://www.youtube.com/watch?v=tVZ15cCRAyE
https://www.youtube.com/watch?v=92NizoBL4uA

profile
알고리즘, 자료구조 블로그: https://gyun97.github.io/

0개의 댓글