String은 Redis의 기본기다
Redis의 String은 가장 단순한 자료구조다. 키 하나에 값 하나를 저장한다. 그래서 처음에는 관계형 데이터베이스의 단순한 컬럼 하나처럼 보인다. 하지만 Redis에서 String은 텍스트만 뜻하지 않는다. 문자열, 숫자처럼 해석 가능한 값, JSON으로 직렬화한 객체, 바이너리 데이터까지 모두 바이트 시퀀스로 저장할 수 있다. Redis 공식 문서도 String을 캐시, 카운터, 비트 연산 등에 활용되는 기본 자료구조로 설명한다.1
가장 기본 형태는 이미 익숙하다.
SET user:1001:name "kim"
GET user:1001:name결과는 단순하다.
"kim"이 장의 핵심은 SET과 GET만 반복하는 것이 아니다. 실무에서는 값을 "무조건 저장"하는 경우보다 "없을 때만 저장", "있을 때만 갱신", "저장과 동시에 만료 시간 설정", "숫자처럼 증가"시키는 일이 훨씬 많다. Redis String은 이런 요구를 짧은 명령으로 처리할 수 있다.
AI 생성 이미지
SET 옵션으로 저장 조건과 만료를 함께 표현하기
SET은 단순 저장 명령이지만 옵션을 붙이면 쓰임새가 크게 넓어진다.
SET key value [NX|XX] [EX seconds|PX milliseconds]대표 옵션은 네 가지다.
NX: 키가 없을 때만 저장한다.XX: 키가 이미 있을 때만 저장한다.EX: 초 단위 만료 시간을 함께 설정한다.PX: 밀리초 단위 만료 시간을 함께 설정한다.
예를 들어 상품 상세 정보를 5분 동안 캐시하려면 다음처럼 쓴다.
SET cache:product:1001 '{"id":1001,"name":"keyboard"}' EX 300이 명령은 값을 저장하고 TTL을 300초로 설정한다. SET 후에 EXPIRE를 따로 실행하는 것보다 안전하다. 두 명령으로 나누면 SET은 성공했지만 애플리케이션 장애나 네트워크 문제로 EXPIRE가 실행되지 않을 수 있다. 그러면 원래 임시 캐시여야 할 데이터가 만료되지 않는 키로 남는다. SET ... EX는 값 저장과 만료 설정을 하나의 명령으로 처리한다.2
NX는 "처음 한 번만" 저장하고 싶을 때 유용하다.
SET signup:email-token:user1001 "836291" NX EX 180이 명령은 해당 키가 없을 때만 인증 코드를 저장하고, 180초 뒤 자동 삭제되게 만든다. 이미 인증 코드가 발급된 상태라면 새 값으로 덮어쓰지 않는다. 중복 요청을 막거나, 짧은 시간 동안 하나의 작업만 허용할 때 자주 쓴다.
간단한 잠금에도 NX와 PX 조합이 등장한다.
SET lock:order:1001 "worker-7" NX PX 30000의미는 "lock:order:1001 키가 없으면 30초짜리 잠금을 만든다"이다. 다만 이것만으로 완전한 분산 락을 구현했다고 생각하면 위험하다. 잠금을 해제할 때는 내가 만든 잠금인지 확인하고 삭제해야 한다. GET으로 확인한 뒤 DEL하는 두 단계 방식은 경쟁 상황에서 안전하지 않을 수 있다. 이런 문제는 Lua 스크립트와 원자성을 다루는 장에서 다시 살펴본다.
반대로 XX는 이미 존재하는 키만 갱신할 때 쓴다.
SET cache:product:1001 '{"id":1001,"name":"new keyboard"}' XX EX 300캐시가 이미 있을 때만 새 값으로 바꾸고 싶다면 이 방식이 맞다. 키가 없다면 새로 만들지 않는다. 이 옵션은 "없는 캐시는 애플리케이션의 정식 조회 흐름에서 만들고, 백그라운드 갱신 작업은 이미 있는 캐시만 갱신한다" 같은 정책에 어울린다.
주의할 점도 있다. SET은 기존 값을 덮어쓴다. 기존 키가 다른 자료구조를 담고 있어도 String 값으로 바꿔 버릴 수 있다. 또한 일반 SET key value는 기존 TTL을 제거한다. Redis에는 기존 TTL을 유지하는 KEEPTTL 옵션도 있지만, 기본 동작은 TTL 제거라고 기억해 두는 편이 안전하다. 캐시 키를 갱신할 때 실수로 EX를 빼면 만료 시간이 사라질 수 있다.
SET cache:product:1001 "old" EX 300
TTL cache:product:1001
SET cache:product:1001 "new"
TTL cache:product:1001마지막 TTL 결과가 -1이라면 키는 존재하지만 만료 시간이 없다는 뜻이다. 임시 데이터에는 값을 갱신할 때도 TTL을 함께 설정하는 습관이 필요하다.
캐시는 String으로 가장 먼저 만든다
가장 흔한 String 활용은 캐시다. 흐름은 단순하다.
Redis에서 먼저 조회한다.
값이 있으면 바로 반환한다.
값이 없으면 데이터베이스나 외부 API에서 조회한다.
조회 결과를 Redis에 저장하고 TTL을 설정한다.
명령으로 표현하면 다음과 같다.
GET cache:article:42값이 없다면 애플리케이션이 원본 저장소에서 게시글을 조회한 뒤 저장한다.
SET cache:article:42 '{"id":42,"title":"Redis String"}' EX 600이 패턴은 단순하지만 효과가 크다. 자주 조회되는 게시글, 상품 상세, 설정값, 외부 API 응답처럼 "읽기는 많고 약간 늦게 갱신되어도 되는 데이터"에 잘 맞는다.
다만 캐시는 원본 데이터가 아니다. TTL을 너무 길게 잡으면 오래된 데이터를 보여줄 수 있고, 너무 짧게 잡으면 캐시 효과가 줄어든다. 키 설계와 데이터 생명주기에서 다룬 원칙처럼 cache:article:42, cache:product:1001처럼 목적이 드러나는 키를 쓰고, 캐시 정책에 맞는 TTL을 함께 정해야 한다.
문자열을 숫자처럼 다루는 카운터
Redis에는 별도의 정수 자료형이 없다. 대신 String에 저장된 값이 정수로 해석 가능하면 숫자 연산을 할 수 있다. INCR은 값을 1 증가시킨다. 키가 없으면 Redis는 0으로 간주한 뒤 1을 더한다. 값은 64비트 부호 있는 정수 범위에서 처리된다.3
조회수 카운터를 만들어 보자.
DEL article:42:views
INCR article:42:views
INCR article:42:views
GET article:42:views결과는 다음과 같다.
"2"INCRBY는 원하는 값만큼 증가시킨다.
INCRBY article:42:views 10DECR, DECRBY는 감소시킨다.
SET stock:item:1001 50
DECR stock:item:1001
DECRBY stock:item:1001 3
GET stock:item:1001이런 명령은 조회수, 좋아요 수, 다운로드 수, API 호출 횟수처럼 이벤트가 발생할 때마다 숫자를 바꾸는 기능에 적합하다. 중요한 장점은 증가와 감소가 Redis 안에서 원자적으로 처리된다는 점이다. 여러 애플리케이션 서버가 동시에 INCR을 보내도 서로 값을 덮어쓰지 않는다.
하지만 모든 카운터가 단순하지는 않다. 재고 수량을 예로 들어 보자.
DECR stock:item:1001이 명령은 빠르고 원자적이다. 그러나 "재고가 0보다 작아지면 안 된다"는 조건까지 자동으로 보장하지는 않는다. 감소 후 값이 음수가 되었을 때 되돌리는 방식은 동시 요청이 많을 때 복잡해진다. 조건 확인과 차감을 하나로 묶어야 하는 문제는 트랜잭션이나 Lua 스크립트가 필요하다.
API 호출 제한에도 카운터를 사용할 수 있다.
INCR rate:user:1001:202605301230
EXPIRE rate:user:1001:202605301230 60키에 사용자 ID와 시간 구간을 포함하면 "사용자별 1분 호출 횟수"를 셀 수 있다. 카운터를 만들고 TTL을 설정하면 일정 시간이 지난 뒤 자동으로 사라진다. 다만 위 예시처럼 INCR과 EXPIRE를 별도 명령으로 실행하면 중간 실패 가능성이 있다. 또한 매 요청마다 EXPIRE를 다시 호출하면 TTL이 계속 갱신될 수 있으므로, 고정된 시간 구간을 엄격하게 다뤄야 할 때는 첫 증가 시점에만 만료 시간을 붙이는 방식이 더 적합하다. 학습 단계에서는 구조를 이해하는 데 집중하고, 운영용 속도 제한기는 Lua 스크립트나 트랜잭션으로 원자성을 보강하는 편이 안전하다.
숫자 연산의 흔한 오류도 기억하자.
SET article:42:views "many"
INCR article:42:views이 경우 Redis는 정수로 해석할 수 없는 값이라 오류를 반환한다. 또한 키 이름을 잘못 입력하면 INCR이 새 키를 만들 수 있다. artcle:42:views처럼 오타가 난 키도 1부터 시작한다. 카운터 키는 네이밍 규칙과 모니터링이 특히 중요하다.
MGET과 MSET으로 왕복 횟수 줄이기
애플리케이션과 Redis가 통신할 때는 명령 하나마다 네트워크 왕복 비용이 든다. 키 세 개를 가져오기 위해 GET을 세 번 보내는 것보다 MGET 한 번으로 가져오는 편이 효율적일 수 있다.
MGET cache:product:1001 cache:product:1002 cache:product:1003MGET은 요청한 키 순서대로 값을 배열로 반환한다. 존재하지 않는 키나 String이 아닌 값을 가진 키는 nil로 반환된다.4
1) "{...product 1001...}"
2) (nil)
3) "{...product 1003...}"여기서 중요한 것은 위치다. 두 번째 결과가 nil이면 두 번째로 요청한 cache:product:1002가 없거나 String 값이 아니라는 뜻이다. 애플리케이션 코드에서는 요청 키 목록과 응답 배열의 인덱스를 정확히 맞춰야 한다.
여러 키를 한 번에 저장할 때는 MSET을 쓴다.
MSET product:1001:name "keyboard" product:1001:price "39000" product:1002:name "mouse" product:1002:price "19000"MSET은 여러 키를 한 번에 설정하고, 기존 값이 있으면 SET처럼 덮어쓴다. 또한 주어진 키들이 부분적으로만 갱신된 상태를 다른 클라이언트가 볼 수 없도록 원자적으로 처리된다.5
하지만 MGET, MSET도 무조건 좋은 것은 아니다. 처리 대상 키가 많아지면 Redis가 한 명령을 수행하는 시간도 늘어난다. Redis는 매우 빠르지만 단일 명령이 오래 걸리면 다른 요청 처리에 영향을 줄 수 있다. 수천, 수만 개 키를 한 번에 처리하기보다 적절한 크기로 나누는 편이 안전하다.
또 하나의 주의점은 TTL이다. 기본 MSET은 여러 값을 저장하지만 각 키에 서로 다른 TTL을 붙이는 용도로는 적합하지 않다. 캐시처럼 만료 시간이 중요한 데이터라면 SET ... EX를 여러 번 사용하거나, 파이프라이닝으로 왕복 비용을 줄이는 방식을 고려한다. Cluster 환경에서는 여러 키 명령에 추가 제약이 생길 수 있는데, 이 부분은 수평 확장에서 다시 다룬다.
JSON 문자열 저장의 장점과 비용
String은 JSON 저장에 자주 쓰인다.
SET user:1001:profile '{"id":1001,"name":"kim","grade":"gold"}' EX 3600이 방식은 읽기 중심 캐시에 좋다. 애플리케이션은 Redis에서 문자열 하나를 가져와 JSON으로 파싱한 뒤 바로 응답을 만들 수 있다. 상품 상세, 게시글 본문, 외부 API 응답처럼 전체 객체를 한 번에 읽고 한 번에 버리는 데이터에 잘 맞는다.
문제는 부분 갱신이다. 예를 들어 grade만 gold에서 vip로 바꾸고 싶어도 Redis String은 JSON 내부 필드를 모른다. 보통은 다음 과정을 거친다.
GET user:1001:profile애플리케이션에서 JSON 파싱
필드 수정
다시 JSON 문자열로 직렬화
SET user:1001:profile ... EX 3600
객체가 작고 갱신이 드물다면 괜찮다. 하지만 값이 크거나 일부 필드만 자주 바뀐다면 매번 전체 문자열을 다시 써야 한다. 동시에 여러 요청이 같은 JSON을 읽고 수정하면 나중에 저장한 값이 먼저 저장한 변경을 덮어쓸 수도 있다.
이럴 때는 다음 장에서 배울 Hash가 더 적합할 수 있다. Hash는 객체의 필드를 나누어 저장하고 특정 필드만 갱신할 수 있다. 반대로 "항상 전체를 읽고 전체를 다시 만든다"면 String이 더 단순하다.
직렬화 형식도 선택해야 한다.
JSON: 사람이 읽기 쉽고 디버깅이 편하다.
MessagePack, Protobuf 같은 바이너리 형식: 더 작고 빠를 수 있지만 확인과 수정이 어렵다.
압축된 문자열: 메모리는 줄일 수 있지만 CPU 비용과 복잡도가 늘어난다.
처음에는 JSON으로 시작해도 좋다. 다만 값이 커지고 트래픽이 늘어나면 "Redis에 넣었으니 빠르다"가 아니라 "네트워크로 큰 문자열을 계속 주고받고 있지 않은가"를 확인해야 한다. Redis String 값 하나의 최대 크기는 512MB이지만, 이 한계에 가까운 값을 저장해도 된다는 뜻은 아니다. 운영에서는 큰 값을 자주 읽고 쓰는 패턴이 지연 시간과 메모리 사용량을 키운다.
String을 사용할 때의 판단 기준
String은 다음 상황에 잘 맞는다.
단일 값 저장: 설정값, 토큰, 인증 코드
전체 객체 캐시: 상품 상세, 게시글, API 응답
숫자 카운터: 조회수, 호출 횟수, 간단한 점수
짧은 생명주기 데이터: TTL이 있는 임시 상태
반대로 다음 상황에서는 다시 생각해야 한다.
객체의 일부 필드만 자주 갱신한다.
값이 너무 커서 읽기·쓰기 비용이 크다.
재고 차감처럼 조건 확인과 변경을 반드시 하나로 묶어야 한다.
여러 키를 대량으로 한 번에 처리해 Redis를 오래 점유할 수 있다.
Redis String은 가장 단순하지만 가장 많이 쓰인다. SET 옵션으로 저장 조건과 만료를 표현하고, INCR 계열로 카운터를 만들고, MGET과 MSET으로 네트워크 왕복을 줄일 수 있다. 이 기본기만 제대로 익혀도 캐시, 인증 코드, 조회수, 속도 제한 같은 많은 기능을 만들 수 있다.
다음 장에서는 객체 데이터를 String 하나에 통째로 넣는 방식의 한계를 넘어서, Hash로 필드 단위 데이터를 다루는 방법을 배운다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.