Redis 자료구조 #String, Hash, List, Set, Sorted Set, TTL, GEO

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여러 값을 조회한다.
SETNXKey가 존재하지 않을 때만 저장한다.

사용 예시

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밀리초 단위로 만료 시간을 설정한다.
EXPIREATUnix 타임스탬프 기준으로 만료 시점을 지정한다.
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를 함께 변경하는 작업, 서버 장애 후 상태 복구, 중복 요청 방지, 데이터 영속성은 별도의 설계 대상이다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다