<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hash on kastori</title><link>http://blog.kastori.dev/tags/hash/</link><description>Recent content in Hash on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Thu, 16 Jul 2026 00:00:00 +0900</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/hash/index.xml" rel="self" type="application/rss+xml"/><item><title>[Redis 실무 입문 #2] 자료구조 5종 — 언제 무엇을 쓰나</title><link>http://blog.kastori.dev/tech/2026-07-16-redis-02-data-structures/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-07-16-redis-02-data-structures/</guid><description>&lt;h2 id="redis를-문자열-저장소로만-쓰면-절반만-쓰는-거다"&gt;&lt;a href="#redis%eb%a5%bc-%eb%ac%b8%ec%9e%90%ec%97%b4-%ec%a0%80%ec%9e%a5%ec%86%8c%eb%a1%9c%eb%a7%8c-%ec%93%b0%eb%a9%b4-%ec%a0%88%eb%b0%98%eb%a7%8c-%ec%93%b0%eb%8a%94-%ea%b1%b0%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;Redis를 문자열 저장소로만 쓰면 절반만 쓰는 거다
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="http://blog.kastori.dev/tech/2026-07-14-redis-01-getting-started/" &gt;지난 편&lt;/a&gt;에서 &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;GET&lt;/code&gt;으로 문자열을 넣고 뺐다. 처음엔 나도 Redis를 딱 이 정도로만 썼다. &amp;ldquo;값 하나 빠르게 저장하는 통&amp;rdquo; 정도로. 그러다 Redis 자료구조를 제대로 보고 나서야, 그동안 애플리케이션 코드로 끙끙대며 짜던 것들이 명령 한 줄로 끝난다는 걸 알았다.&lt;/p&gt;
&lt;p&gt;Redis에는 &lt;strong&gt;5가지 기본 자료구조&lt;/strong&gt;가 있다. 핵심은 문법 암기가 아니라 &lt;strong&gt;&amp;ldquo;이 상황엔 뭘 쓰지?&amp;ldquo;를 판단하는 감&lt;/strong&gt;이다. 하나씩, 어떤 상황에서 꺼내 쓰는지 위주로 보자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="string--캐시와-카운터"&gt;&lt;a href="#string--%ec%ba%90%ec%8b%9c%ec%99%80-%ec%b9%b4%ec%9a%b4%ed%84%b0" class="header-anchor"&gt;&lt;/a&gt;String — 캐시와 카운터
&lt;/h2&gt;&lt;p&gt;가장 기본. JSON을 통째로 캐싱하거나 숫자를 셀 때 쓴다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# JSON 통째로 캐싱 (실무에서 제일 흔한 패턴)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET user:1 &lt;span class="s1"&gt;&amp;#39;{&amp;#34;id&amp;#34;:1,&amp;#34;name&amp;#34;:&amp;#34;kastori&amp;#34;,&amp;#34;role&amp;#34;:&amp;#34;admin&amp;#34;}&amp;#39;&lt;/span&gt; EX &lt;span class="m"&gt;3600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 조회수 카운터&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INCR post:100:views
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;언제 쓰나&lt;/strong&gt;: 조회 결과 캐싱, 조회수·좋아요 같은 단순 카운터. 고민되면 일단 String으로 시작해도 된다. 대부분의 캐시가 여기서 출발한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="hash--객체를-필드별로-세션-프로필"&gt;&lt;a href="#hash--%ea%b0%9d%ec%b2%b4%eb%a5%bc-%ed%95%84%eb%93%9c%eb%b3%84%eb%a1%9c-%ec%84%b8%ec%85%98-%ed%94%84%eb%a1%9c%ed%95%84" class="header-anchor"&gt;&lt;/a&gt;Hash — 객체를 필드별로 (세션, 프로필)
&lt;/h2&gt;&lt;p&gt;하나의 키 안에 여러 필드를 담는다. 여기서 자주 나오는 질문. &amp;ldquo;그냥 JSON을 String에 통째로 넣으면 안 되나?&amp;rdquo; 된다. 그런데 &lt;strong&gt;필드 하나만 바꾸려 할 때&lt;/strong&gt; 차이가 난다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HSET user:1 name &lt;span class="s2"&gt;&amp;#34;kastori&amp;#34;&lt;/span&gt; role &lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt; age &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET user:1 name &lt;span class="c1"&gt;# &amp;#34;kastori&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGETALL user:1 &lt;span class="c1"&gt;# 전체 필드&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HINCRBY user:1 age &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# age만 +1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;JSON을 String에 넣어뒀으면 &lt;code&gt;age&lt;/code&gt; 하나 올리려 해도 전체를 읽어서 파싱하고 다시 통째로 &lt;code&gt;SET&lt;/code&gt;해야 한다. Hash는 &lt;code&gt;HINCRBY&lt;/code&gt;로 그 필드만 콕 집는다. 나는 &amp;ldquo;이 객체의 필드를 따로따로 건드릴 일이 있나?&amp;ldquo;를 기준으로 String과 Hash를 가른다. 있으면 Hash, 없으면 그냥 String에 JSON.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;언제 쓰나&lt;/strong&gt;: 세션 데이터, 사용자 프로필처럼 필드 단위로 읽고 쓰는 객체.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="list--순서-있는-목록-최근-기록-간단한-큐"&gt;&lt;a href="#list--%ec%88%9c%ec%84%9c-%ec%9e%88%eb%8a%94-%eb%aa%a9%eb%a1%9d-%ec%b5%9c%ea%b7%bc-%ea%b8%b0%eb%a1%9d-%ea%b0%84%eb%8b%a8%ed%95%9c-%ed%81%90" class="header-anchor"&gt;&lt;/a&gt;List — 순서 있는 목록 (최근 기록, 간단한 큐)
&lt;/h2&gt;&lt;p&gt;양쪽 끝에서 넣고 빼는 게 빠르다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LPUSH recent:user:1 &lt;span class="s2"&gt;&amp;#34;상품A&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 왼쪽에 추가&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LPUSH recent:user:1 &lt;span class="s2"&gt;&amp;#34;상품B&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LRANGE recent:user:1 &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="c1"&gt;# 최근 5개 조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LTRIM recent:user:1 &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt; &lt;span class="c1"&gt;# 최근 10개만 남기고 자르기&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;ldquo;최근 본 상품&amp;rdquo; 같은 걸 만들 때 딱인데, &lt;strong&gt;&lt;code&gt;LTRIM&lt;/code&gt;을 빼먹으면 사고&lt;/strong&gt;다. 사용자가 상품을 볼 때마다 &lt;code&gt;LPUSH&lt;/code&gt;만 하고 잘라주지 않으면 이 리스트가 무한히 커진다. 최근 10개만 보여줄 거면 넣을 때마다 &lt;code&gt;LTRIM ... 0 9&lt;/code&gt;로 꼬리를 잘라줘야 한다. 나는 이걸 &lt;code&gt;LPUSH&lt;/code&gt;와 세트로 붙여 다닌다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 간단한 작업 큐&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RPUSH job:queue &lt;span class="s2"&gt;&amp;#34;email-1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 생산자&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LPOP job:queue &lt;span class="c1"&gt;# 소비자&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;언제 쓰나&lt;/strong&gt;: &amp;ldquo;최근 N개&amp;rdquo;, 가벼운 작업 큐.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;다만 진짜 튼튼한 메시지 큐가 필요하면 List로 버티지 말자. 재시도·확인응답 같은 게 필요해지는 순간 Redis Streams나 Kafka로 가는 게 맞다. List 큐는 &amp;ldquo;가볍고 단순할 때&amp;quot;까지다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="set--중복-없는-집합-좋아요-중복-방지"&gt;&lt;a href="#set--%ec%a4%91%eb%b3%b5-%ec%97%86%eb%8a%94-%ec%a7%91%ed%95%a9-%ec%a2%8b%ec%95%84%ec%9a%94-%ec%a4%91%eb%b3%b5-%eb%b0%a9%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;Set — 중복 없는 집합 (좋아요, 중복 방지)
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SADD post:100:likes &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 좋아요 누른 사람&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SADD post:100:likes &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 중복은 조용히 무시됨&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SCARD post:100:likes &lt;span class="c1"&gt;# 좋아요 수&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SISMEMBER post:100:likes &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 이 사람 눌렀나? 1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set의 매력은 중복을 알아서 걸러준다는 거다. &amp;ldquo;이미 좋아요 눌렀는지&amp;rdquo; 애플리케이션에서 조회해서 검사할 필요 없이 &lt;code&gt;SADD&lt;/code&gt; 하면 그만이고, 눌렀는지 확인은 &lt;code&gt;SISMEMBER&lt;/code&gt; 한 방이다. 좋아요를 DB로 구현하면서 UNIQUE 제약이랑 씨름해본 사람이라면 이게 얼마나 편한지 안다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 교집합 — &amp;#34;나랑 상대가 공통으로 팔로우하는 사람&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SINTER user:1:following user:2:following
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;언제 쓰나&lt;/strong&gt;: 좋아요·투표처럼 중복을 막아야 하는 집합, 태그, 팔로워 교집합.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="sorted-set--점수로-정렬-실시간-랭킹"&gt;&lt;a href="#sorted-set--%ec%a0%90%ec%88%98%eb%a1%9c-%ec%a0%95%eb%a0%ac-%ec%8b%a4%ec%8b%9c%ea%b0%84-%eb%9e%ad%ed%82%b9" class="header-anchor"&gt;&lt;/a&gt;Sorted Set — 점수로 정렬 (실시간 랭킹)
&lt;/h2&gt;&lt;p&gt;개인적으로 Redis에서 제일 &amp;ldquo;오, 이거네&amp;rdquo; 했던 자료구조다. 각 멤버에 &lt;strong&gt;점수&lt;/strong&gt;를 매기면 알아서 정렬된 상태로 유지된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD game:ranking &lt;span class="m"&gt;1500&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD game:ranking &lt;span class="m"&gt;2300&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user:2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD game:ranking &lt;span class="m"&gt;1800&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user:3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZREVRANGE game:ranking &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt; WITHSCORES &lt;span class="c1"&gt;# 상위 10명 (점수 높은 순)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZREVRANK game:ranking &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# user:1의 순위 (0부터)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZINCRBY game:ranking &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user:1&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# user:1 점수 +100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;실시간 랭킹을 DB로 만들어본 적 있다면, 매 요청마다 &lt;code&gt;ORDER BY score DESC LIMIT 10&lt;/code&gt;을 돌리고 데이터가 쌓일수록 느려지는 걸 겪었을 거다. Sorted Set은 &lt;strong&gt;넣는 순간 이미 정렬&lt;/strong&gt;돼 있어서 순위 조회가 늘 빠르다. &amp;ldquo;내 순위&amp;quot;를 구하는 &lt;code&gt;ZREVRANK&lt;/code&gt;도 공짜로 딸려온다. 이걸 DB로 하려면 서브쿼리로 골치 아프다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;언제 쓰나&lt;/strong&gt;: 실시간 랭킹, 인기글 순위, 실시간 검색어, 우선순위 큐.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="한-장-요약"&gt;&lt;a href="#%ed%95%9c-%ec%9e%a5-%ec%9a%94%ec%95%bd" class="header-anchor"&gt;&lt;/a&gt;한 장 요약
&lt;/h2&gt;&lt;p&gt;헷갈리면 이 표 하나만 기억하자.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;자료구조&lt;/th&gt;
 &lt;th&gt;대표 용도&lt;/th&gt;
 &lt;th&gt;핵심 명령&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;String&lt;/td&gt;
 &lt;td&gt;캐시, 카운터&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;SET&lt;/code&gt; &lt;code&gt;GET&lt;/code&gt; &lt;code&gt;INCR&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Hash&lt;/td&gt;
 &lt;td&gt;세션, 프로필 (필드별 수정)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;HSET&lt;/code&gt; &lt;code&gt;HGET&lt;/code&gt; &lt;code&gt;HINCRBY&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;List&lt;/td&gt;
 &lt;td&gt;최근 N개, 가벼운 큐&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;LPUSH&lt;/code&gt; &lt;code&gt;LRANGE&lt;/code&gt; &lt;code&gt;LTRIM&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Set&lt;/td&gt;
 &lt;td&gt;좋아요, 중복 방지, 교집합&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;SADD&lt;/code&gt; &lt;code&gt;SISMEMBER&lt;/code&gt; &lt;code&gt;SINTER&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Sorted Set&lt;/td&gt;
 &lt;td&gt;실시간 랭킹&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ZADD&lt;/code&gt; &lt;code&gt;ZREVRANGE&lt;/code&gt; &lt;code&gt;ZINCRBY&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;자료구조를 알았으니 이제 실제로 붙일 차례다. 다음 편은 Redis를 도입하는 가장 흔한 이유, &lt;strong&gt;캐시&lt;/strong&gt;다. 값을 저장하는 것까지는 쉬운데, &amp;ldquo;DB를 바꿨는데 캐시가 옛날 값을 준다&amp;quot;는 정합성 문제에서 다들 한 번씩 데인다. 그 얘기를 제대로 하겠다.&lt;/p&gt;</description></item></channel></rss>