Redis가 빠른 이유: 메모리, 단일 스레드, 자료구조
API 진단 편에서 캐시 저장소로 Redis를 언급했습니다. 그런데 Redis는 왜 빠를까요? “메모리라서"는 절반의 답입니다. 메모리 기반이면서 Redis보다 느린 시스템은 많습니다. 이 글은 Redis의 속도를 만드는 세 기둥(메모리, 단일 스레드, 자료구조)과, 그 설계가 요구하는 대가까지 정리합니다. 원리를 알면 “Redis가 느려졌다"는 상황의 진단도 함께 얻어집니다.
기둥 ① 메모리: 디스크가 경로에 없습니다 #
일반 데이터베이스는 내구성을 위해 쓰기를 디스크에 내려보내고, 읽기도 캐시에 없으면 디스크로 갑니다. SSD 편에서 봤듯 이 경로는 마이크로초에서 밀리초 단위입니다. Redis는 모든 데이터를 메모리에 두므로, 요청 처리 경로에 디스크가 아예 없습니다. 메모리 접근은 나노초에서 마이크로초 단위, 자릿수가 2〜3개 다릅니다.
디스크가 완전히 사라진 것은 아닙니다. 영속화(스냅숏, AOF)는 필요하지만, 응답을 기다리게 하지 않는 경로 밖의 작업으로 밀어냈습니다. 스냅숏이 fork와 copy-on-write로 서비스 중단 없이 도는 구조는 fork 편에서 다뤘습니다. “빠름의 절반은 무엇을 경로 밖으로 빼내느냐의 설계"라는 것이 첫 기둥의 교훈입니다.
기둥 ② 단일 스레드: 락이 없는 대신 줄이 하나 #
명령을 처리하는 Redis의 코어는 스레드 하나의 이벤트 루프입니다. 수만 클라이언트의 요청을 하나의 줄로 세워 순서대로 처리합니다. 그런데도 명령 하나하나가 마이크로초 만에 끝나기 때문에 초당 수십만 개를 소화합니다. 직관에 반하는 이 선택의 근거는 프로세스와 스레드 편에서 본 공유 메모리의 비용입니다.
- 락이 필요 없습니다. 동시에 데이터를 만지는 스레드가 없으므로 락 경합도, 데드락도, 락을 쥐고 기다리는 시간도 없습니다. 멀티스레드 시스템이 코어를 늘리고도 락에서 잃는 시간을, Redis는 애초에 치르지 않습니다.
- 모든 명령이 원자적입니다. INCR 같은 명령이 경쟁 조건 걱정 없이 그대로 카운터가 되는 이유, 분산 락 구현에 Redis가 즐겨 쓰이는 이유가 이 직렬성입니다.
- CPU는 병목이 아니었습니다. Redis의 작업은 대부분 메모리 읽고 쓰기라, 한 코어로도 네트워크가 먼저 포화됩니다. 실제로 최근 버전이 멀티스레드를 도입한 곳도 명령 실행이 아니라 네트워크 I/O(읽기·쓰기 버퍼 처리)입니다. 코어가 남으면 인스턴스를 여러 개 띄우는 것이 Redis식 멀티코어 활용입니다.
대가도 명확합니다. 줄이 하나이므로, 느린 명령 하나가 전체를 세웁니다. 100만 키를 훑는 KEYS *, 거대한 컬렉션의 전체 조회, 대형 키 삭제가 100ms를 쓰면 그 뒤의 모든 요청이 100ms를 같이 기다립니다. “Redis가 갑자기 느려졌다"의 단골 원인이고, 처방은 KEYS 대신 SCAN, 큰 삭제는 UNLINK(백그라운드 삭제), 슬로우 로그(SLOWLOG)로 범인 명령을 찾는 것입니다.
기둥 ③ 자료구조: 변환 비용이 없습니다 #
관계형 DB에 “최근 목록"을 저장하면 테이블·인덱스로 변환하고, 조회 때 정렬·파싱으로 다시 변환합니다. Redis는 리스트, 해시, 셋, 정렬 셋(sorted set) 같은 자료구조를 그 모양 그대로 메모리에 들고 있어, 용도에 맞는 연산이 이미 최적화된 형태로 준비돼 있습니다.
- 랭킹보드는 정렬 셋의 “점수로 정렬 유지 + 순위 조회"가 그 자체로 답입니다. SQL이라면 ORDER BY가 매번 할 일을 자료구조가 항상 유지하고 있는 셈입니다.
- 카운터는 INCR 한 명령, 최근 N개 목록은 리스트의 LPUSH + LTRIM, 중복 제거는 셋이 담당합니다.
같은 “빠른 저장소"라도 무엇이 빠른지는 자료구조 선택에 달려 있어서, Redis를 잘 쓴다는 것은 사실상 자료구조를 잘 고른다는 뜻입니다. 반대로 모든 것을 JSON 문자열로 넣고 매번 통째로 읽어 파싱한다면 세 번째 기둥을 버리고 쓰는 것입니다.
그래도 느려지는 경우: 진단 목록 #
원리를 뒤집으면 진단 목록이 나옵니다. Redis가 느리다면 다음을 순서대로 봅니다.
- 느린 명령:
SLOWLOG GET으로 확인합니다. KEYS, 대형 컬렉션 전체 조회, 대형 키 삭제가 단골입니다. - 메모리 초과: maxmemory에 도달하면 축출(eviction) 작업이 얹히고, 스왑으로 밀리면 메모리 시스템이 디스크 시스템이 됩니다(메모리 편의 스왑 이야기가 그대로 적용됩니다). 축출 정책과 메모리 추이를 봅니다.
- 영속화 비용: 스냅숏 fork 순간의 지연, AOF fsync 설정(
always는 디스크가 경로 안으로 들어옵니다)을 확인합니다. - 네트워크 왕복: 명령 하나는 마이크로초라도 왕복은 밀리초입니다. 루프 안에서 GET을 천 번 부르는 코드는 왕복의 곱셈 문제이고, 파이프라인이나 MGET으로 묶는 것이 처방입니다.
정리 #
- Redis의 속도는 메모리(경로에 디스크 없음), 단일 스레드(락 없음 + 명령의 원자성), 자료구조(변환 비용 없음)의 합작입니다.
- 단일 스레드의 대가는 “느린 명령 하나가 전체를 세운다"입니다. KEYS 대신 SCAN, 큰 삭제는 UNLINK, 진단은 SLOWLOG입니다.
- 자료구조 선택이 곧 성능입니다. 랭킹은 정렬 셋, 카운터는 INCR처럼 용도에 맞는 구조를 골라야 세 번째 기둥이 삽니다.
- 느려졌다면 느린 명령 → 메모리·축출 → 영속화 설정 → 왕복 횟수 순으로 봅니다.
- 애플리케이션 쪽에서는 왕복을 묶는 것(파이프라인, MGET)이 서버 튜닝보다 먼저입니다.