[Huge Traffic Handling] Redis의 데이터 타입: 왜 String 하나로는 부족한가

Raha·2026년 4월 17일

Huge Traffic Handling

목록 보기
2/9

들어가며

지난 글에서는 HTTP Session의 한계와 Redis를 세션 저장소로 활용하는 방법을 살펴봤다. Redis가 세션 관리에 적합한 이유는 In-Memory 저장소 특성상 매 요청마다 조회되는 세션 데이터를 빠르게 처리할 수 있기 때문이었다.

이번 글에서는 Redis를 단순히 "빠른 저장소"로만 보는 시각에서 벗어나, 왜 Redis가 다양한 데이터 타입을 지원하는지에 대해 이야기한다. 글을 읽으면서 스스로 이런 질문을 던져보자.

  • String 하나로 모든 데이터를 저장하면 어떤 문제가 생길까?
  • Redis 데이터 타입이 각각 어떤 문제를 해결하기 위해 존재하는가?
  • 분산 환경에서 서버단 처리와 Redis 처리의 차이는 무엇인가?

1. 왜 다양한 데이터 타입이 필요한가

Redis가 String 하나만 지원한다고 가정해보자. 게임 랭킹 Top 10을 저장하려면 어떻게 해야 할까?

// Redis에 이런 식으로 저장해야 한다
SET game:ranking "player1:2000,player2:1800,player3:1500,..."

이렇게 하면 "1위부터 3위까지만 조회해줘"라는 요청이 들어왔을 때 어떤 일이 벌어지는가?

1. Redis에서 전체 String을 꺼낸다
2. 서버단에서 파싱한다 (split, 정규식 등)
3. 필요한 부분만 추출한다
4. 결과를 반환한다

Redis의 가장 큰 장점은 속도다. 그런데 이 방식은 무거운 처리를 서버단으로 넘기는 순간, 그 장점을 상쇄시킨다. 더 심각한 문제는 동시성이다. 서버 100대가 동시에 같은 데이터를 읽고 수정하려 한다면 Race Condition이 발생한다.

Redis가 다양한 데이터 타입을 지원하는 핵심 이유는 하나다.

데이터 구조에 맞는 타입을 쓰면, 파싱 없이 Redis 안에서 직접 원하는 데이터만 꺼낼 수 있다.


2. String: 범용 바이트 저장소

개념

Redis의 String은 이름과 달리 단순한 문자열 저장소가 아니다. 내부적으로 모든 데이터를 바이트(byte) 로 저장하기 때문에, Java의 int, long 같은 타입 구분이 없다. "123"이나 "hello"나 Redis 입장에서는 똑같이 바이트 덩어리다.

이 특성 덕분에 String은 범용 저장소에 가깝다.

카운터와 동시성

String의 강력한 활용 중 하나는 카운터다.

// 좋아요 수 증가
Long likes = redisTemplate.opsForValue().increment("item:123:likes");

// 페이지 뷰 감소
Long views = redisTemplate.opsForValue().decrement("page:views");

서버단에서 직접 구현한다면 이런 순서가 필요하다.

1. Redis에서 "123" 꺼냄
2. Long으로 파싱
3. +1
4. 다시 String으로 저장

이 과정에서 서버 100개가 동시에 같은 키에 접근하면 Race Condition이 발생한다. 100명이 좋아요를 눌렀는데 결과가 3이 되는 상황이다.

Redis의 increment는 이 문제를 해결한다. Redis는 Single Thread로 동작하기 때문에, increment 명령어는 원자적(Atomic) 으로 실행된다. get → +1 → set이 쪼개지지 않고 하나의 명령으로 처리되어, 100명이 동시에 눌러도 정확히 100이 나온다.

정리

항목내용
저장 구조Key → Value (단일 바이트)
주요 활용단순 캐싱, 카운터, 세션 상태
핵심 강점Atomic 연산으로 동시성 문제 없음

3. List: 순서가 있는 작업 대기열

개념

List는 삽입 순서를 유지하는 데이터 구조다. 양쪽 끝(left, right)에서 데이터를 넣고 뺄 수 있어서, 어느 쪽을 선택하느냐에 따라 Queue도 되고 Stack도 된다.

Queue (FIFO): 한쪽에서 넣고 → 반대쪽에서 꺼냄
Stack (LIFO): 같은 쪽에서 넣고 → 같은 쪽에서 꺼냄

예제

// Queue (FIFO): left push → right pop
redisTemplate.opsForList().leftPushAll("tasks", "Task1", "Task2", "Task3");
// Redis 상태: [Task3, Task2, Task1]

String task = redisTemplate.opsForList().rightPop("tasks"); // Task1

// 최근 메시지 전체 조회
List<String> messages = redisTemplate.opsForList().range("chat:room1", 0, -1);

정리

항목내용
저장 구조Key → [Value1, Value2, ...] (순서 유지)
주요 활용작업 대기열, 채팅 메시지 기록, 최근 활동 내역
핵심 강점양방향 입출력으로 Queue/Stack 모두 구현 가능

4. Set: 분산 환경의 중앙 집합

개념

Set은 중복을 허용하지 않는 데이터 구조다. 같은 값을 여러 번 추가해도 한 번만 저장된다.

"중복 제거는 Java의 HashSet으로도 되는데, 왜 Redis Set을 써야 하는가?"라는 의문이 생길 수 있다.

Java HashSet서버 메모리에 올라간다. 서버가 3대라면 HashSet도 3개가 존재한다. 분산 환경에서는 각 서버가 서로 다른 상태를 가지게 되어 데이터 불일치가 발생한다.

Redis Set은 중앙 저장소에 하나만 존재한다. 모든 서버가 동일한 집합을 바라보기 때문에, 분산 환경에서도 일관성이 보장된다.

집합 연산

Set의 또 다른 강점은 교집합, 합집합, 차집합 연산을 Redis 안에서 직접 처리한다는 점이다.

redisTemplate.opsForSet().add("event1:users", "User1", "User2", "User4");
redisTemplate.opsForSet().add("event2:users", "User2", "User3", "User4");

// 두 이벤트 모두 참여한 유저
Set<String> common = redisTemplate.opsForSet().intersect("event1:users", "event2:users");
// 결과: {"User2", "User4"}

// 이벤트1에만 참여한 유저
Set<String> onlyEvent1 = redisTemplate.opsForSet().difference("event1:users", "event2:users");
// 결과: {"User1"}

서버단에서 이 연산을 직접 구현한다면, 전체 데이터를 꺼내서 반복문으로 비교하는 복잡한 코드가 필요하다. Redis는 명령어 하나로 끝낸다.

정리

항목내용
저장 구조Key → {Value1, Value2, ...} (중복 없음, 순서 없음)
주요 활용고유 사용자 목록, 태그 관리, 이벤트 참여자
핵심 강점분산 환경에서 중앙 집합 역할 + Redis 내 집합 연산

5. Hash: 필드 단위로 읽고 쓰는 객체 저장소

개념

Hash는 하나의 Key 아래에 필드-값 쌍을 여러 개 저장하는 구조다. 관계형 DB의 레코드 한 행과 유사하다.

String으로도 사용자 프로필을 저장할 수 있다. user:123 키에 JSON 문자열로 저장하면 된다. 그런데 이메일만 변경하려면 어떻게 해야 하는가?

1. JSON String 전체를 꺼낸다
2. 파싱한다
3. 이메일 필드를 수정한다
4. 다시 JSON으로 직렬화한다
5. 저장한다

Hash라면 다르다.

예제

// Before: JSON String 방식
redisTemplate.opsForValue().set("user:123", "{\"name\":\"John\",\"email\":\"old@email.com\",\"age\":30}");
// 이메일 변경: 전체 파싱 후 재직렬화 필요

// After: Hash 방식
redisTemplate.opsForHash().put("user:123", "name", "John Doe");
redisTemplate.opsForHash().put("user:123", "email", "john@example.com");
redisTemplate.opsForHash().put("user:123", "age", "30");

// 이메일만 변경: 한 줄로 끝
redisTemplate.opsForHash().put("user:123", "email", "new@email.com");

// 특정 필드만 조회
String email = (String) redisTemplate.opsForHash().get("user:123", "email");

업데이트가 잦고 사용자가 많은 서비스에서 이 차이는 성능에 직접적인 영향을 준다.

정리

항목내용
저장 구조Key → {field1: value1, field2: value2, ...}
주요 활용사용자 프로필, 상품 속성, 객체 데이터
핵심 강점필드 단위 읽기/쓰기로 불필요한 파싱 제거

6. Sorted Set: 점수 기반 자동 정렬

개념

Sorted Set은 Set과 마찬가지로 중복을 허용하지 않지만, 각 값에 Score(점수) 를 함께 저장한다. 저장된 요소들은 이 Score를 기준으로 자동 정렬된다.

일반 Set으로 랭킹을 구현하려면 전체 데이터를 서버단으로 꺼내서 직접 정렬해야 한다. Sorted Set은 Redis 안에서 정렬이 이미 되어 있기 때문에, 범위 조회 명령어 하나로 Top N 결과를 바로 가져올 수 있다.

예제

// 점수와 함께 저장
redisTemplate.opsForZSet().add("game:ranking", "Player1", 1500.0);
redisTemplate.opsForZSet().add("game:ranking", "Player2", 2000.0);
redisTemplate.opsForZSet().add("game:ranking", "Player3", 1800.0);

// Top 2 조회 (점수 높은 순)
Set<String> top2 = redisTemplate.opsForZSet().reverseRange("game:ranking", 0, 1);
// 결과: [Player2, Player3]

// 점수 업데이트
redisTemplate.opsForZSet().incrementScore("game:ranking", "Player1", 100.0);
// Player1: 1500.0 → 1600.0

// 특정 점수 범위 조회
Set<String> inRange = redisTemplate.opsForZSet().rangeByScore("game:ranking", 1600.0, 1900.0);
// 결과: [Player1, Player3]

정리

항목내용
저장 구조Key → {Value: Score, ...} (Score 기준 자동 정렬)
주요 활용게임 리더보드, 실시간 랭킹, 시간 기반 데이터
핵심 강점Redis 내 자동 정렬 + 범위/순위 조회

7. 5가지 타입 비교

타입저장 구조핵심 강점대표 활용
StringKey → ValueAtomic 연산, 동시성 안전캐싱, 카운터, 세션 상태
ListKey → [순서 있는 목록]양방향 입출력 (Queue/Stack)작업 대기열, 메시지 기록
SetKey → {중복 없는 집합}분산 중앙 집합 + 집합 연산고유 사용자, 태그, 참여자
HashKey → {필드: 값}필드 단위 읽기/쓰기객체 데이터, 사용자 프로필
Sorted SetKey → {값: 점수}Score 기반 자동 정렬랭킹, 실시간 순위

마치며

Redis가 다양한 데이터 타입을 지원하는 이유는 단순히 "편의성" 때문이 아니다. 각 타입은 서버단에서 처리하면 속도 저하와 동시성 문제가 생기는 작업을 Redis 안에서 직접 처리하기 위해 설계된 도구다.

데이터 구조에 맞는 타입을 선택한다면, 서버 코드는 단순해지고 Redis의 속도 이점은 극대화된다.

profile
Backend Developer | Aspiring Full-Stack Enthusiast

0개의 댓글