Redis는 메모리 기반의 Key-Value 데이터 저장소다. 데이터를 주로 RAM에 저장하기 때문에 일반적인 디스크 기반 데이터베이스보다 매우 빠르게 데이터를 읽고 쓸 수 있다.
처음에는 단순한 캐시처럼 많이 사용됐지만, 지금은 캐시뿐만 아니라 세션 저장, 인증 정보, 실시간 상태 관리, Pub/Sub, 분산 락, 작업 큐 등 여러 용도로 사용된다.
기본적인 구조는 단순하다.
Application -> Redis -> Key / Value
예를 들어:
"user:1001" -> {
"name": "Kim",
"level": 30
}
PostgreSQL이나 MySQL처럼 테이블을 중심으로 데이터를 모델링하는 데이터베이스와는 성격이 다르다.
Redis를 이해할 때는 빠른 메모리 저장소라는 특성과 다양한 자료구조를 이용해서 어떤 문제를 해결하는지를 보는 게 중요하다.
Redis의 기본 구조
Redis는 기본적으로 Key를 이용해서 Value를 찾는다.
Key -> Value
예를 들어:
"user:1001" -> "Kim"
"user:1002" -> "Lee"
값을 저장하고 가져오는 것도 간단하다.
SET user:1001 "Kim"
GET user:1001
결과:
"Kim"
Node.js에서는 Redis 클라이언트를 통해 사용할 수 있다.
await redis.set('user:1001', 'Kim');
const name = await redis.get('user:1001');
이 단순한 구조가 Redis의 기본이다.
Redis가 빠른 이유
Redis가 빠른 가장 큰 이유는 데이터를 메모리에 저장하기 때문이다.
일반적인 관계형 데이터베이스는 데이터를 영속적인 저장 장치에 저장하고 필요한 데이터를 읽어오는 과정이 있다.
Redis는 주로 RAM을 데이터 접근의 중심으로 사용한다.
Application -> MySQL / PostgreSQL -> Disk / Memory
Redis는:
Application -> Redis -> Memory
형태로 매우 빠르게 데이터를 처리할 수 있다.
다만 메모리는 디스크보다 비용이 비싸고 용량도 제한적이다.
그래서 Redis를 모든 데이터를 영구적으로 저장하는 메인 데이터베이스처럼 사용하는 것은 목적에 따라 신중하게 판단해야 한다.
Redis의 자료구조
Redis를 단순한 Key-Value 저장소라고만 이해하면 실제 기능을 많이 놓치게 된다.
Redis는 여러 자료구조를 제공한다.
String
Hash
List
Set
Sorted Set
Stream
각 자료구조가 해결하는 문제가 다르다.
String
가장 기본적인 자료구조다.
SET user:name "Kim"
GET user:name
문자열뿐 아니라 숫자 값도 저장할 수 있다.
SET page:view 100
INCR page:view
이런 특성 때문에 카운터 구현에도 자주 사용한다.
예를 들어 조회수를 관리한다면:
article:100:view_count -> 1520
INCR를 이용해서 원자적으로 증가시킬 수 있다.
Hash
Hash는 하나의 Key 아래 여러 필드를 저장할 수 있다.
user:1001 -> name / level / job
Redis 명령으로 보면:
HSET user:1001 name "Kim"
HSET user:1001 level 30
HSET user:1001 job "Warrior"
조회:
HGET user:1001 name
HGET user:1001 level
단순한 객체 형태의 데이터를 Redis에 저장할 때 사용할 수 있다.
List
List는 순서가 있는 문자열 목록이다.
queue -> A -> B -> C
데이터를 앞이나 뒤에 추가하고 제거할 수 있다.
이 특성을 이용하면 간단한 Queue를 만들 수 있다.
Producer -> Redis List -> Consumer
예를 들어 작업 요청을 Redis List에 넣고 Worker가 하나씩 꺼내 처리하는 구조를 만들 수 있다.
다만 실제 서비스에서 복잡한 작업 큐가 필요하다면 Redis 기반의 별도 Queue 시스템이나 메시지 브로커를 사용하는 것도 고려해야 한다.
Set
Set은 중복되지 않는 값들의 집합이다.
online_users -> 1001 / 1002 / 1003
같은 사용자를 여러 번 추가해도 하나만 유지된다.
SADD online_users 1001
SADD online_users 1001
결과적으로 1001은 한 번만 존재한다.
온라인 사용자 관리, 태그 집합, 특정 그룹의 사용자 관리 등에 사용할 수 있다.
Sorted Set
Sorted Set은 값마다 Score를 가지고 점수에 따라 정렬할 수 있는 자료구조다.
leaderboard -> player:1001 / 5000
leaderboard -> player:1002 / 3200
leaderboard -> player:1003 / 2800
게임의 랭킹 시스템에서 특히 유용하다.
ZADD leaderboard 5000 player:1001
ZADD leaderboard 3200 player:1002
점수를 기준으로 순위를 조회할 수 있다.
ZREVRANGE leaderboard 0 9 WITHSCORES
게임 서버에서 Redis가 자주 사용되는 이유 중 하나가 이런 자료구조를 제공하기 때문이다.
TTL
Redis에서 자주 사용하는 기능이 TTL(Time To Live)이다.
데이터에 만료 시간을 설정할 수 있다.
SET session:1001 "TOKEN"
EXPIRE session:1001 3600
1시간이 지나면 해당 Key가 만료된다.
한 번에 설정할 수도 있다.
SET session:1001 "TOKEN" EX 3600
TTL은 캐시나 세션을 구현할 때 특히 유용하다.
Cache -> user:1001 -> TTL 300초
시간이 지나면 Redis에서 자동으로 만료되므로 애플리케이션에서 직접 삭제할 필요가 줄어든다.
Cache
Redis의 가장 대표적인 사용 방법이다.
예를 들어 PostgreSQL에서 사용자 정보를 가져오는 API가 있다고 하자.
Redis를 사용하지 않는다면:
Client -> Node.js -> PostgreSQL -> Response
매번 DB를 조회하면 같은 데이터를 반복적으로 읽게 될 수 있다.
Redis를 캐시로 사용하면 다음과 같은 구조를 만들 수 있다.
Client -> Node.js -> Redis
Redis Cache Hit -> Response
Redis Cache Miss -> PostgreSQL -> Redis -> Response
예를 들어:
const key = `user:${userId}`;
let user = await redis.get(key);
if (!user) {
user = await getUserFromDatabase(userId);
await redis.set(
key,
JSON.stringify(user),
{ EX: 300 }
);
}
return JSON.parse(user);
Redis에 데이터가 있으면 DB까지 가지 않는다.
이런 패턴을 Cache-Aside라고 한다.
Cache Invalidation
캐시를 사용하는 순간 새로운 문제가 생긴다.
원본 DB의 데이터가 변경됐는데 Redis에 이전 데이터가 남아 있을 수 있다.
PostgreSQL -> name = Kim
Redis -> name = Lee
이런 상황을 Cache Invalidation 문제라고 볼 수 있다.
대표적인 방법은 데이터를 변경할 때 캐시를 삭제하는 것이다.
UPDATE Database -> DELETE Redis Cache
다음 요청이 들어오면 DB에서 최신 데이터를 읽어서 다시 Redis에 저장한다.
Request -> Redis Cache Miss -> Database -> Redis -> Response
캐시를 사용하는 시스템에서는 “어떻게 캐시할 것인가”만큼 “언제 캐시를 지울 것인가”가 중요하다.
Session Store
Redis는 세션 저장소로도 사용할 수 있다.
서버가 여러 대라면 메모리에 세션을 저장하는 방식에 문제가 생길 수 있다.
Client -> Load Balancer -> Server A
Client -> Load Balancer -> Server B
Client -> Load Balancer -> Server C
사용자가 Server A에서 로그인했는데 다음 요청이 Server B로 들어갈 수 있다.
서버 각각의 메모리에 세션을 저장했다면 문제가 발생할 수 있다.
Redis를 공통 세션 저장소로 사용하면:
Server A -> Redis
Server B -> Redis
Server C -> Redis
어느 서버가 요청을 처리하더라도 같은 세션 데이터에 접근할 수 있다.
Pub/Sub
Redis는 메시지를 여러 Subscriber에게 전달하는 Pub/Sub 기능도 제공한다.
Publisher -> Redis Channel -> Subscriber A
Publisher -> Redis Channel -> Subscriber B
Publisher -> Redis Channel -> Subscriber C
예를 들어 Node.js WebSocket 서버가 여러 대 있다고 하자.
WebSocket Server A
WebSocket Server B
WebSocket Server C
한 서버에서 이벤트가 발생했을 때 다른 서버에도 전달해야 할 수 있다.
Redis Pub/Sub을 이용하면 서버 사이에서 이벤트를 전달할 수 있다.
Server A -> Redis Pub/Sub -> Server B
Server A -> Redis Pub/Sub -> Server C
다만 Redis Pub/Sub은 메시지를 영구적으로 저장하는 메시지 큐가 아니다.
Subscriber가 연결되어 있지 않은 동안 발행된 메시지를 나중에 다시 가져오는 용도가 아니다.
메시지 영속성이나 재처리, 소비자 그룹 등이 필요한 경우 Redis Streams나 Kafka 같은 다른 구조를 고려할 수 있다.
Redis Streams
Redis Streams는 이벤트 데이터를 순서가 있는 로그 형태로 저장할 수 있는 자료구조다.
Producer -> Redis Stream -> Event 1
Producer -> Redis Stream -> Event 2
Producer -> Redis Stream -> Event 3
Producer -> Redis Stream -> Event 4
Pub/Sub과 달리 메시지가 Redis에 저장되기 때문에 소비자가 나중에 읽을 수 있다.
Consumer Group을 이용하면 여러 Worker가 작업을 나눠 처리하는 구조도 만들 수 있다.
Producer -> Redis Stream -> Consumer A
-> Consumer B
-> Consumer C
간단한 이벤트 처리부터 작업 큐 형태의 시스템까지 구성할 수 있다.
분산 락
여러 서버가 동시에 같은 작업을 수행하면 문제가 발생할 수 있다.
예를 들어 한정된 아이템을 구매하는 상황을 생각해보자.
Server A -> 재고 확인
Server B -> 재고 확인
재고 -> 1
두 서버가 동시에 재고를 확인하고 구매를 처리하면 재고가 음수가 될 수 있다.
이런 상황에서 여러 프로세스가 동시에 특정 작업을 수행하지 못하도록 분산 락을 사용할 수 있다.
Server A -> Redis Lock -> Critical Section -> Lock Release
Redis는 원자적인 명령과 TTL 등을 이용해 분산 락을 구현할 수 있다.
다만 분산 락은 구현이 생각보다 까다롭다. 단순히 SET 하나 사용하는 수준에서 끝나는 문제가 아니고, 락 만료, 소유권, 장애, 재시도 등을 고려해야 한다.
원자적 연산
Redis의 명령 중에는 원자적으로 실행되는 것들이 있다.
예를 들어 카운터를 증가시키는 INCR이 있다.
INCR counter
여러 요청이 동시에 들어와도 단순한 GET -> 계산 -> SET 방식보다 안전하게 카운터를 증가시킬 수 있다.
GET counter -> 계산 -> SET counter
보다는
INCR counter
처럼 Redis가 제공하는 원자적 명령을 사용하는 것이 적절하다.
이런 특성은 조회수, 재고, 요청 횟수 제한 등 다양한 곳에서 활용할 수 있다.
Rate Limiting
Redis는 API 요청 횟수를 제한하는 데도 사용할 수 있다.
예를 들어 특정 사용자가 1분에 100번까지만 API를 호출할 수 있도록 만들 수 있다.
user:1001:requests -> 73
요청이 들어올 때마다 카운터를 증가시키고 TTL을 설정한다.
Request -> Redis INCR -> 100 이하 -> 허용
Request -> Redis INCR -> 100 초과 -> 거부
API 서버를 여러 대 운영하더라도 Redis를 공통 저장소로 사용하면 전체 서버 기준으로 요청 횟수를 관리할 수 있다.
Persistence
Redis는 메모리 기반이지만 데이터를 디스크에 저장하는 기능도 제공한다.
대표적인 방식은 RDB와 AOF다.
RDB
특정 시점의 Redis 데이터를 스냅샷 형태로 저장한다.
Memory -> Snapshot -> RDB File
주기적인 백업에 적합하고 파일 크기가 비교적 작다는 장점이 있다.
AOF
Redis에서 발생한 쓰기 명령을 로그 형태로 기록한다.
SET user:1 Kim
SET user:2 Lee
INCR counter
이런 작업 기록을 파일에 저장한다.
일반적으로 RDB보다 데이터 손실을 줄이는 방향으로 설정할 수 있지만 저장되는 로그가 커질 수 있다.
Redis를 단순 캐시로 사용한다면 Persistence가 중요하지 않을 수 있다.
반대로 Redis 자체를 중요한 데이터를 저장하는 시스템으로 사용한다면 Persistence와 장애 복구 정책을 반드시 고려해야 한다.
Redis와 Database의 관계
Redis를 MySQL이나 PostgreSQL의 대체품으로 생각하면 설계가 꼬이기 쉽다.
일반적인 구조는 다음과 같다.
Node.js -> Redis -> Cache
Node.js -> Redis -> Session
Node.js -> Redis -> Counter
Node.js -> PostgreSQL -> Persistent Data
PostgreSQL이나 MySQL이 원본 데이터를 관리하고 Redis는 빠르게 접근해야 하는 데이터를 임시 또는 별도의 상태 저장소로 사용하는 식이다.
물론 Redis 자체를 Primary Data Store로 사용하는 시스템도 존재한다.
중요한 것은 Redis가 메모리 기반이라는 사실만 보고 무조건 캐시로만 사용하는 것이 아니라, 데이터의 영속성과 일관성 요구사항을 기준으로 역할을 정하는 것이다.
Redis와 WebSocket
앞에서 WebSocket 서버를 살펴봤다면 Redis가 왜 같이 등장하는지도 연결된다.
WebSocket 서버가 하나라면 메모리에서 연결된 클라이언트를 관리할 수 있다.
WebSocket Server -> Client A
WebSocket Server -> Client B
WebSocket Server -> Client C
하지만 서버가 여러 대가 되면 문제가 생긴다.
Server A -> Client A
Server B -> Client B
A가 보낸 이벤트를 B에게 전달하려면 서버 간 통신이 필요하다.
Redis Pub/Sub을 사용하면:
Server A -> Redis -> Server B -> Client B
이런 구조를 만들 수 있다.
따라서 Redis는 WebSocket 자체를 대체하는 기술이 아니라 여러 서버 사이에서 상태나 이벤트를 공유하기 위한 보조 시스템으로 사용될 수 있다.
Node.js 서버에서 Redis의 위치
Node.js API 서버를 기준으로 보면 Redis는 여러 위치에서 사용할 수 있다.
Node.js -> Redis -> Cache
Node.js -> Redis -> Session
Node.js -> Redis -> Rate Limit
Node.js -> Redis -> Pub/Sub
Node.js -> Redis -> Lock
Node.js -> Redis -> Queue
Node.js -> PostgreSQL -> Persistent Data
Node.js -> External API -> External Service
같은 Redis라도 목적에 따라 사용하는 자료구조와 접근 방식이 달라진다.
String -> Cache / Counter
Hash -> Object-like Data
List -> Queue
Set -> Unique Collection
Sorted Set -> Ranking
Pub/Sub -> Real-time Event
Stream -> Event Processing
TTL -> Expiration
이런 식으로 보면 Redis의 기능들이 단순히 많기만 한 것이 아니라 각각 특정 문제를 해결하기 위해 존재한다는 것을 알 수 있다.
Redis를 사용할 때 주의할 점
Redis는 빠르다는 이유만으로 모든 데이터를 넣는 시스템이 아니다.
가장 먼저 생각해야 할 것은 메모리다.
RAM -> Redis Data
RAM -> Redis Overhead
데이터가 계속 증가하면 결국 메모리 한계에 도달한다.
또한 캐시를 사용할 때는 TTL과 만료 정책을 설계해야 한다.
Cache -> 저장 시점
Cache -> 삭제 시점
Cache -> 유지 시간
Cache -> 원본 DB와의 일관성
Redis가 장애가 발생했을 때 서비스가 얼마나 영향을 받는지도 봐야 한다.
캐시라면 Redis가 잠시 죽어도 DB에서 다시 읽으면 될 수 있지만, 세션이나 중요한 상태 데이터를 Redis에 의존하고 있다면 이야기가 달라진다.
Redis를 도입하는 것보다 Redis가 없어졌을 때 서비스가 어떻게 동작해야 하는지를 먼저 생각하는 편이 낫다.
Redis 전체 구조
지금까지의 내용을 하나로 묶으면 다음과 같다.
Application -> Redis -> String -> Cache / Counter
Application -> Redis -> Hash -> Object-like Data
Application -> Redis -> List -> Queue
Application -> Redis -> Set -> Unique Collection
Application -> Redis -> Sorted Set -> Ranking
Application -> Redis -> Stream -> Event Processing
Application -> Redis -> Pub/Sub -> Real-time Event
Application -> Redis -> TTL -> Expiration
그리고 실제 서비스에서는 PostgreSQL이나 MySQL과 함께 사용한다.
Client -> Node.js -> Redis -> Cache
Client -> Node.js -> Redis -> Session
Client -> Node.js -> Redis -> Counter
Client -> Node.js -> Redis -> Pub/Sub
Client -> Node.js -> PostgreSQL -> Source of Truth
정리
Redis의 본질은 빠른 메모리 기반 데이터 저장소와 다양한 자료구조를 이용해 자주 발생하는 서버 문제를 해결하는 것에 있다.
대표적인 용도를 정리하면 다음과 같다.
Cache
Session
Counter
Rate Limiting
Pub/Sub
Distributed Lock
Queue
Ranking
Real-time State
그리고 자료구조는 용도에 맞춰 선택한다.
String
Hash
List
Set
Sorted Set
Stream
PostgreSQL이나 MySQL이 서비스의 영속적인 데이터를 관리한다면 Redis는 그 옆에서 빠른 접근이 필요한 데이터나 서버 간 공유 상태를 담당하는 경우가 많다.
Node.js -> Redis -> Cache
Node.js -> Redis -> Session
Node.js -> Redis -> Real-time State
Node.js -> Redis -> Pub/Sub
Node.js -> PostgreSQL -> Persistent Data
결국 Redis를 공부할 때 명령어를 하나씩 외우는 것보다 “이 문제를 DB로 처리할 것인가, Redis로 처리할 것인가, 둘을 어떻게 조합할 것인가”를 판단하는 것이 더 중요하다.
Redis는 데이터베이스를 대체하는 기술이라기보다, 애플리케이션과 데이터베이스 사이에서 속도, 상태 공유, 임시 데이터, 이벤트 처리 같은 문제를 해결하기 위한 별도의 데이터 계층으로 보는 편이 이해하기 좋다.