Bitmap과 Bitfield — 비트 단위 데이터 처리
수백만 명의 사용자가 오늘 접속했는지를 1비트로 기록할 수 있다면, 얼마나 많은 메모리를 절약할 수 있을까요?
개념 정의
Bitmap은 Redis String 타입을 비트 배열로 활용하는 방식입니다. 각 비트(0 또는 1)에 하나의 상태를 저장할 수 있어, 대규모 이진 상태 추적에 매우 효율적입니다. Bitfield는 비트맵을 확장하여 임의 크기의 정수를 비트 오프셋으로 읽고 쓸 수 있게 합니다.
왜 필요한가
사용자 100만 명의 오늘 출석 여부를 저장하는 방법을 비교합니다.
방법 1: Set에 출석한 사용자 ID 저장
SADD attendance:2026-03-19 "user:1" "user:2" ...
→ 50만 명 출석 시: ~32MB
방법 2: Hash에 사용자별 필드로 저장
HSET attendance:2026-03-19 user:1 1 user:2 1 ...
→ 50만 명 출석 시: ~25MB
방법 3: Bitmap (사용자 ID가 정수)
SETBIT attendance:2026-03-19 1 1
SETBIT attendance:2026-03-19 2 1
→ 100만 명 전체 비트맵: ~125KB (100만 / 8)
Bitmap은 200배 이상 메모리를 절약합니다.
Bitmap 기본 명령어
SETBIT / GETBIT
# 오프셋 위치의 비트를 1로 설정
SETBIT login:2026-03-19 1001 1 # 사용자 1001 로그인
SETBIT login:2026-03-19 1002 1 # 사용자 1002 로그인
SETBIT login:2026-03-19 1003 1 # 사용자 1003 로그인
# 특정 오프셋의 비트 조회
GETBIT login:2026-03-19 1001 # 1 (로그인함)
GETBIT login:2026-03-19 9999 # 0 (로그인 안 함)
# SETBIT 반환값: 이전 비트 값
SETBIT login:2026-03-19 1001 1 # 1 (이미 1이었음)
SETBIT login:2026-03-19 5000 1 # 0 (0이었음 → 1로 변경)
주의: 오프셋에 따른 메모리 할당
# 오프셋이 크면 중간 비트가 0으로 채워진 메모리가 할당됨
SETBIT sparse 100000000 1 # 100M번째 비트 → 약 12MB 할당
# 사용자 ID가 크고 희소한 경우 비효율적
# → 이 경우 Set이나 HyperLogLog가 나을 수 있음
BITCOUNT — 1의 개수 세기
# 전체 비트맵에서 1의 개수
BITCOUNT login:2026-03-19
# (integer) 3
# 바이트 범위 지정
BITCOUNT login:2026-03-19 0 10 # 0~10번째 바이트(0~87번째 비트)
# 비트 범위 지정 (7.0+)
BITCOUNT login:2026-03-19 0 100 BIT # 0~100번째 비트
BITPOS — 첫 번째 0 또는 1 찾기
# 첫 번째 1 비트의 위치
BITPOS login:2026-03-19 1
# (integer) 1001
# 첫 번째 0 비트의 위치
BITPOS login:2026-03-19 0
# 범위 지정
BITPOS login:2026-03-19 1 100 200 BIT # 100~200 비트 범위 (7.0+)
BITOP — 비트 연산
여러 비트맵 간의 비트 연산을 수행합니다.
# 3월 17일, 18일, 19일 로그인 기록
SETBIT login:0317 1 1
SETBIT login:0317 2 1
SETBIT login:0317 3 1
SETBIT login:0318 2 1
SETBIT login:0318 3 1
SETBIT login:0318 4 1
SETBIT login:0319 3 1
SETBIT login:0319 4 1
SETBIT login:0319 5 1
# AND: 3일 연속 로그인한 사용자
BITOP AND result:consecutive login:0317 login:0318 login:0319
BITCOUNT result:consecutive # 1 (사용자 3만 3일 연속)
# OR: 3일 중 하나라도 로그인한 사용자
BITOP OR result:any login:0317 login:0318 login:0319
BITCOUNT result:any # 5 (사용자 1,2,3,4,5)
# XOR: 두 날 중 하루만 로그인한 사용자
BITOP XOR result:xor login:0317 login:0318
# 사용자 1(17일만), 4(18일만)
# NOT: 비트 반전
BITOP NOT result:not login:0319
BITOP의 시간 복잡도
O(N) — N은 가장 긴 비트맵의 바이트 수
100만 사용자 비트맵: ~125KB → 매우 빠름
1억 사용자 비트맵: ~12MB → 약간의 시간 소요
Bitfield — 비트 단위 정수 조작
Bitfield는 비트맵 위에서 임의 크기의 정수를 읽고 쓸 수 있습니다.
기본 사용법
# u8: 부호 없는 8비트 정수, i16: 부호 있는 16비트 정수
# SET: 값 설정
# GET: 값 조회
# INCRBY: 값 증가
# 오프셋 0에 u8(부호 없는 8비트) 값 200 저장
BITFIELD mykey SET u8 0 200
# 오프셋 8에 u8 값 100 저장
BITFIELD mykey SET u8 8 100
# 조회
BITFIELD mykey GET u8 0 GET u8 8
# [200, 100]
# 원자적 증가
BITFIELD mykey INCRBY u8 0 10
# [210]
비트 레이아웃
바이트: [ 0번 바이트 ][ 1번 바이트 ][ 2번 바이트 ]
비트: 0 1 2 3 4 5 6 7 8 9 ...
|── u8 @0 ──| |── u8 @8 ──|
# 비트 단위로 정밀한 팩킹 가능
BITFIELD packed SET u4 0 15 SET u4 4 8 SET u8 8 255
# 4비트(15) + 4비트(8) + 8비트(255) = 16비트 = 2바이트
오버플로우 제어
# WRAP: 오버플로우 시 순환 (기본)
BITFIELD mykey OVERFLOW WRAP INCRBY u8 0 200
# u8 최대 255 → 200 + 200 = 400 → 400 % 256 = 144
# SAT: 오버플로우 시 최대/최소값에 고정
BITFIELD mykey OVERFLOW SAT INCRBY u8 0 300
# 255 (최대값에 고정)
# FAIL: 오버플로우 시 연산 실패 (nil 반환)
BITFIELD mykey OVERFLOW FAIL INCRBY u8 0 300
# (nil)
Bitfield 활용: 컴팩트 카운터 배열
# 24시간 시간대별 방문 카운터 (각 u16 = 16비트)
# 24개 × 16비트 = 384비트 = 48바이트에 24개 카운터 저장!
# 14시 방문 카운터 증가
BITFIELD hourly_visits INCRBY u16 #14 1
# #14는 "14번째 u16 오프셋" = 비트 오프셋 14*16 = 224
# 14시 방문 수 조회
BITFIELD hourly_visits GET u16 #14
# 모든 시간대 조회
BITFIELD hourly_visits GET u16 #0 GET u16 #1 ... GET u16 #23
실전 패턴
패턴 1: 출석 시스템
from datetime import date, timedelta
def check_in(user_id):
"""오늘 출석 체크"""
today = date.today().isoformat()
r.setbit(f"attendance:{today}", user_id, 1)
def is_checked_in(user_id, target_date=None):
"""출석 여부 확인"""
if target_date is None:
target_date = date.today().isoformat()
return r.getbit(f"attendance:{target_date}", user_id)
def get_daily_count(target_date):
"""일별 출석자 수"""
return r.bitcount(f"attendance:{target_date}")
def get_consecutive_days(user_id, days=7):
"""N일 연속 출석 확인"""
keys = [f"attendance:{(date.today() - timedelta(days=i)).isoformat()}"
for i in range(days)]
# AND 연산으로 모든 날 출석한 사용자 비트맵 생성
r.bitop('AND', 'temp:consecutive', *keys)
result = r.getbit('temp:consecutive', user_id)
r.delete('temp:consecutive')
return bool(result)
패턴 2: 활성 사용자 분석
def track_active(user_id, feature):
"""기능별 활성 사용자 추적"""
today = date.today().isoformat()
r.setbit(f"active:{feature}:{today}", user_id, 1)
def feature_overlap(feature_a, feature_b, target_date):
"""두 기능을 모두 사용한 사용자 수"""
key_a = f"active:{feature_a}:{target_date}"
key_b = f"active:{feature_b}:{target_date}"
r.bitop('AND', 'temp:overlap', key_a, key_b)
count = r.bitcount('temp:overlap')
r.delete('temp:overlap')
return count
def weekly_active_users(feature):
"""주간 활성 사용자 (7일 합집합)"""
keys = [f"active:{feature}:{(date.today() - timedelta(days=i)).isoformat()}"
for i in range(7)]
r.bitop('OR', f'wau:{feature}', *keys)
return r.bitcount(f'wau:{feature}')
패턴 3: 기능 플래그(Feature Flag)
# 32개의 기능 플래그를 u32 하나에 저장
FEATURE_DARK_MODE = 0
FEATURE_NEW_UI = 1
FEATURE_BETA = 2
def enable_feature(user_id, feature_bit):
"""기능 활성화"""
r.bitfield(f"features:{user_id}",
'SET', 'u1', f'#{feature_bit}', 1)
def is_feature_enabled(user_id, feature_bit):
"""기능 활성화 여부"""
result = r.bitfield(f"features:{user_id}",
'GET', 'u1', f'#{feature_bit}')
return bool(result[0])
패턴 4: Bitfield로 컴팩트 게임 데이터
# 게임 캐릭터 스탯을 비트필드로 팩킹
# HP(u16) + MP(u16) + ATK(u8) + DEF(u8) + LVL(u8) = 72비트 = 9바이트
def create_character(char_id, hp, mp, atk, defense, level):
key = f"char:{char_id}"
r.bitfield(key,
'SET', 'u16', '#0', hp, # HP: 0~65535
'SET', 'u16', '#1', mp, # MP: 0~65535
'SET', 'u8', '#4', atk, # ATK: 0~255 (바이트 오프셋 4)
'SET', 'u8', '#5', defense, # DEF: 0~255
'SET', 'u8', '#6', level # LVL: 0~255
)
def take_damage(char_id, damage):
"""OVERFLOW SAT으로 HP가 0 이하로 내려가지 않게"""
result = r.bitfield(f"char:{char_id}",
'OVERFLOW', 'SAT',
'INCRBY', 'u16', '#0', -damage
)
return result[0] # 남은 HP
Bitmap vs HyperLogLog vs Set
| 기준 | Bitmap | HyperLogLog | Set |
|---|---|---|---|
| 멤버십 확인 | O(1) | 불가 | O(1) |
| 카디널리티 | BITCOUNT O(N) | PFCOUNT O(1) | SCARD O(1) |
| 교집합/합집합 | BITOP | PFMERGE(합집합만) | SINTER/SUNION |
| 메모리 (ID 1~100만) | 125KB | 12KB | ~32MB |
| ID 요구사항 | 정수, 연속적 | 없음 | 없음 |
| 정확도 | 100% | ±0.81% | 100% |
정리
Bitmap과 Bitfield는 비트 단위 데이터 처리에 최적화된 도구입니다.
- Bitmap은 내부적으로 String 타입이며, SETBIT/GETBIT으로 비트 단위 접근을 제공합니다
- BITOP으로 여러 비트맵 간 AND/OR/XOR/NOT 연산이 가능하여 교집합/합집합 분석에 유용합니다
- BITCOUNT는 CPU의 POPCNT 명령어를 활용하여 매우 빠르게 1 비트를 셉니다
- Bitfield는 비트맵 위에 임의 크기의 정수를 읽고 쓸 수 있어, 컴팩트 카운터 배열에 적합합니다
- 사용자 ID가 정수이고 연속적일 때 가장 효율적이며, 희소한 ID에서는 메모리 낭비가 발생합니다
- 출석 시스템, 활성 사용자 분석, 기능 플래그 등에 널리 활용됩니다
댓글 로딩 중...