에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/캐시

[Redis] 자료구조 활용법: String, Hash, List, Set, Sorted Set 언제 쓰나

반응형
[Redis] 자료구조 활용법: String, Hash, List, Set, Sorted Set 언제 쓰나

[Redis] 자료구조 활용법: String, Hash, List, Set, Sorted Set 언제 쓰나

Redis를 단순 key-value 캐시로만 쓰면 절반만 쓰는 거예요. Redis의 진짜 강점은 여러 자료구조예요. 자료구조를 잘 고르면 애플리케이션에서 직접 짤 로직을 Redis에 맡겨서 코드가 확 단순해져요. 랭킹을 직접 정렬하는 대신 Sorted Set에 넣는 식이죠.

다섯 가지 핵심 자료구조를 각각 언제 꺼내 쓰는지, 실무 패턴과 함께 하나씩 짚어요. Redis 기본 설정과 RedisTemplateSpring Data Redis 설정 글을 먼저 보면 좋아요.

Redis 자료구조 선택 맵. 카운터는 String, 부분 갱신은 Hash, 순서 있는 목록과 큐는 List, 중복 없는 모음은 Set, 랭킹과 점수 정렬은 Sorted Set으로 직접 짤 로직을 자료구조에 위임한다

01. String, 값과 카운터

가장 기본이에요. 단순 값, 캐시, 그리고 카운터로 써요. 카운터가 중요한데, INCR이 원자적이라 동시성 걱정 없이 숫자를 올릴 수 있어요.

// 조회수 카운터 — 원자적 증가
redisTemplate.opsForValue().increment("views:post:1");

// 값 + TTL
redisTemplate.opsForValue().set("token:abc", userId, Duration.ofMinutes(30));

여러 서버가 동시에 increment를 불러도 정확히 더해져요. 애플리케이션에서 락을 걸고 더하는 로직을 짤 필요가 없어요. 조회수·재고 카운트 같은 데 잘 맞아요.

02. Hash, 객체의 필드들

Hash는 하나의 키 아래 여러 필드를 둬요. 객체를 통째로 JSON으로 넣는 대신, 필드 단위로 저장하고 일부만 갱신할 수 있어요.

JSON 통째 저장과 Hash 비교. JSON은 필드 하나를 바꾸려고 전체를 읽고 수정하고 다시 써야 해서 동시 수정 시 덮어쓰기 위험이 있지만, Hash는 HINCRBY로 그 필드만 원자적으로 갱신한다
redisTemplate.opsForHash().put("user:1", "name", "jess");
redisTemplate.opsForHash().put("user:1", "age", "30");
redisTemplate.opsForHash().increment("user:1", "loginCount", 1);  // 필드 단위 증가

한 객체에서 특정 필드만 자주 바뀌면 Hash가 유리해요. JSON으로 통째 저장하면 한 필드를 바꾸려고 전체를 읽고-수정하고-쓰기 해야 하는데, Hash는 그 필드만 건드려요. 읽기-수정-쓰기 왕복이 없으니 동시 수정 때 서로 덮어쓰는 사고도 사라져요. 단 필드가 아주 많으면 메모리를 더 쓰니 균형은 봐야 해요.

03. List, 큐와 스택

List는 순서가 있는 모음이에요. 양 끝에서 넣고 빼서 큐나 스택으로 써요.

// 큐 — 왼쪽으로 넣고 오른쪽에서 빼기 (FIFO)
redisTemplate.opsForList().leftPush("queue:job", job);
Object job = redisTemplate.opsForList().rightPop("queue:job");

최근 목록에도 자주 써요. 최근 본 상품 10개처럼, leftPush로 넣고 trim으로 앞 N개만 남기면 "최근 N개"가 유지돼요.

// 최근 본 상품 10개만 유지
redisTemplate.opsForList().leftPush("recent:user:1", productId);
redisTemplate.opsForList().trim("recent:user:1", 0, 9);

작업 큐로 쓸 때는 블로킹 pop(BLPOP/BRPOP)이 유용해요. 큐가 비어 있으면 새 항목이 들어올 때까지 기다리니, 바쁜 대기 없이 작업을 꺼낼 수 있어요. 다만 본격적인 메시지 큐(재처리·소비자 그룹)는 Streams가 더 적합해요. List 큐는 가벼운 용도까지예요.

04. Set, 중복 없는 모음

Set은 중복을 허용하지 않는 모음이에요. "이미 있나" 확인이 빠르고, 집합 연산(교집합·합집합)을 줘요.

// 게시글 좋아요 누른 사용자 (중복 방지)
redisTemplate.opsForSet().add("post:1:likes", "user:1");
Long count = redisTemplate.opsForSet().size("post:1:likes");

// 공통 관심사 = 교집합
redisTemplate.opsForSet().intersect("user:1:tags", "user:2:tags");

좋아요·태그·방문자 집합처럼 "중복 없이 모으고 개수를 세거나 겹치는 걸 찾는" 데 좋아요. 두 집합의 교집합으로 공통 관심사를 뽑는 것도 한 줄이에요.

05. Sorted Set, 랭킹

Sorted Set은 각 원소에 점수(score)를 매겨 자동 정렬해요. 랭킹·리더보드에 딱이에요.

Redis Sorted Set 랭킹 구조. 원소가 항상 점수순으로 유지되어 상위 10명 조회, 내 순위 확인, 점수 범위 조회를 정렬 쿼리 없이 바로 뽑는다
// 점수 추가/갱신
redisTemplate.opsForZSet().add("ranking", "user:1", 1500);
redisTemplate.opsForZSet().incrementScore("ranking", "user:1", 100);

// 상위 10명 (높은 점수순)
redisTemplate.opsForZSet().reverseRange("ranking", 0, 9);

// 내 순위
redisTemplate.opsForZSet().reverseRank("ranking", "user:1");

랭킹을 DB로 구현하면 점수가 바뀔 때마다 정렬 쿼리를 돌려야 해서 무거워요. Sorted Set은 넣을 때 자동 정렬되니, 상위 N명이나 내 순위를 바로 뽑아요. 실시간 랭킹은 Sorted Set이 거의 정답이에요.

점수가 꼭 게임 점수일 필요는 없어요. 타임스탬프를 점수로 두면 "시간순 정렬"이 되고, 가격을 점수로 두면 "가격대별 조회"(rangeByScore)가 돼요. 예약 시스템에서 만료 시각을 점수로 넣어두고 지난 것만 뽑는 식으로도 써요. 점수를 무엇으로 두느냐에 따라 정렬·범위 조회를 다양하게 활용할 수 있어요.

06. 보너스, Bitmap과 HyperLogLog

다섯 가지 외에 알아두면 좋은 둘이 있어요. 둘 다 메모리를 극단적으로 아끼는 특수 자료구조예요.

Bitmap은 비트 하나로 true/false를 저장해요. "오늘 출석한 사용자"처럼 사용자 ID를 비트 위치로 두면, 수백만 명의 출석 여부를 아주 적은 메모리로 다뤄요. 출석체크, 일별 활성 사용자(DAU) 같은 데 써요.

// 사용자 1이 오늘 방문 → 비트 ON
redisTemplate.opsForValue().setBit("dau:2026-07-16", 1, true);
// 오늘 방문자 수 = 켜진 비트 수
redisTemplate.execute((RedisCallback<Long>) c -> c.bitCount("dau:2026-07-16".getBytes()));

HyperLogLog은 "고유 개수"를 추정해요. 정확한 값 대신 약간의 오차를 허용하는 대신, 메모리를 거의 안 써요. 수억 개의 고유 방문자 수를 12KB 정도로 세요. 정확한 집계가 아니라 "대략 몇 명"이면 충분한 UV 카운팅에 맞아요.

둘 다 "정확함을 약간 양보하고 메모리를 극단적으로 아끼는" 트레이드오프예요. 대규모 카운팅에서 Set으로 다 담기 부담스러우면 이쪽을 떠올려요.

07. 무엇을 고를까

  • 단순 값·카운터 → String
  • 객체의 필드를 부분 갱신 → Hash
  • 순서 있는 목록·큐 → List
  • 중복 없는 모음·집합 연산 → Set
  • 점수 기반 정렬·랭킹 → Sorted Set

핵심은 "애플리케이션에서 짤 로직을 Redis 자료구조로 옮길 수 있나"를 보는 거예요. 정렬·중복제거·카운팅을 직접 구현하기 전에, 그걸 기본으로 해주는 자료구조가 있는지 먼저 떠올리면 코드가 단순해져요.

08. 자주 만나는 문제

큰 컬렉션에서 명령이 느려요

일부 명령은 원소 수에 비례해 느려져요(O(N)). 예를 들어 거대한 Set 전체를 가져오는 명령은 그만큼 오래 걸려요. 큰 컬렉션은 범위로 나눠 조회하거나 자료구조 설계를 다시 봐요.

KEYS 명령으로 전체를 훑었더니 멈췄어요

Redis는 싱글 스레드로 명령을 처리해요. 그래서 KEYS *처럼 전체 키를 한 번에 훑는 명령이 도는 동안 다른 모든 명령이 줄을 서요. 키가 수백만 개면 그 시간만큼 서비스 전체가 멈추는 거예요. 운영에서는 절대 쓰지 말고, 키를 조금씩 나눠 순회하는 SCAN을 써요. 같은 이유로 큰 컬렉션의 O(N) 명령도 운영 장애의 단골이에요.

키가 너무 많아 메모리가 늘어요

TTL 없는 키가 쌓이는 거예요. 캐시성 데이터는 TTL을 두고, 자료구조 키도 수명을 관리해요. 메모리 정책은 TTL·메모리 정책 편에서 따로 다뤄요.

정리

Redis는 한마디로 자료구조 서버예요. 카운터는 String, 부분 갱신은 Hash, 큐는 List, 중복 없는 집합은 Set, 랭킹은 Sorted Set으로 두면 애플리케이션에서 직접 짤 로직을 Redis가 대신 맡아줘요. 그래서 정렬·중복제거·카운팅을 손으로 구현하기 전에, 그걸 기본으로 해주는 자료구조가 있는지 먼저 떠올리는 게 Redis를 잘 쓰는 첫걸음이에요.

여기까지가 Spring Data Redis 설정캐싱에 이어지는 자료구조 편이에요. 이 자료구조들을 여러 명령으로 묶어 원자적으로 다루는 이야기는 트랜잭션과 Lua로 이어져요.

출처: Redis — Data Types · Spring Data Redis — RedisTemplate Operations

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."