블룸 필터 — 있을 수도 있다는 대답의 가치
"이 데이터가 DB에 있는지 확인하려면 매번 쿼리를 날려야 할까요? 없을 확률이 99%인데도?"
블룸 필터란
블룸 필터(Bloom Filter) 는 원소가 집합에 속하는지 아닌지를 빠르게 판별 하는 확률적 자료구조입니다.
특이한 점은 응답이 두 가지뿐이라는 것입니다.
- "확실히 없다" → 100% 정확
- "있을 수도 있다" → false positive 가능
false negative는 없습니다. "없다"고 하면 진짜 없는 것입니다.
왜 필요한가
- **DB 조회 전 필터링 **: 존재하지 않는 키에 대한 불필요한 DB 조회 방지
- ** 캐시 침투(cache penetration) 방어 **: 존재하지 않는 데이터로 캐시를 우회하는 공격 방어
- ** 중복 URL 검사 **: 웹 크롤러에서 이미 방문한 URL 체크
- ** 스팸 필터 **: 스팸 이메일 주소 빠른 검사
동작 원리
구조
- ** 비트 배열 **: 크기 m의 비트 배열 (모두 0으로 초기화)
- ** 해시 함수 **: k개의 독립적인 해시 함수
삽입
원소 x를 추가할 때, k개의 해시 함수 결과 위치의 비트를 1로 설정합니다.
해시 함수 3개, 비트 배열 크기 10
추가: "apple"
h1("apple") = 2, h2("apple") = 5, h3("apple") = 8
비트 배열: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
추가: "banana"
h1("banana") = 1, h2("banana") = 5, h3("banana") = 7
비트 배열: [0, 1, 1, 0, 0, 1, 0, 1, 1, 0]
조회
원소 y를 검사할 때, k개의 해시 결과 위치의 비트를 확인합니다.
- ** 모두 1** → "있을 수도 있다" (다른 원소에 의해 설정되었을 수도 있음)
- ** 하나라도 0** → "확실히 없다"
조회: "cherry"
h1("cherry") = 2, h2("cherry") = 5, h3("cherry") = 3
비트 배열[3] = 0 → "확실히 없다" ✓
조회: "grape"
h1("grape") = 1, h2("grape") = 5, h3("grape") = 8
모두 1 → "있을 수도 있다" (실제로는 없지만 false positive!)
코드 예제 — Java
public class BloomFilter {
private BitSet bitSet;
private int size; // 비트 배열 크기
private int hashCount; // 해시 함수 개수
public BloomFilter(int expectedElements, double falsePositiveRate) {
// 최적 비트 배열 크기: m = -(n × ln(p)) / (ln2)²
this.size = optimalSize(expectedElements, falsePositiveRate);
// 최적 해시 함수 개수: k = (m/n) × ln2
this.hashCount = optimalHashCount(size, expectedElements);
this.bitSet = new BitSet(size);
}
public void add(String element) {
for (int i = 0; i < hashCount; i++) {
int hash = hash(element, i);
bitSet.set(hash);
}
}
public boolean mightContain(String element) {
for (int i = 0; i < hashCount; i++) {
int hash = hash(element, i);
if (!bitSet.get(hash)) {
return false; // 확실히 없다
}
}
return true; // 있을 수도 있다
}
// 이중 해싱으로 k개의 해시 함수 생성
private int hash(String element, int i) {
int hash1 = element.hashCode();
int hash2 = hash1 >>> 16; // 상위 비트 활용
long combined = (hash1 & 0xFFFFFFFFL) + (long) i * (hash2 & 0xFFFFFFFFL);
return (int) (Math.abs(combined) % size);
}
private static int optimalSize(int n, double p) {
return (int) Math.ceil(-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
private static int optimalHashCount(int m, int n) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}
False Positive 확률
p ≈ (1 - e^(-kn/m))^k
m: 비트 배열 크기
n: 삽입된 원소 수
k: 해시 함수 수
| 예상 원소 수 (n) | 목표 FP 확률 (p) | 필요 비트 배열 (m) | 해시 함수 수 (k) |
|---|---|---|---|
| 1,000,000 | 1% | 9.6MB | 7 |
| 1,000,000 | 0.1% | 14.4MB | 10 |
| 10,000,000 | 1% | 96MB | 7 |
삭제 문제와 해결
블룸 필터는 기본적으로 **삭제를 지원하지 않습니다 **. 비트를 0으로 바꾸면 다른 원소까지 영향을 받기 때문입니다.
Counting Bloom Filter
각 위치를 비트 대신 ** 카운터 **로 저장합니다.
public class CountingBloomFilter {
private int[] counters;
private int size;
private int hashCount;
public void add(String element) {
for (int i = 0; i < hashCount; i++) {
counters[hash(element, i)]++;
}
}
public void remove(String element) {
// 먼저 존재 여부 확인
if (!mightContain(element)) return;
for (int i = 0; i < hashCount; i++) {
int idx = hash(element, i);
if (counters[idx] > 0) counters[idx]--;
}
}
public boolean mightContain(String element) {
for (int i = 0; i < hashCount; i++) {
if (counters[hash(element, i)] == 0) return false;
}
return true;
}
}
메모리는 약 4배 증가하지만 (비트 → 4바이트 카운터), 삭제가 가능해집니다.
백엔드/실무 연결
DB 조회 최적화
사용자 요청이 올 때, 블룸 필터로 먼저 확인하면 불필요한 DB 조회를 줄일 수 있습니다.
@Service
public class UserService {
private final BloomFilter bloomFilter;
private final UserRepository repository;
public User findByEmail(String email) {
// 블룸 필터로 빠른 판별
if (!bloomFilter.mightContain(email)) {
return null; // 확실히 없음 — DB 조회 스킵
}
// "있을 수도 있다" → DB에서 확인
return repository.findByEmail(email).orElse(null);
}
}
Redis Bloom Filter
Redis에서 RedisBloom 모듈을 사용하면 분산 환경에서도 블룸 필터를 쓸 수 있습니다.
BF.RESERVE myfilter 0.01 1000000 # FP 1%, 100만 원소
BF.ADD myfilter "user@example.com"
BF.EXISTS myfilter "user@example.com" # → 1 (있을 수도 있다)
BF.EXISTS myfilter "unknown@test.com" # → 0 (확실히 없다)
캐시 침투 방어
공격자가 존재하지 않는 키로 반복 요청하면 캐시를 우회하여 DB에 직접 부하를 줍니다. 블룸 필터로 "확실히 없는" 키를 빠르게 거부할 수 있습니다.
HBase / Cassandra
Google의 BigTable 논문에서 블룸 필터가 활용됩니다. HBase와 Cassandra는 SSTable에 블룸 필터를 저장하여, 특정 행이 해당 SSTable에 없는지 빠르게 판별합니다. 불필요한 디스크 I/O를 크게 줄여줍니다.
Chrome의 악성 URL 검사
Chrome 브라우저는 악성 URL 데이터베이스를 블룸 필터로 로컬에 저장합니다. URL 입력 시 로컬에서 먼저 확인하고, "있을 수도 있다"면 서버에 정밀 확인을 요청합니다.
주의할 점
블룸 필터의 "있다"는 거짓 긍정(false positive)이 있다
블룸 필터는 "확실히 없다"는 보장하지만, "있다"는 오류 가능성이 있습니다. 따라서 블룸 필터 통과 후 반드시 실제 데이터를 확인하는 2단계 검증이 필요합니다.
원소 삭제가 불가능
기본 블룸 필터에서는 원소를 삭제할 수 없습니다. 비트를 0으로 바꾸면 다른 원소의 존재 여부까지 영향받기 때문입니다. 삭제가 필요하면 Counting Bloom Filter를 사용해야 합니다.
정리
- ** 블룸 필터 **는 "확실히 없다" 또는 "있을 수도 있다"를 O(k) 시간에 판별합니다
- false positive 는 있지만 false negative 는 없습니다
- 비트 배열 크기와 해시 함수 수를 조절하여 false positive 확률을 제어합니다
- 삭제 불가 가 기본이지만, Counting Bloom Filter로 해결할 수 있습니다
- DB 조회 최적화, 캐시 침투 방어, HBase/Cassandra의 SSTable 등 실무에서 폭넓게 활용됩니다
- Redis Bloom Filter로 분산 환경에서도 쉽게 사용할 수 있습니다