"이 데이터가 DB에 있는지 확인하려면 매번 쿼리를 날려야 할까요? 없을 확률이 99%인데도?"

블룸 필터란

블룸 필터(Bloom Filter) 는 원소가 집합에 속하는지 아닌지를 빠르게 판별 하는 확률적 자료구조입니다.

특이한 점은 응답이 두 가지뿐이라는 것입니다.

  • "확실히 없다" → 100% 정확
  • "있을 수도 있다" → false positive 가능

false negative는 없습니다. "없다"고 하면 진짜 없는 것입니다.

왜 필요한가

  • **DB 조회 전 필터링 **: 존재하지 않는 키에 대한 불필요한 DB 조회 방지
  • ** 캐시 침투(cache penetration) 방어 **: 존재하지 않는 데이터로 캐시를 우회하는 공격 방어
  • ** 중복 URL 검사 **: 웹 크롤러에서 이미 방문한 URL 체크
  • ** 스팸 필터 **: 스팸 이메일 주소 빠른 검사

동작 원리

구조

  • ** 비트 배열 **: 크기 m의 비트 배열 (모두 0으로 초기화)
  • ** 해시 함수 **: k개의 독립적인 해시 함수

삽입

원소 x를 추가할 때, k개의 해시 함수 결과 위치의 비트를 1로 설정합니다.

PLAINTEXT
해시 함수 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** → "확실히 없다"
PLAINTEXT
조회: "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

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 확률

PLAINTEXT
p ≈ (1 - e^(-kn/m))^k

m: 비트 배열 크기
n: 삽입된 원소 수
k: 해시 함수 수
예상 원소 수 (n)목표 FP 확률 (p)필요 비트 배열 (m)해시 함수 수 (k)
1,000,0001%9.6MB7
1,000,0000.1%14.4MB10
10,000,0001%96MB7

삭제 문제와 해결

블룸 필터는 기본적으로 **삭제를 지원하지 않습니다 **. 비트를 0으로 바꾸면 다른 원소까지 영향을 받기 때문입니다.

Counting Bloom Filter

각 위치를 비트 대신 ** 카운터 **로 저장합니다.

JAVA
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 조회를 줄일 수 있습니다.

JAVA
@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 모듈을 사용하면 분산 환경에서도 블룸 필터를 쓸 수 있습니다.

PLAINTEXT
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로 분산 환경에서도 쉽게 사용할 수 있습니다
댓글 로딩 중...