백엔드 데이터 인프라 52편. Redis Set — 중복 없는 컬렉션. SADD·SMEMBERS·SISMEMBER·SINTER·SUNION·SDIFF 같은 핵심 명령어와 태그·좋아요·읽음 표시·공통 친구 추천 같은 집합 연산 패턴까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 52편이에요. 51편 에서 Redis List 로 양 끝이 빠른 큐·스택을 잡았다면, 이번 52편은 Redis Set — 중복 없는 컬렉션이에요. Java HashSet (중복 없는 컬렉션 자료구조) · Python set 과 같은 모델이고, 거기에 집합 연산 (교집합·합집합·차집합)이 매우 강력해요.
Redis Set이 어렵게 느껴지는 이유
Set은 개념이 단순한데, 두 가지가 처음 헷갈려요.
첫째, List 와 어떻게 다른가 가 잡히지 않아요. 둘 다 여러 값을 담는 컬렉션인데 List는 중복 허용 + 순서 보존, Set은 중복 X + 순서 무의미. 같은 컬렉션 카테고리라 어디에 어느 걸 써야 할지 처음에는 혼동돼요.
둘째, Sorted Set 과 이름이 비슷해서 헷갈려요. Sorted Set 은 점수 기준 정렬 까지 추가된 별도 타입이에요. Set + 점수 가 Sorted Set 이라고 외우면 깔끔해요. Set 은 정렬·점수가 없어요.
이 글에서 Set이 자연스러운 자리 와 집합 연산이 빛나는 패턴 을 정리하고, 53편 Sorted Set 으로 넘어가요.
Set 기본 명령어 6종
SADD — 멤버 추가
> SADD tags:article99 "java"
(integer) 1
> SADD tags:article99 "spring" "tutorial" "java"
(integer) 2 # "java" 는 이미 있음 → 추가 안 됨
반환값 = 실제 추가된 멤버 수 (중복 입력은 카운트 안 됨). 복잡도 = O(1) per 멤버.
SMEMBERS — 모든 멤버 조회
> SMEMBERS tags:article99
1) "java"
2) "spring"
3) "tutorial"
순서는 보장 안 됨 (Set 정의상). 복잡도 = O(N). 큰 Set은 SSCAN (Set 점진 스캔 명령) 으로 페이징.
SISMEMBER — 멤버 존재 여부 (O(1))
> SISMEMBER tags:article99 "java"
(integer) 1 # 있음
> SISMEMBER tags:article99 "python"
(integer) 0 # 없음
O(1) — 멤버 수와 무관해요. "이 사용자가 이 글에 좋아요 눌렀나?" 같은 체크가 수십억 좋아요 중에서도 즉시 끝나요.
SCARD — 멤버 수 (O(1))
> SCARD tags:article99
(integer) 3
SET 의 length. 카운터 용도로도 자주 써요.
SREM — 멤버 제거
> SREM tags:article99 "tutorial"
(integer) 1
> SREM tags:article99 "tutorial" "java" "python"
(integer) 1 # 실제 제거된 = "java" 한 개 (이미 tutorial 빠짐, python 없음)
SPOP / SRANDMEMBER — 랜덤 멤버
> SPOP tags:article99 # 랜덤 하나 빼서 반환 + Set 에서 제거
"spring"
> SRANDMEMBER tags:article99 # 랜덤 하나 반환 (Set 유지)
"java"
> SRANDMEMBER tags:article99 3 # 랜덤 3개 (중복 X)
추첨·랜덤 추천 같은 곳에 쓰여요.
여기서 시험 함정이 하나 있어요 — SMEMBERS 순서는 의미 없어요. Set은 insertion order (삽입 순서) 도 보존하지 않아요. 호출할 때마다 순서가 달라질 수 있어요 (Redis 내부 구조 의존). 순서가 중요하면 List 또는 Sorted Set 로 가야 해요.
집합 연산 — Set 의 진짜 강점
여기가 Set 이 빛나는 자리. 다른 자료구조로는 만들기 까다로운 교집합·합집합·차집합이 한 줄로 끝나요.
SINTER — 교집합
> SADD likes:article99 "user42" "user99" "user77"
(integer) 3
> SADD likes:article100 "user42" "user99" "user88"
(integer) 3
> SINTER likes:article99 likes:article100
1) "user42"
2) "user99"
"두 글에 모두 좋아요 누른 사용자" — 추천 시스템의 가장 기본 빌딩 블록이에요.
SUNION — 합집합
> SUNION likes:article99 likes:article100
1) "user42"
2) "user77"
3) "user88"
4) "user99"
"두 글 중 하나라도 좋아요 누른 사용자".
SDIFF — 차집합
> SDIFF likes:article99 likes:article100
1) "user77" # article99 만 좋아요, article100 은 아님
"article99 만 좋아요 누른 사용자 (article100 은 안 봄)".
결과를 저장 — *STORE
> SINTERSTORE result likes:article99 likes:article100
(integer) 2
# 결과 Set "result" 에 저장
SUNIONSTORE·SDIFFSTORE 도 같아요. 연산 결과를 새 Set 으로 보존 해서 후속 처리에 쓸 수 있어요.
한 줄 정리 — 집합 연산 = SINTER(교집합) · SUNION(합집합) · SDIFF(차집합) · *STORE(결과 저장). 추천·태그 매칭·중복 제거의 표준 도구.
패턴 1: 태그 시스템
가장 흔한 자리. 각 글에 태그 Set, 각 태그에 글 Set — 양방향 인덱스.
# 글에 태그 추가
> SADD tags:article99 "java" "spring" "redis"
# 태그에 글 추가 (역방향)
> SADD articles:java "article99"
> SADD articles:spring "article99"
> SADD articles:redis "article99"
쿼리 — "java + spring 둘 다 태그 된 글":
> SINTER articles:java articles:spring
1) "article99"
DB로 같은 쿼리를 짜면 JOIN 두 번 + WHERE IN — 인덱스 잘 박혀 있어도 ms 단위. Redis Set 교집합은 μs 단위예요.
패턴 2: 좋아요·읽음 표시
# user42 가 article99 에 좋아요
> SADD likes:article99 "user42"
# user42 가 article99 좋아요 눌렀나?
> SISMEMBER likes:article99 "user42"
(integer) 1
# 좋아요 수
> SCARD likes:article99
# 사용자가 좋아요 누른 모든 글
> SADD user:42:likes "article99"
> SMEMBERS user:42:likes
중복 자동 제거 — 같은 사용자가 두 번 좋아요 눌러도 SADD 는 무시해요. 애플리케이션에서 중복 체크 안 해도 되고요.
읽음 표시도 같아요:
> SADD seen:user42 "article99" "article100"
> SISMEMBER seen:user42 "article99" # 봤나?
패턴 3: 공통 친구·추천
소셜 추천의 클래식 — "내 친구의 친구 (내가 모르는)".
# 친구 관계
> SADD friends:user42 "user99" "user77" "user88"
> SADD friends:user99 "user42" "user66" "user77"
# user99 의 친구 중 내가 모르는 사람
> SDIFF friends:user99 friends:user42
1) "user66"
"공통 친구":
> SINTER friends:user42 friends:user99
1) "user77"
한 줄로 끝나는 마법이에요. RDB로 같은 쿼리 = 복잡한 self-JOIN (한 테이블을 자기 자신과 조인).
패턴 4: 중복 제거된 카운팅
# 오늘 article99 본 사용자 (중복 X)
> SADD viewers:article99:2026-05-17 "user42"
> SADD viewers:article99:2026-05-17 "user42" # 중복 → 무시
> SADD viewers:article99:2026-05-17 "user99"
> SCARD viewers:article99:2026-05-17
(integer) 2 # 고유 방문자 수
여기서 정말 중요한 자리 — 사용자가 1억 명 규모면 Set 메모리 부담 (각 user_id 가 문자열로 저장). 그럴 땐 HyperLogLog (Probabilistic 데이터 타입, 즉 확률 기반 근사 자료구조, 48편) 으로 근사 카운팅 이 더 효율적이에요. 정확한 멤버십이 필요한가 가 선택 기준이고요.
Set 내부 구조 — intset vs hashtable
여기까지 따라오셨다면 한 가지 의문이 들 거예요 — "Set 이 메모리 효율은 어떤가요?". 답은 Hash 와 비슷하게 자동 전환.
- intset — 모든 멤버가 작은 정수 인 경우, 정렬된 정수 배열로 저장 → 매우 메모리 효율
- listpack — 작은 Set (멤버 수 ≤ 128, 각 멤버 크기 ≤ 64바이트) → 연속 메모리
- hashtable — 큰 Set → 전통 해시 테이블
설정 = set-max-intset-entries·set-max-listpack-entries·set-max-listpack-value.
사용자 ID 가 정수 라면 intset 에 들어가 매우 효율적이에요. 문자열 ID 라도 작은 Set 은 listpack 으로 들어가고요.
시험 직전 한 번 더 — Redis Set 함정 압축 노트
- Set = 중복 X + 순서 무의미, Java HashSet 과 같은 모델
- 핵심 명령 6종 =
SADD·SMEMBERS·SISMEMBER·SCARD·SREM·SPOP SADD반환값 = 실제 추가된 멤버 수 (중복은 카운트 안 됨)SISMEMBER= O(1) — 수십억 멤버 중에서도 즉시 체크SMEMBERS순서 = 보장 안 됨 — 호출할 때마다 다를 수 있음SCARD= O(1), 카운터 용도SPOP= 랜덤 멤버 빼서 반환 + 제거SRANDMEMBER= 랜덤 반환만 (제거 X)- 집합 연산 =
SINTER(교집합) ·SUNION(합집합) ·SDIFF(차집합) *STORE변형 = 결과를 새 Set으로 저장 (SINTERSTORE등)- 자주 쓰는 자리 = 태그·좋아요·읽음 표시·중복 제거·추천
- 태그 시스템 = 양방향 인덱스 (
tags:article99↔articles:java) - 공통 친구 =
SINTER friends:A friends:B - "내 친구의 친구" =
SDIFF friends:B friends:A - 좋아요 중복 =
SADD가 자동 제거 (애플리케이션 체크 X) - 정확한 멤버십 필요 = Set, 근사 카운트면 HyperLogLog
- 내부 구조 = intset (작은 정수) · listpack (작은 일반) · hashtable (큰 Set)
- 정수 ID = intset 매우 메모리 효율
- Set vs List = 중복 허용 + 순서 보존 여부
- Set vs Sorted Set = 점수와 정렬이 필요한가
- 큰 Set 조회 =
SSCAN으로 페이징
공식 문서: Redis Sets 에서 Set 명령어 전체 reference 를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 47편 — Redis란 + PostgreSQL과의 역할 분담
- 48편 — Redis 데이터 타입 13종 한 번에 정리
- 49편 — Redis String 깊이 + 분산 락 패턴
- 50편 — Redis Hash + 객체 캐싱 패턴
- 51편 — Redis List + 큐·캡 리스트 패턴
다음 글: