Redis는 왜 빠를까? 이벤트 루프부터 RDB·AOF까지
비동기 작업을 처리하기 위해 Redis Streams를 검토하면서 가장 먼저 든 의문은 Redis 자체가 어떻게 동작하는가였다.
Redis는 흔히 메모리를 사용하고 싱글 스레드로 동작하기 때문에 빠르다고 설명한다. 하지만 메모리를 사용하는 것과 싱글 스레드라는 특징만으로는 수많은 연결을 처리하는 과정이나, 느린 명령 하나가 전체 응답을 지연시키는 이유를 설명하기 어렵다.
또한 Redis를 실제 서비스에 사용하려면 속도뿐만 아니라 장애가 발생했을 때 데이터를 어떻게 복구하고, 어떤 방식으로 서버를 이중화할지도 알아야 한다.
이번 글에서는 Redis의 명령 실행 구조와 이벤트 루프를 먼저 살펴본 뒤 RDB, AOF, Sentinel, Cluster가 각각 어떤 문제를 해결하는지 정리해보고자 한다.
1. Redis란?
Redis는 데이터를 메모리에 저장하는 데이터 저장소이다. 단순한 문자열뿐 아니라 Hash, List, Set, Sorted Set, Stream과 같은 자료구조를 제공하며, 각 자료구조에 맞는 명령을 서버에서 직접 실행할 수 있다.
일반적인 디스크 기반 데이터베이스는 데이터를 찾는 과정에서 디스크 I/O가 발생할 수 있다. 반면 Redis는 주 데이터를 메모리에 두기 때문에 데이터 접근 시간이 짧다.
하지만 Redis가 빠른 이유를 단순히 “메모리는 디스크보다 빠르다”로만 설명할 수는 없다. Redis는 명령을 처리하는 방식도 짧은 응답 시간에 맞춰 설계되어 있다.
2. Redis는 정말 싱글 스레드인가?
Redis 공식 문서는 Redis를 mostly single threaded 구조라고 설명한다. 핵심은 클라이언트 명령의 실행 경로이다. Redis는 대부분의 명령을 하나의 메인 실행 흐름에서 순차적으로 처리한다.
예를 들어 두 클라이언트가 거의 동시에 다음 명령을 보냈다고 가정해보자.
Client A → INCR counter
Client B → GET counter
Redis는 두 명령의 자료구조 연산을 여러 스레드에서 동시에 실행하지 않는다. 메인 스레드가 한 명령을 처리한 뒤 다음 명령을 처리한다.
이 구조에는 다음과 같은 특징이 있다.
- 같은 데이터에 접근하는 명령 사이에 복잡한 락을 둘 필요가 적다.
- 명령 실행 중 다른 작업 스레드로 전환하는 비용을 줄일 수 있다.
- 하나의 명령은 중간에 다른 명령이 끼어들지 않고 처리된다.
- 반대로 하나의 명령이 오래 걸리면 뒤에 기다리는 요청도 함께 지연된다.
여기서 “싱글 스레드”를 Redis 프로세스에 스레드가 하나만 존재한다는 뜻으로 이해하면 안 된다. Redis는 디스크 I/O와 같은 백그라운드 작업에 별도의 스레드나 프로세스를 사용한다. Redis 6부터는 설정에 따라 네트워크 읽기와 쓰기의 일부를 I/O 스레드에 맡길 수도 있다.
다만 네트워크 I/O에 여러 스레드를 활용하는 것과 Redis 명령을 여러 스레드에서 병렬 실행하는 것은 다른 이야기이다. Redis의 핵심 자료구조 연산은 여전히 주로 메인 스레드에서 순차적으로 처리된다.
3. 한 스레드로 여러 연결을 처리하는 이벤트 루프
클라이언트 연결마다 하나의 스레드를 만들지 않는다면 Redis는 어떻게 많은 연결을 처리할 수 있을까?
Redis는 이벤트 루프와 I/O multiplexing을 사용한다. 운영체제에 여러 소켓을 등록해두고, 그중 읽거나 쓸 준비가 된 소켓이 무엇인지 한 번에 확인하는 방식이다.
전체 흐름을 단순화하면 다음과 같다.
1. 여러 클라이언트 소켓을 감시한다.
2. 읽을 데이터가 준비된 소켓을 확인한다.
3. 요청을 읽고 명령을 실행한다.
4. 응답을 쓸 수 있는 소켓에 결과를 보낸다.
5. 다시 준비된 이벤트를 확인한다.
어떤 클라이언트가 다음 데이터를 보낼 때까지 메인 스레드가 해당 연결만 붙잡고 기다리지 않는다. 현재 처리할 수 있는 연결을 골라 짧게 처리한 뒤 다시 이벤트 루프로 돌아간다.
이것이 Non-blocking I/O가 중요한 이유이다. 네트워크 대기 시간 동안 스레드가 멈춰 있는 대신, 실행할 수 있는 다른 연결의 작업을 처리할 수 있다.
4. Redis가 빠른 이유
Redis의 성능은 하나의 특징이 아니라 여러 조건이 함께 작동한 결과이다.
4.1. 데이터를 메모리에서 조회한다
주 데이터가 메모리에 있으므로 일반적인 조회와 변경에서 디스크의 임의 접근을 기다리지 않는다. 영속화를 사용하더라도 클라이언트 명령이 매번 디스크에 저장된 자료구조를 직접 찾아가는 방식은 아니다.
4.2. 대부분의 명령이 짧다
GET, SET, HSET처럼 자주 사용하는 명령은 작은 범위의 자료를 빠르게 다룬다. 명령 하나가 짧게 끝나면 순차 실행 구조에서도 다음 요청으로 빠르게 넘어갈 수 있다.
4.3. 명령 실행 경로가 단순하다
여러 작업 스레드가 같은 자료구조를 동시에 변경하지 않으므로 락 경합을 줄일 수 있다. 요청마다 별도 스레드를 배정하는 모델과 비교하면 명령 실행을 위해 작업 스레드를 계속 전환하는 비용도 적다.
다만 “컨텍스트 스위칭이 전혀 없다”는 뜻은 아니다. 운영체제에는 Redis 외에도 여러 프로세스와 스레드가 실행되고, Redis도 백그라운드 작업을 사용한다. 정확한 표현은 클라이언트 명령마다 작업 스레드를 바꾸는 구조가 아니라는 것이다.
4.4. 네트워크 대기를 이벤트로 처리한다
연결 하나의 입출력을 기다리면서 전체 서버가 멈추지 않는다. 이벤트 루프는 현재 처리할 수 있는 소켓을 중심으로 작업을 이어간다.
4.5. 요청 왕복 횟수를 줄일 수 있다
명령 하나의 실행 시간이 짧더라도 네트워크 왕복이 반복되면 전체 응답 시간은 커진다. Redis는 여러 명령을 한 번에 보내는 pipelining과 MGET, MSET 같은 다중 key 명령을 제공한다.
5. 싱글 스레드 구조에서 기다리면 안 되는 이유
Redis의 장점은 개별 명령이 짧게 끝난다는 전제에서 잘 드러난다. 반대로 큰 데이터를 한 번에 순회하는 명령이 들어오면 메인 스레드가 그 명령을 끝낼 때까지 다른 요청을 처리하지 못한다.
대표적인 예가 운영 환경에서의 KEYS *이다. KEYS는 전체 key space를 순회하므로 데이터가 많을수록 실행 시간이 길어진다. Redis 공식 문서도 일반 애플리케이션 코드에서는 KEYS 대신 점진적으로 key를 탐색하는 SCAN 사용을 권장한다.
자료구조 안에 너무 많은 원소를 넣은 뒤 HGETALL, SMEMBERS처럼 전체 원소를 반환하는 명령을 사용하는 것도 같은 문제를 만들 수 있다.
따라서 Redis 명령을 선택할 때는 다음을 함께 확인해야 한다.
- 명령의 시간 복잡도는 얼마인가?
- 하나의 key에 원소가 얼마나 많이 쌓일 수 있는가?
- 응답 데이터의 크기는 어느 정도인가?
- 운영 환경에서 전체 key를 한 번에 조회하고 있지는 않은가?
- Slow Log에 오래 걸리는 명령이 기록되고 있지는 않은가?
Redis가 싱글 스레드라서 빠르다는 설명의 반대편에는, 느린 명령을 메인 실행 흐름에 넣으면 전체가 함께 기다린다는 조건이 존재한다.
6. 메모리의 데이터를 어떻게 남길까?
Redis의 주 데이터는 메모리에 있다. 프로세스나 서버가 종료된 뒤에도 데이터를 복구해야 한다면 메모리 상태를 디스크에 기록해야 한다.
Redis Open Source는 대표적으로 RDB와 AOF 두 가지 영속화 방식을 제공한다.
6.1. RDB
RDB는 특정 시점의 데이터 전체를 스냅샷 파일로 저장한다.
Redis가 RDB를 만들 때는 일반적으로 자식 프로세스를 생성하고, 자식 프로세스가 메모리의 데이터를 파일로 기록한다. 부모 프로세스는 계속 클라이언트 요청을 처리한다. 이 과정은 운영체제의 Copy-on-Write를 이용한다.
RDB의 특징은 다음과 같다.
- 하나의 압축된 스냅샷 파일로 백업하기 편하다.
- 큰 데이터셋을 비교적 빠르게 복원할 수 있다.
- 마지막 스냅샷 이후의 변경은 장애 시 유실될 수 있다.
- 데이터가 클 때
fork()와 Copy-on-Write로 인한 메모리·지연 부담을 고려해야 한다.
6.2. AOF
AOF(Append Only File)는 데이터셋을 변경한 쓰기 명령을 로그에 이어 붙인다. Redis를 다시 시작할 때 이 명령을 순서대로 재실행해 데이터를 복원한다.
AOF의 fsync 정책에 따라 성능과 유실 가능 범위가 달라진다.
| 정책 | 동작 | 특징 |
|---|---|---|
always |
쓰기마다 fsync |
유실 범위는 작지만 디스크 지연의 영향을 크게 받는다. |
everysec |
보통 1초마다 fsync |
성능과 내구성 사이의 일반적인 절충안이다. |
no |
운영체제에 동기화를 맡긴다. | 성능 부담은 적지만 장애 시 유실 범위가 커질 수 있다. |
AOF는 쓰기 명령이 계속 쌓이므로 파일이 커진다. Redis는 현재 데이터를 만들 수 있는 더 짧은 명령 집합으로 AOF를 다시 작성하는 rewrite 기능을 제공한다.
6.3. RDB와 AOF 비교
| 기준 | RDB | AOF |
|---|---|---|
| 기록 방식 | 특정 시점의 전체 스냅샷 | 쓰기 명령 로그 |
| 장애 시 유실 범위 | 마지막 스냅샷 이후 | fsync 정책에 따라 달라짐 |
| 파일 크기 | 비교적 작음 | 상대적으로 커질 수 있음 |
| 재시작 | 큰 데이터에서 비교적 빠름 | 로그 재실행 때문에 느려질 수 있음 |
| 주요 용도 | 백업과 빠른 복구 | 더 작은 유실 범위가 필요한 복구 |
둘 중 하나만 선택해야 하는 것은 아니다. Redis는 RDB와 AOF를 함께 사용할 수 있다. 두 방식을 모두 사용한 상태에서 재시작하면 더 완전한 데이터셋을 가진 AOF를 복구에 사용한다.
어떤 방식을 선택할지는 Redis에 저장한 데이터가 사라졌을 때 어디에서 다시 만들 수 있는지에 따라 달라진다. 단순 캐시라면 영속화를 사용하지 않을 수도 있지만, 작업 메시지나 세션처럼 유실 비용이 있는 데이터라면 영속화와 복제 정책을 함께 검토해야 한다.
7. Sentinel과 Cluster의 차이
Sentinel과 Cluster는 모두 Redis 장애 대응과 관련되어 있어 비슷해 보이지만 해결하려는 문제가 다르다.
7.1. Sentinel
Sentinel은 Redis Cluster를 사용하지 않는 복제 구성에 고가용성을 제공한다.
여러 Sentinel 프로세스가 master와 replica의 상태를 감시한다. master 장애에 대해 필요한 수의 Sentinel이 동의하면 replica 하나를 새 master로 승격하고, 다른 replica와 클라이언트가 새 master를 바라보도록 전환한다.
Sentinel의 주요 역할은 다음과 같다.
- Redis 인스턴스 상태 감시
- 장애 알림
- 자동 failover
- 현재 master 주소를 클라이언트에 제공
Sentinel을 사용한다고 key가 여러 master에 자동으로 나뉘는 것은 아니다. 기본 데이터 쓰기는 하나의 master가 담당한다.
7.2. Cluster
Redis Cluster는 key space를 16,384개의 hash slot으로 나누고 여러 master node에 분산한다.
key는 CRC16 계산 결과를 이용해 하나의 slot에 배정되고, 각 master가 일부 slot을 담당한다. 따라서 데이터와 쓰기 부하를 여러 master로 나눌 수 있다.
Cluster는 분산 저장뿐 아니라 replica를 이용한 장애 조치도 제공한다. 다만 비동기 복제를 사용하므로 네트워크 분할과 장애 시점에 따라 이미 응답한 쓰기가 유실될 수 있는 구간이 존재한다.
또한 여러 key를 한 번에 다루는 연산은 관련 key가 같은 hash slot에 있을 때만 실행할 수 있는 경우가 있다. Cluster를 도입하면 key 이름과 multi-key 연산도 slot 배치를 고려해 설계해야 한다.
7.3. 비교
| 기준 | Sentinel | Cluster |
|---|---|---|
| 주된 목적 | 단일 master 복제 구성의 고가용성 | 데이터 분산과 수평 확장, 고가용성 |
| 쓰기 분산 | 하지 않음 | 여러 master로 분산 |
| 데이터 분할 | 없음 | 16,384개 hash slot |
| 클라이언트 역할 | 현재 master를 조회 | key가 속한 node로 요청 |
| 설계 시 주의 | failover와 replica 지연 | slot, multi-key 명령, 재분배 |
데이터와 처리량이 한 Redis master의 범위 안에 있고 자동 장애 조치가 필요하다면 Sentinel 구성을 검토할 수 있다. 데이터나 쓰기 부하를 여러 node로 나눠야 한다면 Cluster가 해결하려는 문제에 가깝다.
8. 정리
Redis는 메모리에 데이터를 두고, 대부분의 클라이언트 명령을 하나의 메인 실행 흐름에서 순차적으로 처리한다. 여러 네트워크 연결은 이벤트 루프와 I/O multiplexing으로 다루기 때문에 연결마다 작업 스레드를 할당하고 기다릴 필요가 없다.
이 구조는 락 경합과 요청별 스레드 전환 부담을 줄이지만, 느린 명령 하나가 다른 요청까지 지연시킬 수 있다는 조건을 가진다. 따라서 Redis를 사용할 때는 명령의 시간 복잡도와 key 크기를 함께 관리해야 한다.
RDB와 AOF는 메모리 데이터를 디스크에서 복구하기 위한 방식이다. Sentinel은 하나의 master를 중심으로 한 복제 구성의 장애 조치를 담당하고, Cluster는 key를 여러 master에 분산하여 수평 확장하는 구조이다.
결국 Redis를 이해할 때는 “싱글 스레드라서 빠른 저장소”라는 한 문장보다 다음 네 가지 질문을 나누어 보는 편이 정확하다.
- 명령은 어떤 실행 흐름에서 처리되는가?
- 네트워크 대기는 어떻게 피하는가?
- 장애 후 데이터는 어디까지 복구해야 하는가?
- 하나의 master로 충분한가, 데이터를 분산해야 하는가?
이 기준을 먼저 정하면 이벤트 루프, RDB·AOF, Sentinel·Cluster가 서로 다른 기능의 목록이 아니라 하나의 운영 구조로 연결된다.