Redis란?
Redis(Remote Dictionary Server)는 메모리 기반의 데이터 저장소로, Key-Value 구조를 중심으로 다양한 자료구조를 제공한다.
일반적인 관계형 데이터베이스가 영속적인 데이터 관리와 복잡한 관계형 질의에 강점을 가진다면, Redis는 메모리에서 데이터를 빠르게 읽고 쓰며 원자적인 연산을 수행하는 데 강점이 있다.
Redis는 단순한 캐시뿐만 아니라 다음과 같은 용도로 활용된다.
- 로그인 세션 및 인증 정보 관리
- API 요청 횟수 제한(Rate Limiting)
- 실시간 게임 상태 및 매칭 대기열 관리
- 랭킹 시스템
- 사용자별 온라인 상태 관리
- 위치 기반 검색
- 임시 데이터와 만료 데이터 관리
Redis를 효과적으로 사용하려면 각 자료구조가 제공하는 연산과 시간 복잡도, 데이터 만료 정책을 이해해야 한다.
String
String은 Redis에서 가장 기본적인 자료구조다. 문자열뿐만 아니라 숫자, 직렬화된 JSON, 바이너리 데이터 등을 저장할 수 있다.
주요 명령어
| 명령어 | 설명 |
|---|---|
SET | 값을 저장한다. |
GET | 값을 조회한다. |
INCR | 숫자 값을 증가시킨다. |
DECR | 숫자 값을 감소시킨다. |
MSET | 여러 Key-Value를 저장한다. |
MGET | 여러 값을 조회한다. |
SETNX | Key가 존재하지 않을 때만 저장한다. |
사용 예시
SET user:1001:name "PlayerOne"
GET user:1001:name
SET game:1001:score 100
INCR game:1001:score
SET session:abc123 "user:1001" EX 1800
마지막 명령어는 세션 정보를 저장하고 지정한 시간이 지나면 만료되도록 설정한다.
실무 활용
String은 다음과 같은 상황에 적합하다.
- 사용자 세션과 인증 관련 임시 데이터
- API 요청 횟수 카운터
- 캐시 데이터
- 간단한 플래그 및 상태 값
- 직렬화된 객체 저장
INCR과 같은 명령은 원자적으로 실행되므로 여러 요청이 동시에 카운터를 증가시키는 상황에서 유용하다.
단, 여러 명령을 조합한 전체 작업이 자동으로 원자성을 보장하는 것은 아니다. GET으로 값을 조회한 다음 애플리케이션에서 계산하고 SET으로 저장하면 동시 요청으로 갱신이 유실될 수 있다. 이러한 경우 원자적 명령, 트랜잭션 또는 Lua 스크립트를 고려해야 한다.
Hash
Hash는 하나의 Key 아래 여러 필드(Field)와 값(Value)을 저장하는 자료구조다. 일반적인 객체나 레코드와 유사하게 데이터를 표현할 수 있다.
주요 명령어
| 명령어 | 설명 |
|---|---|
HSET | 필드와 값을 저장한다. |
HGET | 특정 필드의 값을 조회한다. |
HMGET | 여러 필드의 값을 조회한다. |
HGETALL | 모든 필드와 값을 조회한다. |
HDEL | 특정 필드를 삭제한다. |
HINCRBY | 숫자 필드를 지정한 값만큼 증가시킨다. |
HEXISTS | 필드의 존재 여부를 확인한다. |
사용 예시
HSET user:1001 name "PlayerOne" level 15 gold 5000
HGET user:1001 name
HGET user:1001 level
HINCRBY user:1001 gold 1000
HGETALL user:1001
데이터는 다음과 같은 구조로 표현된다.
user:1001
├─ name : PlayerOne
├─ level : 15
└─ gold : 6000
실무 활용
Hash는 사용자 프로필, 게임 캐릭터 상태, 설정값 등 여러 필드로 구성된 데이터를 저장하는 데 적합하다.
게임 서버에서 캐릭터의 레벨과 경험치, 골드를 각각 별도의 Key로 저장할 수도 있지만, 하나의 Hash에 관련 필드를 모아 관리할 수도 있다.
다만 Hash에 필드를 모았다고 해서 모든 필드 변경이 하나의 업무 트랜잭션으로 보장되는 것은 아니다. 또한 관계형 데이터베이스처럼 외래키 제약이나 복잡한 조건 검색을 제공하지 않는다.
일반적으로 TTL은 Hash 전체 Key에 적용된다. 개별 필드의 만료가 필요하다면 Redis 버전과 지원 명령을 확인해야 한다.
List
List는 문자열 값을 순서대로 저장하는 자료구조다. 양쪽 끝에서 데이터를 추가하거나 제거하는 작업에 적합하다.
주요 명령어
| 명령어 | 설명 |
|---|---|
LPUSH | 리스트의 왼쪽에 값을 추가한다. |
RPUSH | 리스트의 오른쪽에 값을 추가한다. |
LPOP | 왼쪽 값을 제거하고 반환한다. |
RPOP | 오른쪽 값을 제거하고 반환한다. |
LRANGE | 지정한 범위의 값을 조회한다. |
LLEN | 리스트 길이를 조회한다. |
BLPOP | 값이 들어올 때까지 대기하며 왼쪽 값을 가져온다. |
사용 예시
RPUSH queue:jobs "job:1"
RPUSH queue:jobs "job:2"
RPUSH queue:jobs "job:3"
LPOP queue:jobs
작업을 오른쪽에 추가하고 왼쪽에서 가져오면 FIFO(First In, First Out) 방식의 대기열을 구현할 수 있다.
실무 활용
List는 다음과 같은 용도에 적합하다.
- 간단한 작업 대기열
- 최근 활동 목록
- 메시지 버퍼
- FIFO 또는 LIFO 형태의 데이터 관리
그러나 List만으로 안정적인 작업 큐를 완성했다고 보기는 어렵다.
작업을 가져온 직후 소비자가 장애로 종료되면 해당 작업이 유실될 수 있다. 안정적인 작업 처리에는 Redis Streams의 소비자 그룹, 처리 확인(ACK), 재처리 정책 등을 고려할 수 있다.
또한 LRANGE로 많은 요소를 조회하면 데이터 크기에 비례하는 비용이 발생할 수 있으므로 조회 범위를 제한하는 것이 좋다.
Set
Set은 중복을 허용하지 않는 문자열 값의 집합이다. 저장 순서는 보장하지 않으며, 집합 연산을 효율적으로 수행할 수 있다.
주요 명령어
| 명령어 | 설명 |
|---|---|
SADD | 집합에 값을 추가한다. |
SREM | 특정 값을 제거한다. |
SISMEMBER | 값의 존재 여부를 확인한다. |
SMEMBERS | 모든 구성원을 조회한다. |
SCARD | 집합의 원소 개수를 조회한다. |
SINTER | 여러 집합의 교집합을 구한다. |
SUNION | 여러 집합의 합집합을 구한다. |
SDIFF | 집합의 차집합을 구한다. |
사용 예시
SADD game:1001:friends "user:1002" "user:1003"
SADD game:1001:friends "user:1002"
SISMEMBER game:1001:friends "user:1002"
SCARD game:1001:friends
동일한 값을 여러 번 추가해도 Set에는 중복으로 저장되지 않는다.
집합 연산 예시
SADD event:summer:participants "user:1" "user:2" "user:3"
SADD event:winter:participants "user:2" "user:3" "user:4"
SINTER event:summer:participants event:winter:participants
두 이벤트에 모두 참여한 사용자를 조회할 수 있다.
실무 활용
Set은 다음과 같은 상황에 적합하다.
- 온라인 사용자 목록
- 이벤트 참여자 관리
- 사용자별 보유 권한 집합
- 중복 없는 태그 관리
- 두 사용자 그룹의 공통 구성원 검색
특정 이벤트의 참여자를 Set으로 관리하면 동일한 사용자가 중복 등록되는 것을 방지할 수 있다.
단, Set에 값이 있다는 사실만으로 참여 처리와 보상 지급 전체가 원자적으로 완료되는 것은 아니다. 보상 지급까지 정확하게 처리해야 한다면 별도의 상태 관리와 트랜잭션 설계가 필요하다.
Sorted Set
Sorted Set은 각 원소에 점수(Score)를 부여하고, 점수를 기준으로 정렬된 상태를 유지하는 자료구조다.
Set과 마찬가지로 원소의 중복은 허용하지 않지만, 각 원소에 점수가 존재한다는 차이가 있다.
점수가 같으면 원소의 사전식 순서 등을 기준으로 정렬된다.
주요 명령어
| 명령어 | 설명 |
|---|---|
ZADD | 점수와 원소를 추가한다. |
ZSCORE | 원소의 점수를 조회한다. |
ZRANK | 오름차순 기준 순위를 조회한다. |
ZREVRANK | 내림차순 기준 순위를 조회한다. |
ZRANGE | 정렬된 범위를 조회한다. |
ZREVRANGE | 역순으로 정렬된 범위를 조회한다. |
ZINCRBY | 원소의 점수를 증가시킨다. |
ZREM | 원소를 제거한다. |
사용 예시
ZADD ranking:season1 1200 "player:1001"
ZADD ranking:season1 2500 "player:1002"
ZADD ranking:season1 1800 "player:1003"
ZREVRANGE ranking:season1 0 2 WITHSCORES
예상 결과는 다음과 같다.
player:1002 2500
player:1003 1800
player:1001 1200
점수가 높은 순서대로 상위 플레이어를 조회한다.
점수 갱신
ZINCRBY ranking:season1 300 "player:1001"
플레이어의 점수를 지정한 값만큼 증가시킨다.
실무 활용
- 게임 점수 랭킹
- 시즌별 리더보드
- 우선순위 작업 목록
- 최근 접속 시간 기준 사용자 정렬
- 점수 또는 시간 범위 기반 검색
Sorted Set은 점수 갱신과 정렬된 조회를 함께 지원하므로 게임 랭킹 시스템에 적합하다.
다만 랭킹 데이터가 매우 커지거나 점수 갱신이 집중되면 메모리 사용량과 쓰기 병목을 고려해야 한다. 시즌별로 Key를 분리하거나 오래된 데이터를 정리하는 정책이 필요할 수 있다.
자료구조별 비교
| 자료구조 | 핵심 특징 | 대표 활용 사례 |
|---|---|---|
| String | 단일 값 저장 및 원자적 카운터 연산 | 캐시, 세션, 요청 횟수 |
| Hash | 필드와 값의 묶음 | 사용자 프로필, 캐릭터 상태 |
| List | 순서가 있는 목록 | 간단한 큐, 최근 활동 |
| Set | 중복 없는 집합 | 온라인 사용자, 참여자 |
| Sorted Set | 점수 기반 정렬 | 랭킹, 우선순위 |
| GEO | 좌표 기반 위치 검색 | 주변 사용자, 근처 매장 |
핵심은 데이터의 형태와 필요한 연산을 기준으로 자료구조를 선택하는 것이다. 모든 데이터를 String으로 직렬화해 저장할 수도 있지만, 그렇게 하면 Redis가 제공하는 자료구조별 연산의 이점을 활용하기 어렵다.
TTL과 만료 정책
TTL(Time To Live)은 데이터가 만료되기까지 남은 시간을 의미한다.
Redis는 Key에 만료 시간을 설정할 수 있으며, 만료된 Key는 더 이상 정상적인 데이터로 조회되지 않는다.
주요 명령어
| 명령어 | 설명 |
|---|---|
EXPIRE | 초 단위로 만료 시간을 설정한다. |
PEXPIRE | 밀리초 단위로 만료 시간을 설정한다. |
EXPIREAT | Unix 타임스탬프 기준으로 만료 시점을 지정한다. |
TTL | 남은 만료 시간을 초 단위로 조회한다. |
PTTL | 남은 만료 시간을 밀리초 단위로 조회한다. |
PERSIST | 만료 설정을 제거한다. |
SET ... EX | 값을 저장하면서 만료 시간을 설정한다. |
사용 예시
SET cache:user:1001 "PlayerOne" EX 300
TTL cache:user:1001
해당 데이터는 지정한 시간이 지나면 만료된다.
Hash에도 TTL을 적용할 수 있다.
HSET session:abc123 userId 1001 status "active"
EXPIRE session:abc123 1800
이 경우 Hash 전체가 만료된다.
TTL의 동작 방식
Redis는 만료 시점에 모든 Key를 정확히 같은 순간에 일괄 삭제하는 방식으로만 동작하지 않는다.
일반적으로 다음과 같은 방식을 함께 사용한다.
- 지연 만료(Lazy Expiration): 만료된 Key에 접근할 때 만료 여부를 확인하고 제거한다.
- 능동 만료(Active Expiration): Redis가 주기적으로 만료 대상 Key를 검사하고 정리한다.
따라서 만료 시점과 실제 메모리 회수 시점이 정확히 일치하지 않을 수 있다. 다만 만료된 Key는 정상적인 조회 대상이 아니다.
TTL 설계 시 주의점
TTL 갱신 여부
일반적인 SET 명령으로 기존 값을 덮어쓰면 기존 TTL이 제거될 수 있다. 만료 시간을 유지해야 한다면 명령 옵션과 사용 중인 Redis 버전의 동작을 확인해야 한다.
캐시 만료 집중
많은 Key가 같은 시점에 만료되면 데이터베이스로 요청이 집중되는 캐시 스탬피드(Cache Stampede)가 발생할 수 있다.
이를 줄이려면 TTL에 무작위 편차를 추가하거나, 캐시 갱신 작업을 조정하거나, 기존 값을 잠시 유지하는 전략 등을 사용할 수 있다.
TTL은 영구 저장을 대체하지 않는다
Redis에 영속성 설정이 존재하더라도 TTL이 설정된 데이터는 만료 정책에 따라 사라진다. 계정 정보, 결제 내역, 게임 재화 원장처럼 손실되면 안 되는 데이터의 유일한 저장소로 TTL 기반 캐시를 사용해서는 안 된다.
Redis GEO
Redis GEO는 지리적 좌표를 저장하고, 좌표를 기반으로 위치 검색을 수행할 수 있도록 지원하는 기능이다.
Redis GEO는 내부적으로 Sorted Set을 활용해 위치 정보를 관리한다. 위도와 경도를 인코딩한 값을 이용해 위치를 효율적으로 검색한다.
주요 명령어
| 명령어 | 설명 |
|---|---|
GEOADD | 위치 정보를 추가하거나 갱신한다. |
GEOPOS | 저장된 위치의 좌표를 조회한다. |
GEODIST | 두 위치 사이의 거리를 계산한다. |
GEOSEARCH | 특정 좌표 또는 위치를 기준으로 검색한다. |
GEOHASH | 위치를 Geohash 문자열로 조회한다. |
위치 등록
GEOADD game:players:location \
126.9780 37.5665 "player:1001" \
127.0276 37.4979 "player:1002"
GEOADD의 인자 순서는 경도(Longitude), 위도(Latitude), 멤버(Member) 다.
위 예시에서는 서울의 두 위치에 플레이어를 등록한다.
거리 계산
GEODIST game:players:location \
"player:1001" "player:1002" km
두 플레이어 사이의 거리를 킬로미터 단위로 조회한다.
주변 사용자 검색
GEOSEARCH game:players:location \
FROMLONLAT 126.9780 37.5665 \
BYRADIUS 5 km \
ASC \
WITHDIST \
COUNT 20
위 명령은 기준 좌표로부터 반경 내에 있는 위치를 가까운 순서대로 최대 지정 개수만큼 조회한다. WITHDIST를 사용하면 거리도 함께 반환된다.
실무 활용
Redis GEO는 다음과 같은 위치 기반 기능에 활용할 수 있다.
- 위치 기반 게임의 주변 플레이어 검색
- 가까운 매장이나 시설 검색
- 주변 사용자 추천
- 일정 반경 내 이벤트 탐색
- 위치 기반 매칭 후보 조회
다만 GEO는 전문적인 지리정보시스템(GIS)을 대체하지 않는다. 복잡한 공간 교차, 다각형 포함 여부, 정밀한 지리 공간 분석이 필요하다면 PostGIS와 같은 공간 데이터베이스를 고려해야 한다.
또한 Redis GEO가 반환하는 결과는 위치 기반 후보군이다. 실제 게임 매칭이나 서비스 추천에서는 접속 상태, 권한, 지역 제한, 서버 부하 등의 추가 조건을 적용해야 한다.
게임 서버에서의 자료구조 활용
게임 서버를 설계할 때는 자료구조를 개별적으로 이해하는 것보다 실제 기능에 어떻게 조합하는지가 중요하다.
| 기능 | 자료구조 | 설계 목적 |
|---|---|---|
| 로그인 세션 | String | 세션 식별자와 사용자 정보 연결 |
| 캐릭터 상태 캐시 | Hash | 여러 상태 필드 관리 |
| 매칭 대기열 | List 또는 Sorted Set | 대기 순서 또는 점수 기반 관리 |
| 온라인 사용자 목록 | Set | 중복 없는 사용자 집합 |
| 시즌 랭킹 | Sorted Set | 점수 기반 정렬 및 상위권 조회 |
| 인증 코드 | String + TTL | 유효 시간 제한 |
| API 요청 제한 | String + TTL | 시간 구간별 요청 횟수 관리 |
| 주변 플레이어 검색 | GEO | 반경 기반 후보 조회 |
예를 들어 실시간 게임 서버의 매칭 시스템을 생각해 보자.
- Set으로 온라인 상태인 플레이어를 관리한다.
- Sorted Set으로 MMR(Matchmaking Rating) 기반 매칭 후보를 관리한다.
- GEO로 위치 기반 후보를 검색한다.
- String과 TTL로 짧은 수명의 매칭 잠금이나 임시 상태를 관리한다.
여러 자료구조를 조합하면 필요한 연산을 Redis 내부에서 효율적으로 처리할 수 있다.
그러나 자료구조 선택만으로 시스템의 정합성이 보장되지는 않는다. 여러 Key를 함께 변경하는 작업, 서버 장애 후 상태 복구, 중복 요청 방지, 데이터 영속성은 별도의 설계 대상이다.