블룸 필터
블룸 필터 (Bloom filter)
블룸 필터는 Redis가 제공하는 확률적 데이터 구조 중 하나예요. 아이템이 집합(set) 안에 존재하는지, 아주 작은 고정 크기의 메모리만으로 확인할 수 있게 해주죠. 이번 페이지에서는 블룸 필터가 왜 이렇게 메모리를 아끼면서 빠른지, 실제로 어떻게 쓰는지, 그리고 어떤 한계가 있는지를 하나씩 살펴볼게요.
블룸 필터는 어떻게 동작하나요?
Redis Open Source의 블룸 필터(probabilistic data structure)를 쓰면, 아이템이 집합 안에 존재하는지 아주 작고 고정된 크기의 메모리 공간으로 확인할 수 있어요.
집합의 모든 아이템을 통째로 저장하는 대신, 블룸 필터는 아이템의 해시(Hash)된 표현만 저장해요. 덕분에 정확도(precision)를 조금 희생하는 대신, 매우 공간 효율적이고 빨라지는 거죠.
여기서 중요한 특징이 있어요.
- 블룸 필터는 "아이템이 집합에 없다"는 것을 보장할 수 있어요.
- 하지만 "아이템이 집합에 있다"는 것은 **추정(estimation)**만 가능해요.
즉, "없다(negative)"라고 답하면 그건 100% 확실하지만, N번의 "있다(positive)" 응답 중 1번 정도는 틀릴 수 있어요. 처음에는 좀 이상해 보일 수 있는데, 이런 형태의 불확실성은 컴퓨터 과학에서 충분히 유용하게 쓰여요.
- 높은 비용의 작업을 막아주는 "없다"라는 답: 예를 들어 이미 사용 중인 사용자 이름인지, 신고된 도난 신용카드인지, 사용자가 이미 본 광고인지 등을 확인할 때요.
사용 사례 (Use cases)
금융 사기 탐지 (Financial fraud detection, finance)
"이 사용자가 이전에 이 위치에서 결제한 적이 있나?" 같은 질문에 답하며, 사용자의 비정상적인 쇼핑 패턴을 확인하는 용도로 써요.
- 사용자마다 블룸 필터 하나를 두고, 모든 거래마다 확인해요.
- 매우 빠른 응답(로컬 레이턴시)을 제공해요.
- 사용자가 이동했을 때를 대비해 여러 리전에 복제할 수 있어요.
- 규모가 커져도 성능 저하를 막아줘요.
이런 애플리케이션에 Redis 블룸 필터를 쓰면 다음과 같은 이점이 있어요.
- 거래 완료가 빨라져요.
- 네트워크 파티션이 발생해도 거래가 끊길 가능성이 줄어들어요(연결 유지 시간이 짧아지니까요).
- 카드 소유자와 판매자 양쪽에 보안 레이어가 하나 더 생겨요.
금융 업계에서 블룸 필터로 답할 수 있는 다른 질문들도 있어요.
- 사용자가 이 카테고리의 제품/서비스를 구매한 적이 있나요?
- 검증된 온라인 샵(Amazon, Apple app store 같은 대형 리테일러)에서 구매할 때 보안 단계를 일부 생략해도 되나요?
- 이 신용카드가 분실/도난 신고된 적이 있나요? 특히 이 경우 블룸 필터를 쓰면, 금융 기관들이 카드 번호 자체를 공개하지 않고도 도난/차단된 카드 번호 목록을 교환할 수 있다는 추가 이점이 있어요.
광고 배치 (Ad placement, retail, advertising)
다음 질문에 답하는 용도로 써요.
- 사용자가 이미 이 광고를 봤나요?
- 사용자가 이미 이 제품을 샀나요?
사용자마다 블룸 필터를 하나씩 두고, 구매한 모든 제품을 저장해요. 추천 엔진이 새 제품을 추천하면, 그 제품이 사용자의 블룸 필터에 있는지 확인하죠.
- 없다면 → 광고를 보여주고 블룸 필터에 추가해요.
- 있다면 → 없는 제품을 찾을 때까지 과정을 반복해요.
이런 애플리케이션에 Redis 블룸 필터를 쓰면:
- 거의 실시간에 가까운 맞춤 경험을 비용 효율적으로 제공할 수 있어요.
- 비싼 인프라에 투자할 필요가 없어요.
사용자 이름 중복 확인 (SaaS, 콘텐츠 게시 플랫폼)
"이 사용자 이름/이메일/도메인 이름/slug이 이미 사용된 적 있나요?"라는 질문에 답하는 용도예요.
- 가입한 사용자마다 블룸 필터를 하나씩 둬요.
- 새 사용자가 원하는 사용자 이름을 입력하면, 그 이름이 블룸 필터에 있는지 확인해요.
- 없다면 → 사용자를 생성하고 이름을 필터에 추가해요.
- 있다면 → 앱이 메인 데이터베이스를 확인하거나, 이름을 거부할 수 있어요.
확인 시간은 규모가 커져도 동일하게 유지돼요. 이 경우에도 매우 빠르고 효율적이며, 비싼 인프라 투자가 필요 없어요.
예제 (Example)
자전거 제조사가 백만 종류의 서로 다른 자전거를 만든다고 가정해볼게요. 새 모델에서 중복 모델명을 방지하고 싶다면 블룸 필터로 중복을 감지할 수 있어요. 아래 예제에서는 백만 개 항목을 담을 공간과 0.1% 오차율을 가진 필터를 만든 뒤, 모델명 하나를 추가하고 존재 여부를 확인해요. 그다음 여러 모델명을 추가하고 모두 존재하는지 확인해요.
블룸 필터의 주요 연산은 다음과 같아요. BF.RESERVE로 필터를 만들고, BF.ADD/BF.MADD로 아이템을 추가하며, BF.EXISTS/BF.MEXISTS로 존재 여부를 확인합니다. 공간 효율적인 확률적 집합 멤버십 검사가 필요할 때 쓰는 명령들이죠.
res1 = r.bf().reserve("bikes:models", 0.01, 1000)
print(res1) # >>> True
res2 = r.bf().add("bikes:models", "Smoky Mountain Striker")
print(res2) # >>> True
res3 = r.bf().exists("bikes:models", "Smoky Mountain Striker")
print(res3) # >>> True
res4 = r.bf().madd("bikes:models", "Rocky Mountain Racer", "Cloudy City Cruiser", "Windy City Wippet",)
print(res4) # >>> [True, True, True]
res5 = r.bf().mexists("bikes:models", "Rocky Mountain Racer", "Cloudy City Cruiser", "Windy City Wippet",)
print(res5) # >>> [True, True, True]
CLI로는 이렇게 해요.
> BF.RESERVE bikes:models 0.01 1000
OK
> BF.ADD bikes:models "Smoky Mountain Striker"
(integer) 1
> BF.EXISTS bikes:models "Smoky Mountain Striker"
(integer) 1
> BF.MADD bikes:models "Rocky Mountain Racer" "Cloudy City Cruiser" "Windy City Wippet"
1) (integer) 1
2) (integer) 1
3) (integer) 1
> BF.MEXISTS bikes:models "Rocky Mountain Racer" "Cloudy City Cruiser" "Windy City Wippet"
1) (integer) 1
2) (integer) 1
3) (integer) 1
BF.RESERVE의 인자는 key, error_rate, capacity 순서예요. 위 예제에서는 오차율 0.01(1%)과 용량 1000을 지정했어요. 각 언어별 클라이언트(redis-py, node-redis, Jedis, go-redis, NRedisStack, Predis, redis-rs 등)에서도 동일한 연산을 제공해요. 자세한 사용법은 각 언어의 Quick-Start 문서를 참고하면 됩니다.
크기 조정과 확장
1. 용량 (Capacity)
필터의 용량은 아이템 개수로 표현돼요. 필터가 가득 차면, 새 아이템을 추가해도 오류는 발생하지 않지만 오차율(error rate)이 커지기 시작해요.
2. 확장 (EXPANSION)
블룸 필터에 아이템을 추가할 때 "가득 찼다"는 이유로 실패하는 일은 없어요. 대신 오차율이 증가하기 시작하죠. 필터 초기화 때 설정한 오차율에 가깝게 유지하기 위해, 블룸 필터는 **자동 확장(auto-scale)**을 해요. 용량에 도달하면 추가 서브 필터(sub-filter)가 생성되는 거예요.
새 서브 필터의 크기는 이전 서브 필터 크기에 EXPANSION을 곱한 값이에요.
- 필터에 저장할 아이템 수를 모른다면,
EXPANSION을 2 이상으로 설정해 서브 필터 개수를 줄이는 걸 권장해요. - 반대로 메모리 사용을 줄이고 싶다면
EXPANSION을 1로 설정하는 걸 권장해요. - 기본값은
2예요.
새로운 서브 필터가 생길 때마다 원하는 오차율을 유지하기 위해 해시 함수의 개수도 계속 늘어나요.
혹시 "어차피 확장할 거라면 왜 작은 필터에 높은 확장률을 설정하죠?"라고 궁금할 수 있는데, 그 이유는 필터를 많이 유지해야 하는 경우를 위한 거예요. 사용자별 또는 제품별로 필터가 있고, 대부분은 작게 유지되지만 일부는 활동이 많아서 확장해야 하는 상황을 생각해 보면 돼요.
3. NONSCALING
확장할 필요가 없다는 걸 안다면 NONSCALING 플래그를 쓰면 돼요. 이러면 해시 함수를 하나 덜 사용하기 때문이에요. 다만 처음에 할당한 용량에 도달하면 오차율이 커지기 시작한다는 점을 기억해야 해요.
블룸 필터의 총 크기
블룸 필터가 실제로 사용하는 메모리는 선택한 오차율에 따라 결정돼요.
- 최적 해시 함수 개수는
ceil(-ln(error_rate) / ln(2))이에요. - 원하는
error_rate와 최적 해시 함수 개수로 계산한 아이템당 필요한 비트 수는-ln(error_rate) / ln(2)^2이에요. 따라서 필터의 총 필요 비트 수는capacity * -ln(error_rate) / ln(2)^2입니다.
구체적인 수치로 보면:
- 1% 오차율 → 해시 함수 7개, 아이템당 9.585 비트
- 0.1% 오차율 → 해시 함수 10개, 아이템당 14.378 비트
- 0.01% 오차율 → 해시 함수 14개, 아이템당 19.170 비트
비교를 위해, Redis set으로 멤버십 검사를 할 때 필요한 메모리는 다음과 같아요.
memory_with_sets = capacity*(192b + value)
예를 들어 IP 주소 집합이라면 아이템당 약 40바이트(320비트)가 필요해요. 이는 0.01% 오탐(false positive) 오차율의 블룸 필터가 쓰는 19.170 비트보다 훨씬 큰 값이죠.
블룸 필터 vs 쿠쿠 필터 (Bloom vs. Cuckoo filters)
- 블룸 필터는 아이템 삽입에서 일반적으로 더 나은 성능과 확장성을 보여줘요. 데이터셋에 아이템을 자주 추가한다면 블룸 필터가 이상적일 수 있어요.
- 쿠쿠 필터는 확인 연산이 더 빠르고, 삭제도 가능해요.
성능 (Performance)
- 블룸 필터에서 삽입(Insertion)은 O(K) 이에요. 여기서
k는 해시 함수 개수예요. - 아이템 확인(Checking)은 O(K) 이며, 필터가 쌓여 있는(stacked) 경우에는 O(K*n) 이에요.
n은 쌓인 필터 개수예요.
학술 자료 (Academic sources)
더 알아보기 (Learn more)
- Cuckoo filter — 블룸 필터의 대안
- Count-min sketch — 또 다른 확률적 데이터 구조
- Probabilistic data structures 개요 — 확률적 데이터 구조 전체
- Bloom filter 명령어 참조 (BF.RESERVE, BF.ADD, BF.MADD, BF.EXISTS, BF.MEXISTS 등)