객체를 통째로 저장할지, 필드로 나눌지
앞 장에서는 JSON으로 직렬화한 객체를 Redis String 하나에 저장하는 방식을 보았다. 예를 들어 사용자 프로필을 다음처럼 저장할 수 있다.
SET user:1001:profile '{"name":"Kim","email":"[email protected]","plan":"free","login_count":0}'
GET user:1001:profile이 방식은 단순하다. 애플리케이션에서 쓰는 객체 모양을 거의 그대로 저장하고, 조회할 때도 한 번에 가져오면 된다. API 응답 전체를 캐시하거나, 상품 상세 페이지처럼 객체 전체를 항상 함께 읽는 경우에는 좋은 선택이다.
문제는 일부 필드만 바꾸고 싶을 때 생긴다. plan만 free에서 pro로 바꾸려 해도 Redis에서는 JSON 문자열 전체를 가져와 파싱하고, 값을 수정한 뒤 다시 전체 문자열로 저장해야 한다. login_count만 1 증가시키는 일도 마찬가지다. 객체가 커질수록 네트워크로 오가는 데이터가 늘고, 동시에 여러 요청이 같은 JSON을 갱신하면 마지막에 저장한 값이 앞선 변경을 덮어쓸 위험도 커진다.
Redis Hash는 이런 상황을 위해 자주 쓰인다. Hash는 하나의 Redis 키 아래에 여러 개의 field-value 쌍을 저장하는 자료구조다. 공식 문서에서도 Hash를 기본적인 객체나 여러 카운터 묶음을 표현하는 데 사용할 수 있다고 설명한다.1
user:1001:profile
name -> Kim
email -> [email protected]
plan -> free
login_count -> 0여기서 Redis 키는 user:1001:profile 하나다. 그 안에 name, email, plan, login_count 같은 필드가 들어간다. 관계형 데이터베이스의 행(row) 하나를 Redis 키 하나로 표현한다고 생각하면 이해하기 쉽다.
AI 생성 이미지
HSET으로 객체 필드 저장하기
Hash에 필드를 저장할 때는 HSET을 사용한다.
HSET user:1001:profile name "Kim" email "[email protected]" plan "free" login_count 0HSET은 Hash가 없으면 새로 만들고, 이미 있으면 지정한 필드를 추가하거나 덮어쓴다. 여러 필드를 한 번에 넣을 수도 있고, 하나만 수정할 수도 있다.
HSET user:1001:profile plan "pro"이 명령은 plan 필드만 바꾼다. JSON String 방식처럼 전체 객체를 다시 저장할 필요가 없다.
Hash에서도 값은 기본적으로 문자열로 저장된다. login_count에 0을 넣어도 Redis 내부에서는 문자열 값으로 보관된다. 다만 뒤에서 볼 HINCRBY 같은 명령은 해당 문자열을 정수로 해석할 수 있을 때 숫자 연산을 수행한다.
TTL은 조금 주의해서 봐야 한다. Redis에서 가장 기본적인 만료 시간은 Redis 키 전체에 걸린다.
EXPIRE user:1001:profile 3600
TTL user:1001:profile이렇게 설정하면 name만 1시간 뒤에 사라지는 것이 아니라 user:1001:profile Hash 전체가 만료된다. 그래서 오랫동안 Hash를 설명할 때는 "Hash는 필드별 TTL을 지원하지 않고, 키 전체에만 TTL을 건다"라고 말하는 경우가 많았다.
다만 최신 Redis를 사용한다면 한 가지를 더 알아두어야 한다. Redis 7.4부터는 HEXPIRE로 Hash 안의 특정 필드에 만료 시간을 설정할 수 있다.2
HEXPIRE user:1001:profile 3600 FIELDS 1 login_count이 명령은 user:1001:profile Hash 전체가 아니라 login_count 필드에만 3600초 TTL을 건다. 하지만 모든 운영 환경이 Redis 7.4 이상인 것은 아니고, 사용하는 클라이언트 라이브러리나 관리 도구가 필드별 TTL 명령을 충분히 지원하는지도 확인해야 한다. 또 HSET으로 해당 필드를 새 값으로 덮어쓰면 그 필드에 걸어 둔 만료 정보가 초기화될 수 있다는 점도 기억해야 한다.
따라서 이 장에서는 기본적으로 "TTL은 키 단위로 관리한다"는 감각을 먼저 잡자. 필드마다 생명주기가 분명히 다르고 Redis 7.4 이상의 필드 만료 기능을 안정적으로 쓰기 어렵다면, 하나의 Hash에 억지로 묶기보다 키를 분리하는 편이 더 단순하고 안전하다.
필요한 필드만 읽기: HGET, HMGET, HGETALL
필드 하나만 읽을 때는 HGET을 사용한다.
HGET user:1001:profile name응답은 다음과 같다.
"Kim"여러 필드가 필요하지만 전체 객체까지는 필요 없다면 HMGET을 사용한다.
HMGET user:1001:profile name plan응답은 요청한 필드 순서대로 돌아온다.
1) "Kim"
2) "pro"이 방식은 사용자 목록 화면처럼 일부 정보만 필요한 기능에서 유용하다. 예를 들어 목록에는 이름과 요금제만 필요하고, 이메일이나 소개글은 상세 화면에서만 필요할 수 있다. 이때 HGETALL로 모든 필드를 가져오는 대신 HMGET으로 필요한 필드만 읽으면 네트워크 전송량을 줄일 수 있다.
전체 필드를 가져올 때는 HGETALL을 사용한다.
HGETALL user:1001:profile응답은 보통 필드와 값이 번갈아 나오는 형태다.
1) "name"
2) "Kim"
3) "email"
4) "[email protected]"
5) "plan"
6) "pro"
7) "login_count"
8) "0"클라이언트 라이브러리는 이 결과를 Map, Dictionary, Object 같은 구조로 바꿔 주기도 한다. 이때 응답에 나온 순서 자체에 의존하기보다는 필드 이름을 기준으로 값을 매핑해야 한다. Hash는 "필드 이름으로 값을 찾는 구조"이지, 순서를 보장받기 위해 쓰는 자료구조가 아니다.
그리고 더 중요한 점은 HGETALL이 모든 필드와 값을 한 번에 가져온다는 사실이다. 필드가 몇 개 없는 사용자 프로필에는 문제가 되지 않는다. 그러나 필드가 수백 개, 수천 개로 늘어난 Hash에 습관적으로 HGETALL을 사용하면 Redis 서버와 애플리케이션 사이에 불필요하게 큰 응답이 오간다. 큰 Hash에서 전체 조회가 필요하지 않다면 HGET, HMGET을 우선 고려하고, 순회가 필요하면 HSCAN처럼 나누어 읽는 방식을 검토해야 한다.3
사용자 프로필 예제로 비교하기
같은 사용자 프로필을 두 방식으로 저장해 보자.
String JSON 방식은 다음과 같다.
SET user:1001:profile '{"name":"Kim","email":"[email protected]","plan":"free","login_count":0}' EX 3600Hash 방식은 다음과 같다.
HSET user:1001:profile name "Kim" email "[email protected]" plan "free" login_count 0
EXPIRE user:1001:profile 3600둘 중 어느 쪽이 항상 더 좋다고 말할 수는 없다. 판단 기준은 읽기와 쓰기의 패턴이다.
| 기준 | String JSON | Hash |
|---|---|---|
| 전체 객체 조회 | 단순하고 편리함 | HGETALL 필요 |
| 일부 필드 조회 | 전체를 가져와 파싱해야 함 | HGET, HMGET 가능 |
| 일부 필드 수정 | 전체를 다시 저장해야 함 | HSET으로 필드만 수정 |
| 숫자 필드 증가 | 파싱 후 재저장 필요 | HINCRBY 가능 |
| 중첩 객체 표현 | 자연스러움 | 필드 설계가 복잡해질 수 있음 |
| 필드별 TTL | 객체 내부 필드 단위로는 어려움 | Redis 7.4+에서는 HEXPIRE 가능. 그 전에는 키 TTL 중심 |
API 응답 캐시처럼 "항상 전체를 읽고, 거의 수정하지 않는 데이터"라면 String JSON이 더 단순하다. 반대로 사용자 프로필, 상품 요약 정보, 게시글 통계처럼 "일부 필드를 자주 읽거나 수정하는 데이터"라면 Hash가 더 잘 맞는다.
예를 들어 상품 정보를 생각해 보자.
HSET product:9001 title "Keyboard" price 79000 stock 25 view_count 0상품 상세 화면에서는 전체 필드가 필요할 수 있다.
HGETALL product:9001하지만 가격만 표시하는 영역에서는 price만 읽으면 된다.
HGET product:9001 price관리자가 가격을 수정할 때도 전체 상품 데이터를 다시 저장하지 않는다.
HSET product:9001 price 85000이처럼 Hash는 "객체처럼 보이지만 필드 단위로 다루고 싶은 데이터"에 적합하다.
HINCRBY로 객체 안의 숫자 필드 증가시키기
Hash 안의 숫자 필드는 HINCRBY로 증가시킬 수 있다.
HINCRBY user:1001:profile login_count 1이 명령은 user:1001:profile Hash 안의 login_count 필드를 1 증가시킨다. 필드가 없으면 0에서 시작해 증가한다. 감소도 가능하다.
HINCRBY product:9001 stock -1Redis의 단일 명령은 원자적으로 실행된다. 즉 여러 클라이언트가 동시에 HINCRBY user:1001:profile login_count 1을 보내도 증가 연산 자체가 중간에 섞여 깨지지 않는다. 조회해서 더하고 다시 저장하는 방식보다 안전하다.
다만 HINCRBY가 모든 비즈니스 조건을 해결해 주지는 않는다. 예를 들어 재고가 0보다 작아지면 안 되는 상황을 생각해 보자.
HINCRBY product:9001 stock -1이 명령은 현재 재고가 0인지 확인한 뒤 조건부로 차감하지 않는다. 단순히 정수 값을 감소시킬 뿐이다. "재고가 1 이상일 때만 차감한다"처럼 읽기와 조건 검사와 쓰기가 함께 필요한 로직은 트랜잭션(Transaction)이나 Lua 스크립트로 묶어야 한다. 이 주제는 뒤의 원자성 장에서 더 자세히 다룬다.
또 하나의 제한은 값이 정수로 해석 가능해야 한다는 점이다.
HSET user:1001:profile login_count "many"
HINCRBY user:1001:profile login_count 1이 경우 Redis는 숫자 연산을 할 수 없어 오류를 반환한다. 숫자로 증가시킬 필드는 애플리케이션에서 처음부터 숫자 문자열만 들어가도록 관리해야 한다.
메모리와 네트워크 관점에서 보기
Hash를 쓰는 이유는 명령어가 편해서만은 아니다. 설계 관점에서는 메모리 사용량과 네트워크 전송량에도 영향을 준다.
먼저 여러 필드를 각각 별도 String 키로 저장하는 방식을 생각해 보자.
SET user:1001:name "Kim"
SET user:1001:email "[email protected]"
SET user:1001:plan "free"
SET user:1001:login_count 0이 방식은 필드별로 완전히 다른 TTL을 주기 쉽다는 장점이 있다. 하지만 사용자 한 명을 표현하는 데 Redis 키가 네 개 필요하다. 사용자가 많아질수록 키 수도 함께 늘어난다. 운영 중 SCAN으로 탐색해야 할 키도 많아지고, 키 이름 자체가 차지하는 메모리도 누적된다.
Hash는 관련 필드를 하나의 상위 키 아래에 묶는다.
HSET user:1001:profile name "Kim" email "[email protected]" plan "free" login_count 0이렇게 하면 키 공간이 덜 흩어진다. 사용자 프로필이라는 객체 단위로 조회, 삭제, 만료를 관리하기 쉽다.
네트워크 측면에서는 "얼마나 자주 전체를 가져오는가"가 중요하다. 전체 객체가 작고 매번 모든 필드가 필요하다면 String JSON 하나를 GET하는 방식이 충분히 효율적일 수 있다. 반대로 객체가 크고 화면마다 필요한 필드가 다르다면 Hash가 유리하다. HMGET user:1001:profile name plan처럼 필요한 필드만 가져오면 응답 크기를 줄일 수 있기 때문이다.
하지만 Hash도 과하게 커지면 부담이 된다. 하나의 Hash에 너무 많은 필드를 몰아넣으면 HGETALL 한 번의 응답이 커지고, 특정 키 하나에 접근이 집중될 수 있다. "사용자 프로필"과 "사용자 활동 통계"처럼 성격과 갱신 빈도가 다른 데이터는 분리하는 편이 낫다.
HSET user:1001:profile name "Kim" email "[email protected]" plan "pro"
HSET user:1001:stats login_count 13 last_login_at "2026-05-30T10:00:00Z"프로필은 자주 바뀌지 않고, 통계는 자주 증가한다. 두 데이터를 나누면 읽기 패턴과 TTL 정책을 따로 가져갈 수 있다.
Hash를 선택할 때의 기준
Hash는 다음 조건에 잘 맞는다.
하나의 객체가 여러 필드로 이루어져 있다.
일부 필드만 자주 읽거나 수정한다.
객체 안에 조회수, 로그인 횟수, 재고처럼 증가·감소하는 숫자 필드가 있다.
관련 필드를 하나의 Redis 키로 묶어 관리하고 싶다.
반대로 다음 상황에서는 신중해야 한다.
매번 전체 객체만 읽고 쓰며, 필드 단위 접근이 거의 없다.
깊게 중첩된 JSON 구조를 그대로 보존해야 한다.
필드마다 만료 시간이 달라야 하는데 Redis 7.4 미만을 쓰거나
HEXPIRE를 운영에서 쓰기 어려운 환경이다.하나의 Hash가 너무 커져 전체 조회 비용이 커질 가능성이 높다.
Redis 자료구조를 고를 때는 저장 모양보다 사용 패턴을 먼저 봐야 한다. 객체처럼 생겼다고 무조건 Hash를 쓰는 것이 아니다. 전체를 한 번에 다루면 String JSON이 단순하고, 필드 단위로 다루면 Hash가 자연스럽다.
이 장의 핵심은 하나다. Hash는 Redis에서 객체를 "쪼개서 다루기" 위한 기본 도구다. HSET으로 필요한 필드만 바꾸고, HGET과 HMGET으로 필요한 만큼만 읽고, HINCRBY로 숫자 필드를 안전하게 증가시킬 수 있다. 여기에 Redis 버전에 따라 필드별 TTL 같은 기능까지 활용할 수 있음을 기억해 두면 좋다. 이 감각을 익히면 다음 장에서 List, Set, Sorted Set을 배울 때도 Redis 자료구조를 기능 설계의 언어로 바라볼 수 있다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.