Redis의 Set 자료구조란?
정렬되지 않은 unique string(중복 없는 문자열)의 Collection

Redis에는 Sorted Set이라는 자료 구조가 존재한다.
Sorted Set은 중복 없는 문자열이 점수를 기준으로 자동 정렬되는 자료 구조이다.
Set은 Sorted Set과 마찬가지로 하나의 Key에 대한 중복 없는 문자열의 Collection이지만, score 개념이 없고 정렬되지 않는다.
즉, 순서가 보장되지 않는 단순 집합이다.
이러한 특성 때문에 다음과 같은 상황에서 효율적으로 사용할 수 있다고 한다.
- 고유한 항목 추적 (예: 특정 블로그 게시글에 접근한 모든 고유 IP 주소를 추적)
- 관계 표현 (예: 특정 역할(role)을 가진 모든 사용자의 집합)
- 일반적인 집합 연산 수행 - 교집합(intersection), 합집합(union), 차집합(difference) 등
Set 기본 명령어
데이터 삽입
// SADD [KEY] [VALUE]
// 한번에 여러 개의 value를 삽입하는 것도 가능하다
SADD myset Hello World
(integer) 2
// 중복 문자열은 삽입이 불가
SADD myset World
(integer) 0
모든 데이터 조회
// SMEMBERS [KEY]
SMEMBERS myset
1) "Hello"
2) "World"
저장된 데이터 개수 조회
// SCARD [KEY]
SCARD myset
(integer) 2
여러 개의 Set의 Value 교집합
// 임의의 Set 2개 생성
SADD set1 a b c
SADD set2 b c d
// SINTER [KEY1] [KEY2] ..
SINTER set1 set2
1) "b"
2) "c"
여러 개의 Set의 Value 합집합
// SUNION [KEY1] [KEY2
SUNION set1 set2
1) "c"
2) "d"
3) "a"
4) "b"
여러 개의 Set의 Value 차집합
// SDIFF [KEY1] [KEY2] ...
SDIFF set1 set2
1) "a"
Set에 특정 Value 존재하는지 확인
// SISMEMBER [KEY] [VALUE]
SISMEMBER set1 a
// 있으면 1
(integer) 1
SISMEMBER set1 d
// 없으면 0
(integer) 0
Set에서 특정 Value 삭제
// SREM [KEY] [VALUE]
SREM set1 a
(integer) 1
// 삭제 확인
SMEMBERS set1
1) "b"
2) "c"
Bitmap 자료구조란?
String 타입 위에 정의된, String을 비트 벡터(bit vector)처럼 다루는 비트 단위(bit-oriented) 연산의 집합
여기서 비트 벡터란?
비트 벡터(bit vector)는 0과 1이 일렬로 늘어선 배열이다.

String은 내부적으로 byte(8 bit)의 배열 형태이기 때문에, 0과1의 긴 배열로 볼 수 있으며, 비트 벡터처럼 다루는 것이 가능하다.
String은 바이너리 데이터를 그대로 저장할 수 있는 binary-safe blob(Binary Large Object) 이고 최대 길이가 512MB이기 때문에, 하나의 Bitmap으로 최대 2^32개의 서로 다른 비트를 다룰 수 있다.
또한 하나 이상의 String에 대해 비트 단위 연산(bitwise operation)을 수행할 수 있다.
이 비트 단위 연산을 통해 다음과 같은 계산도 가능하다.
예로 들어 다음 두 Bitmap이 존재한다고 할때
월요일: 사용자 0,2번 출석 → 1 0 1 0 ...
화요일: 사용자 1,2번 출석 → 0 1 1 0 ...
AND, OR, XOR 계산으로 여러 날의 출석 데이터를 조합한 집계가 가능하다.
- AND (둘 다) → 월·화 둘 다 출석한 사용자 (2)
- OR (둘 중 하나라도) → 월 또는 화에 한 번이라도 출석한 사용자 (0, 1, 2)
- XOR (한쪽만) → 둘 중 하루만 출석한 사용자 (0, 1)
Bitmap의 대표적인 활용 사례로는 다음과 같은 것들이 있다고 한다.
- 효율적인 집합 표현 — 집합의 원소가 0부터 N까지의 정수에 대응되는 경우, 이를 비트로 효율적으로 표현
- 객체 권한 관리 — 파일 시스템이 권한을 저장하는 방식처럼, 각 비트가 특정 권한(permission)을 나타내도록 사용
Bitmap의 가장 큰 장점은 공간 효율성이다.
일반적으로 boolean값을 저장하는 데는 8 bit가 사용된다.
하지만 위처럼 Bitmap은 데이터 저장 시 1 bit씩만 사용하기 때문에, 수많은 상태값을 매우 적은 메모리로 관리할 수 있다.
Bitmap 자료 구조의 특징은 다음과 같다.

하나의 Key에 대한 여러개의 Offset(=인덱스)가 존재하고, 각 Offset에 대한 Value가 존재한다.
여기서 Offset에는 인덱스이기 때문에 0 이상의 정수만 넣을 수 있고, Value에는 0과 1의 값만 넣을 수 있다.
Bitmap 기본 명령어
데이터 삽입
// SETBIT [KEY] [OFFSET] [VALUE]
SETBIT pings:2024-01-01-00:00 123 1
특정 Offset의 Value 조회
// GETBIT [KEY] [OFFSET]
GETBIT pings:2024-01-01-00:00 123
(integer) 1
// 존재하지 않는 offset이면 0을 반환
GETBIT pings:2024-01-01-00:00 456
(integer) 0
Value가 1로 저장된 데이터 개수 조회
// BITCOUNT [KEY]
BITCOUNT pings:2024-01-01-00:00
(integer) 1
AND/OR/XOR 연산
// 요일 별 출석한 사용자 추가
// 월요일 출석: 사용자 0번, 2번
SETBIT mon 0 1
SETBIT mon 2 1
// 화요일 출석: 사용자 1번, 2번
SETBIT tue 1 1
SETBIT tue 2 1
// BITOP [AND|OR|XOR|NOT] [결과 KEY] [KEY1] [KEY2 ...]
// 결과 KEY에 연산 결과가 저장됨
BITOP AND result_and mon tue
BITOP OR result_or mon tue
BITOP XOR result_xor mon tue
BITOP 명령어에서 결과 Key로 지정한 Key에 연산 결과가 저장된다.
해당 Key가 없으면 새로 생성되고, 이미 존재하면 기존 값을 덮어쓴다.
SET, Bitmap의 공통점 / 차이점
공통점
- 두 자료구조 모두 같은 값이 중복으로 누적되지 않는다 (Set은 멤버 중복을 거부, Bitmap은 각 비트 위치가 단일 상태만 가짐).
차이점
- Set은 문자열, 숫자, UUID등의 다양한 데이터를 그대로 저장할 수 있지만, Bitmap은 0과 1만 저장 가능
- Set은 각 member가 메모리를 차지하기 때문에 데이터 수가 늘어날 수록 메모리 사용량이 선형적으로 증가하지만, Bitmap은 데이터 저장 시 각각 1 bit씩만 사용하므로 대규모 데이터를 매우 적은 메모리로 저장할 수 있음
- Set은 데이터 목록 조회, 집합 연산 등 데이터 자체를 다룰 수 있는 명령어를 제공하지만, Bitmap은 데이터 목록을 조회하는게 불가능하고 bit로 저장되기 때문에 사람이 데이터를 읽는데 불편함이 있음
따라서 두 자료구조는 쓰임새가 다르다.
Set은 중복 없는 데이터의 집합을 저장하고, 그 데이터 자체를 조회하거나 집합 연산해야 할 때 적합하다.
반면 Bitmap은 0과 1로 표현할 수 있는 수많은 상태값을 최소한의 메모리로 기록해야 할 때 적합하다.
하루 서비스 방문자 수(DAU) 카운팅하기(RDB, Set, Bitmap)
하루 동안 방문한 사용자 수를 DAU(Daily Active Users)라고 한다.
이 DAU를 카운팅하고 조회할 때마다 RDB로 처리하게 되면 트래픽이 증가할 수록 성능 부담이 커진다.
이 문제를 Redis의 Set, Bitmap으로 성능 개선이 가능하다.
RDB로 DAU 처리
먼저 RDB를 사용했을 때의 성능을 테스트해보자.
테스트를 위해 다음과 같이 DAU를 데이터베이스에 기록, 조회하는 코드를 작성한다.
@Transactional
public void recordActiveUser(Long userId) {
LocalDate today = LocalDate.now();
// 이미 활동 기록이 있는 유저라면 저장하지 않는다.
if (!dailyActiveUserRepository.existsByUserIdAndActiveDate(userId, today)) {
// 활동 기록 저장
dailyActiveUserRepository.save(new DailyActiveUser(userId, today));
}
}
@Transactional(readOnly = true)
public long getDau(LocalDate date) {
// 특정 날짜의 DAU를 SQL문의 COUNT를 활용해 계산
return dailyActiveUserRepository.countByActiveDate(date);
}
이후 부하 테스트를 위해 1000명의 가상 유저가 10초동안 DAU를 기록, 조회하는 K6 스크립트를 작성하고 테스트 진행.

총 12,866개의 API요청에서 약 1,156 TPS가 발생한 것을 확인 할 수 있다.
Set으로 DAU 처리

하루 동안 방문한 사용자를 기록할 때, 중복된 사용자를 기록하면 안된다.
여기서 데이터 저장 시, 순서는 고려할 필요가 없다.
때문에 위와 같은 상황에서 Set 자료구조가 적합하다고 볼 수 있다.
하루 단위로 방문한 사용자를 기록하기 때문에 Key값으로 날짜를 넣고, 각 Value에는 사용자 id를 기록한다.
하루 동안 방문한 사용자 수 집계 시에는 Set에 저장된 데이터 수를 반환하면 된다.
테스트를 위해 다음과 같이 DAU를 Redis의 Set에 기록, 조회하는 코드를 작성한다.
public void recordActiveUserWithRedis(Long userId) {
LocalDate today = LocalDate.now();
String key = "dau:" + today.toString(); // dau:YYYY-MM-DD
//SADD [KEY] [VALUE]
redisTemplate.opsForSet().add(key, userId.toString());
}
public long getDauWithRedis(LocalDate date) {
String key = "dau:" + date.toString();
// SCARD [KEY]
return redisTemplate.opsForSet().size(key);
}
이후 동일한 조건으로 부하테스트 진행.

총 40,132개의 API요청에서 약 3,914TPS가 발생한 것을 확인 할 수 있다.
메모리 사용량도 한번 체크해보자.
redis-cli에서 다음과 같이 작성하면 된다.
// MEMORY USAGE [KEY]
MEMORY USAGE dau:2026-05-28
(integer) 951792
저장한 Key에 해당하는 메모리 사용량이 951,792 byte가 사용되고 있음을 확인할 수 있다.
Bitmap으로 DAU 처리

하루 동안 방문한 사용자 수를 기록하기 위해 사용자 별로 방문 여부를 기록해주는 방법이다.
방문 여부는 Bitmap에 boolean값으로 저장한다.
마찬가지로 하루 단위로 방문한 사용자를 기록하기 때문에 Key값으로 날짜를 넣고, Offset에는 사용자 id, Value에는 방문 여부를 0과 1로 저장한다.
테스트를 위해 다음과 같이 DAU를 Redis의 Bitmap에 기록, 조회하는 코드를 작성한다.
public void recordActiveUserWithRedis(Long userId) {
LocalDate today = LocalDate.now();
String key = "dau:" + today.toString(); // dau:YYYY-MM-DD
// SETBIT [KEY] [OFFSET] [VALUE]
redisTemplate.opsForValue().setBit(key, userId, true);
}
public long getDauWithRedis(LocalDate date) {
String key = "dau:" + date.toString();
// BITCOUNT [KEY]
return redisTemplate.execute(
(RedisCallback<Long>) (connection) -> connection.bitCount(key.getBytes())
);
}
이후 다시 동일한 조건으로 부하테스트 진행.

총 36,198개의 API요청에서 약 3,507TPS가 발생한 것을 확인 할 수 있다.
성능 자체는 Set을 사용했을 때 더 좋게 나왔다.
이후 메모리 사용량도 한번 체크해보자.
MEMORY USAGE dau:2026-05-28
(integer) 24632
951,792 byte를 사용하던 Set에 비해 24,632byte만 사용되고 있음을 확인할 수 있다.
마치며
이렇게 Set과 Bitmap의 특징에 따라 각각의 장단점이 존재함을 확인 할 수 있었다.
따라서 어떤 상황에 어떤 자료구조가 필요한 지 잘 파악해서 사용하면 의미있는 최적화를 이끌어낼 수 있을 것 같다.
'Redis' 카테고리의 다른 글
| Redis - Hash (0) | 2026.06.04 |
|---|---|
| Redis - Geospatial (0) | 2026.05.28 |
| Redis - Sorted Set (ZSET) (0) | 2026.05.20 |
