ssuperjun 님의 블로그
[스터디4] 개발자를 위한 레디스 본문
1장: 마이크로서비스 아키텍처와 레디스
모놀리틱 아키텍처 vs 마이크로서비스 아키텍처
- 모놀리틱 아키텍처에서는 모든 데이터를 하나의 DB에서 관리하고자 해 중앙 집약저인 RDBMS를 표준으로 삼음
- MSA에서는 각 서비스가 독립적으로 동작
- 비즈니스 특성과 데이터의 형태를 고려해 RDBMS or NoSQL 중 하나를 선택하는 것이 중요
NoSQL의 특징: 실시간 응답, 확장성(scale-out), 고가용성(복제, 샤딩 등), 클라우드 네이티브(클라우드 제공 업체의 DBaaS 이용 가능), 단순성(데이터 저장소를 간단하게 사용, 서비스별 적합한 데이터 모델 적용), 유연성(폭발적으로 증가한 데이터를 저장)
NoSQL 데이터 저장소 유형: 그래프, 칼럼(column 단위로 데이터 저장), 문서(스키마 유연), 키-값 유형
레디스의 특징: 인메모리 DB(빠름, 휘발), 다양한 자료구조 지원, 싱글 스레드(메인 1개, 별도 3개), 고가용성 및 확장성(센티널, 클러스터 구조)
레디스의 역할: 데이터 저장소(AOF, RDB), 메시지 브로커(stream 자료구조를 메시지 큐로 활용해 pub/sub 기능 사용)
*pub/sub: 1개의 채널에 데이터를 던지면 이 채널을 듣고 있는 모든 소비자는 데이터를 빠르게 가져감
2장: 레디스 시작하기
레디스 설치: 소스 파일 이용 방식(wget 후 압축 해제), apt 및 yum repository 이용 방식
서버 환경 설정 변경: maxclients 값 변경, THP 비활성화(RDB로 데이터 저장 시 CoW 과정에서 4KB만 복사해도 될 것을 계속 2MB씩 복사해 메모리 낭비), vm.overcommit_memory=1(필요한 메모리를 초과해 할당하는 것도 가능), tcp-backlog 파라미터(Redis가 클라이언트와 통신할 때 사용하는 큐 크기 지정)
레디스 설정 파일 변경: redis.conf(실행 시에도 이 설정파일 사용)
bind(Redis에 접속하는 ip주소 허용), protected-mode(yes면 패스워드 설정), requirepass(서버 접속 패스워드), daemonize(yes면 데몬으로 실행), dir(작업 디렉토리)
레디스 실행: 프로세스 시작 및 종료, 레디스 접속(redis-cli 입력), 데이터 저장과 조회(SET key value, GET key)
여러 데이터 저장과 조회 = MSET, MGET
3장: 레디스 기본 개념
자료구조
string: 키 하나에 value 하나. INCR(value 1 증가) 커맨드는 원자적(일관성 유지)
list: 일반적인 배열. 스택과 큐로 사용 가능
list 커맨드: LPUSH, RPUSH, LRANGE, RANGE(범위 출력), LPOP, LTRIM(지정한 범위만 남기기), LINSERT(BEFORE 옵션으로 앞에 추가, AFTER 옵션으로 뒤에 추가), LSET(덮어쓰기), LINDEX(특정 인덱스의 데이터 확인)
hash: key-value(필드-값) 형태. 각 아이템(row)마다 다른 필드(col)를 가질 수 있음
hash 커맨드: HSET, HGET, HMGET(여러 필드 값 가져오기), HGETALL(모든 필드-값 쌍 반환)
set: 집합 연산, 유일한 원소
set 커맨드: SADD, SMEMBERS(조회), SREM(삭제), SPOP, SUNION, SINTER, SDIFF
sorted set: value는 스코어-값 쌍. 스코어로 정렬됨
ZADD 옵션: XX(아이템이 이미 존재할 때만 스코어 업데이트), NX(아이템이 존재하지 않을 때만 신규 삽입), LT(업데이트할 스코어가 기존보다 작을 때만 업데이트. 아니면 신규 삽입), GT(클 때만 업데이트)
ZRANGE 옵션: WITHSCORES(데이터와 스코어 함께 출력), REV(역순), BYSCORE(스코어로 데이터 조회), '('(해당 스코어 제외), BYLEX(사전 순 조회)
비트맵: key-value에서 value가 비트 형태. 저장 공간이 512MB(2^32 비트)로 작음. SETBIT, GETBIT, BITCOUNT
hyperloglog: 집합의 카디널리티(원소의 개수)를 추정하기 위한 데이터 구조. (ex. 검색 엔진의 하루 검색어 수) 같은 데이터를 여러번 계산하지 않도록 과거의 항목을 기억하기 때문에 메모리를 효과적으로 줄임. 메모리는 매우 적게 사용하고 오차는 적음.
hyperloglog는 카운팅 시 통계적 근사 수치를 이용
데이터를 앞에서부터 읽으며 현재까지 관측된 데이터를 바탕으로 전체 개수를 추정하는데, 운에 의한 오차를 줄이고자 데이터를 수많은 버킷으로 나눈 뒤 각 버킷에 기록된 추정치를 하나로 합치는 조화 평균을 수행함
PFADD(저장), PFCOUNT(아이템 개수 추정)
geospatial: 경도 위도 데이터를 sorted set으로 저장. GEOADD(저장), GEOPOS(조회), GEOSEARCH(특정 거리 내 아이템 조회)
stream: pub/sub 기능(실시간 이벤트, 로그성 데이터 저장 등). 7장에서 자세히
키의 자동 생성과 삭제
1. 키가 존재하지 않을 때 아이템을 넣으면 아이템을 삽입하기 전에 빈 자료구조 생성
2. 모든 아이템을 삭제하면 키도 자동 삭제(stream 제외)
3. 키가 없는 상태에서 읽기 전용 커맨드 수행 시, 키가 있으나 아이템이 없는 것처럼 동작
키 관련 커맨드: EXISTS(키 존재 확인), KEYS(모든 키 조회), SCAN(커서를 기반으로 특정 범위의 키만 조회. KEYS보다 안전), SORT, RENAME, COPY(source키의 값을 dest키 값에 복사), TYPE(자료구조 타입 반환), OBJECT(키 상세정보)
키 삭제: FLUSHALL(모든 키 삭제. 중간에 다른 명령어 못 끼어듦), DEL(키&모든 아이템 삭제), UNLINK(DEL처럼 키와 데이터를 삭제하지만, 다른 명령어 못 끼어듦)
키 만료 시간 정의: EXPIRE, EXPIREAT, EXPIRETIME, TTL(키가 몇초 뒤에 만료되는지)
=> 메모리에 저장될 수 있는 데이터는 한정적
4장: 레디스 자료 구조 활용 사례
sorted set: 게임 서비스에서 사용자의 스코어를 기반으로 데이터가 정렬됨(실시간 리더보드), 게시물 태그 기능도 RDBMS에 비하면 간결하게 구현 가능
랜덤 데이터 추출: hash, set, sorted set에 저장된 전체 키 중 하나를 O(1)로 추출
좋아요 처리하기: 관계형 DB는 셀렉트로 중복값 확인 후 insert가 필요한데, 레디스는 set에 sadd만으로 쉽게 구현 가능
일일 순 방문자수(중복 아님) 구하기: string 자료구조의 비트맵 사용(마치 비트마스킹으로 방문자에 해당하는 비트를 0에서 1로 바꿈)
sorted set 시간 복잡도
| 리더보드 요구사항 | 명령어 | 복잡도 |
| 점수 갱신 | ZINCRBY | O(log N) |
| 내 현재 순위 조회 | ZREVRANK | O(log N) |
| 상위 K명 조회 | ZREVRANGE | O(log N + K) |
| 특정 점수 구간 조회 | ZRANGEBYSCORE | O(log N + K) |

Sorted Set 내부 구조
Skip List (스킵 리스트) — 점수(score) 기준의 범위 조회와 순위 계산에 사용. 평균 O(log N)의 탐색·삽입·삭제를 보장.
Hash Table (해시 테이블) — 멤버 이름으로 점수를 즉시 조회할 때 사용. 덕분에 ZSCORE가 O(1).
=> 이 두 구조로 인해 메모리를 더 쓰는 대신, 순위 조회와 점수 조회 모두 빠르게 처리
Heap과 Skip List 차이

5장: 레디스를 캐시로 사용하기
읽기 전략
look aside: 일반적인 캐시 미스 시 전략(캐시(Redis)에 데이터 없으면 원본 DB에서 읽어옴)
cache warming: 시스템이 처음 시작된 경우, 새로운 공연 티켓팅 오픈 상황이라면 레디스에 데이터가 없어 죄다 캐시 미스 발생 -> 해결 위해 미리 DB에서 캐시로 데이터 밀어넣기
쓰기 전략
앱 -> Redis -> 원본 DB
write through: 데이터 업데이트 시 캐시에 쓰기&DB에 쓰기 - 캐시는 항상 최신의 데이터 유지 but 시간 리소스 소비
cache invalidation: DB에 업데이트하면 캐시에서 해당 데이터 삭제 - 리소스 낭비 적게
write behind(back): 캐시에 우선 업데이트 후, 비동기적으로(나중에) DB에 업데이트
적절한 만료 시간(expire time) 설정이 중요
데이터가 꽉 찼을 때 write 안되게 설정(maxmemory=noeviction)했다면, 장애 원인이 되기도 함
eviction(캐시에 데이터 가득 찼을 때 삭제하는 정책) 종류: Noeviction, LRU, LFU, 랜덤
TTL(time to live)이 너무 짧으면 cache stampede 등 불필요한 작업 발생
cache stampede 현상: 만료된 키를 여러 앱이 한꺼번에 요청 -> DB에서 데이터 읽어올 때 '중복 읽기' 발생 -> 각 앱이 읽어온 데이터를 레디스에 쓰면서 '중복 쓰기' 발생
캐시 스탬피드 완화하려는 노력들: 적절한 만료 시간 설정, 선 계산(DB에서 캐시로 미리 데이터 가져오기), PER 알고리즘(만료에 가까워질수록 자주 만료된 캐시 항목 확인하는 '확률 함수'로 선계산)
키가 만료돼도 바로 삭제되지 않음
passive 방식(클라이언트가 키가 접근할 때 만료된 키라면 그때 삭제) vs. active 방식(1초에 10번 간격으로, TTL 있는 키 중 랜덤하게 20개 추출한 뒤 만료된 키 모두 삭제)
세션 = 클라이언트의 상태 정보(로그인 동안 정보 유지)
레디스를 세션 스토어로 사용해 클라이언트(사용자)의 트래픽 분산
클라이언트 <-> 여러 서버 <-> 세션 스토어 <-> 데이터베이스
만약 세션 스토어가 없다면, 유저의 세션 데이터는 여러 서버로 복사돼 공간 낭비
hash 자료구조가 세션 데이터 저장에 적합(value에 이름, id, 마지막페이지 등 기록)
6장: 레디스를 메시지 브로커로 사용하기
MSA 구조에서 예기치 못한 통신 장애 가능 - 비동기 통신이 바람직
메시지 브로커 핵심 역할: 메시지를 어딘가에 쌓아 둔 뒤 나중에 처리하도록
메시징 큐

이벤트 스트림


생산자 or 발행자: 데이터 생성
소비자 or 구독자: 데이터 수신, 조회
이벤트 스트림 특징: 메시지 복제 저장 불필요, 데이터 영속성, 다대다 상황에서 유리
레디스의 pub/sub
데이터가 한번 채널 전체에 전파되면 삭제됨
오류 처리를 고려하지 않아, 성능 향상이나 비동기 작업에 유리
정확한 메시지 전달 보장 대신, 간단한 알림같은 fine-and-forget 패턴에 적합
레디스 stream 자료구조
kafka 영감
데이터가 계속 추가되는 append-only 방식
데이터 전파 명령어: publish
특정 채널 구독 명령어: subscribe
특정 패턴과 일치하는 채널 한번에 구독 명령어: psubscribe
클러스터 구조에서 메시지가 모든 레디스 노드에 복제되는 건 비효율적
=> share pub/sub 기능: 같은 슬롯을 가지고 있는 노드 간에만 pub/sub 메시지 전파
레디스의 list를 메시징 큐로 사용하기
트위터 각 유저의 타임라인 캐시 데이터를 레디스에서 list 자료구조로 관리 - 자주 들어오지 않는 사용자는 append 제한 가능
이벤트 루프(큐에 새 데이터가 있는지 체크하는 과정)의 주기적인 폴링은 비효율적 -> list의 블로킹 기능으로 데이터 들어올 때까지 대기
스트림을 처리하는 것 = 연속적인 데이터를 처리하는 것(대량 데이터는 쪼개서 처리)
예매 서비스에서
여러 프로듀서가 이벤트 생성 - 웹 서버, 상품, 결제 이벤트
다양한 소비자가 이를 처리: MySQL, DW는 상품 처리, MySQL, 캐시는 웹 서버 프로세스 처리, MySQL, 이메일, push 서비스는 결제 프로세스 처리
레디스의 stream 데이터는 유니크한 ID를 가지며, 이는 저장 시점의 시간. 시간으로 특정 데이터 검색 가능
스트림 생성과 데이터 입력
데이터 조회: 1. kafka처럼 소비자가 실시간으로 처리되는 데이터 리스닝 2. ID를 이용해 필요한 데이터 검색
소비자와 소비자그룹
팬아웃: 같은 데이터를 여러 소비자에게 전달
레디스 stream 데이터는 고유 ID로 순서 보장 가능, kafaka는 불가
=> kafka의 소비자 그룹: kafka에서 데이터 처리 순서 보장, 한 토픽을 여러 파티션으로 분리해 파티션 냅누에서는 메시지의 순서 보장
레디스의 소비자 그룹: 소비자 그룹 내의 한 소비자는 다른 소비자가 아직 읽지 않은 데이터를 읽어 감(병렬 처리)
ACK와 보류 리스트
장애 처리 방식
여러 서비스(소비자)가 메시지 브로커를 이용해 데이터를 처리하는 상황에서, 갑자기 장애로 시스템이 종료된 경우 재처리 필요
어디까지 전달? 전달된 메시지 처리 유무? 확인 필요
전달된 메시지 리스트를 보류 리스트에 저장하고 있다가, 소비자로부터 ACK를 받으면 데이터 처리가 완료됐으므로 보류 리스트에서 제거
kafka에서의 오프셋: 소비자가 다음으로 읽어야 할 위치 기록
소비자가 ACK를 보내는 방식 3가지
at most once: 소비자는 메시지를 받자마자 실제 처리하기 전에 ACK 전송. 실제 처리 도중 소비자 다운 시 문제. 손실을 감수하고 빠른 응답 필요한 경우 선택
at least once: 소비자는 받은 메시지를 모두 처리한 뒤 ACK 전송. 실제로 메시지가 처리됐찌만 ACK를 전송하기 전에 소비자가 다운되는 경우 발생 가능. 이미 처리한 메시지를 한번 더 처리하는 문제 존재
exactly once: 모든 메시지가 무조건 한번씩 전송. 이를 위해 set 등의 추가 자료구조로 이미 처리된 메시지인지 확인
메시지 재할당
소비자에게 장애가 발생하면, 다른 소비자가 보류 리스트를 이용해 대신 처리
stream 상태 확인 커맨드: XINFO
특정 소비자 그룹에 속한 소비자 정보 확인: XINFO consumers <stream key><소비자그룹 이름>
stream에 속한 전체 소비자 그룹 list 확인: XINFO GROUPS <stream key>
stream 자체의 정보(내부 인코딩 방식, 첫번째와 마지막 메시지 ID): XINFO STREAM <stream key>
7장: 레디스 데이터 백업 방법
데이터 영구 저장 방식(캐시지만 백업이 필요한 경우)
AOF: 데이터 변경 커맨드를 파일에 그대로 저장
RDB: 스냅샷
저장에 fork, CoW(새로운 값이 write될 때만 메모리에 새로 할당) 사용
연결 재시도 시 새 메모리로 따로 저장하므로, max memory 값은 실제 메모리 크기의 절반만 설정 필요
'인턴' 카테고리의 다른 글
| [스터디3] MySQL 스터디 - 클러스터드 vs 논클러스터드 인덱스 2차 실험 (0) | 2026.03.20 |
|---|---|
| [장애 이력 자동 작성 도구] 발표 이후 고도화 - 장애 감지 로직 수정 (0) | 2026.03.19 |
| [장애 이력 자동 작성 도구 17] 최종 발표 자료 (0) | 2026.03.12 |
| [장애 이력 자동 작성 도구 16] Redis, MySQL 추가 고민 (0) | 2026.03.12 |
| [장애 이력 자동 작성 도구 15] MySQL 시행착오 (0) | 2026.03.12 |