Theme:

"이 값이 존재하는지" 같은 단순한 Yes/No 질문에 해시맵을 쓰는 것은 낭비가 아닐까요?

비트셋이란

비트셋(BitSet)은 각 비트 하나가 하나의 Boolean 값을 표현하는 자료구조입니다.

  • boolean 배열: 요소당 1바이트(8비트) 사용
  • BitSet: 요소당 1비트 사용
  • 대량의 Boolean 데이터를 다룰 때 메모리를 약 8배 절약할 수 있습니다
PLAINTEXT
boolean 배열: [00000001] [00000000] [00000001] [00000000]  → 4바이트, 4개 값
BitSet:       [1 0 1 0]                                     → 1바이트 미만, 4개 값

왜 필요한가

다음과 같은 상황에서 비트셋이 유용합니다.

  • 1억 개의 정수 중 어떤 수가 존재하는지 확인 → HashSet은 수백 MB, BitSet은 약 12MB
  • ** 대규모 소수 판별** (에라토스테네스의 체) → 수억 개의 플래그 저장
  • ** 블룸 필터** → 확률적 존재 여부 확인
  • ** 권한 시스템** → 비트 플래그로 권한 조합 표현

Java BitSet 기본 사용

JAVA
import java.util.BitSet;

BitSet bitSet = new BitSet(100); // 100비트 크기

// 비트 설정
bitSet.set(0);      // 0번 비트를 1로
bitSet.set(5);      // 5번 비트를 1로
bitSet.set(10, 20); // 10~19번 비트를 1로

// 비트 확인
boolean exists = bitSet.get(5);   // true
boolean absent = bitSet.get(3);   // false

// 비트 해제
bitSet.clear(5);    // 5번 비트를 0으로

// 설정된 비트 수
int count = bitSet.cardinality(); // 1로 설정된 비트 개수

// 반복
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
    System.out.println("비트 " + i + "이 설정됨");
}

내부 구조

Java BitSetlong 배열 로 비트를 저장합니다.

JAVA
// BitSet 내부 (간략화)
public class BitSet {
    long[] words; // 64비트씩 묶어서 저장

    public void set(int bitIndex) {
        int wordIndex = bitIndex >> 6;        // bitIndex / 64
        words[wordIndex] |= (1L << bitIndex); // 해당 비트를 1로 설정
    }

    public boolean get(int bitIndex) {
        int wordIndex = bitIndex >> 6;
        return (words[wordIndex] & (1L << bitIndex)) != 0;
    }
}
  • 인덱스 5를 설정하면: words[0]의 5번째 비트가 1이 됩니다
  • 인덱스 70을 설정하면: words[1]의 6번째 비트가 1이 됩니다 (70 = 64 + 6)

비트 연산

BitSet은 집합 연산을 비트 연산 으로 처리합니다. CPU가 64비트를 한 번에 처리하므로 매우 빠릅니다.

JAVA
BitSet a = new BitSet();
a.set(1); a.set(2); a.set(3);

BitSet b = new BitSet();
b.set(2); b.set(3); b.set(4);

// 합집합 (OR)
BitSet union = (BitSet) a.clone();
union.or(b);  // {1, 2, 3, 4}

// 교집합 (AND)
BitSet intersection = (BitSet) a.clone();
intersection.and(b);  // {2, 3}

// 차집합
BitSet diff = (BitSet) a.clone();
diff.andNot(b);  // {1}

// 대칭 차집합 (XOR)
BitSet symDiff = (BitSet) a.clone();
symDiff.xor(b);  // {1, 4}

이런 연산은 long 배열의 각 원소에 비트 연산을 적용하므로, 64개의 요소를 단일 CPU 명령어 로 처리합니다.

에라토스테네스의 체 최적화

소수를 구하는 에라토스테네스의 체를 BitSet으로 최적화할 수 있습니다.

boolean 배열 버전

JAVA
// boolean 배열: n이 1억이면 약 100MB
boolean[] isComposite = new boolean[n + 1];
for (int i = 2; i * i <= n; i++) {
    if (!isComposite[i]) {
        for (int j = i * i; j <= n; j += i) {
            isComposite[j] = true;
        }
    }
}

BitSet 버전

JAVA
// BitSet: n이 1억이면 약 12.5MB
public List<Integer> sieve(int n) {
    BitSet composite = new BitSet(n + 1);
    List<Integer> primes = new ArrayList<>();

    for (int i = 2; i <= n; i++) {
        if (!composite.get(i)) {
            primes.add(i);
            // i가 int 범위를 넘지 않도록 long으로 계산
            for (long j = (long) i * i; j <= n; j += i) {
                composite.set((int) j);
            }
        }
    }
    return primes;
}

메모리 사용량이 8배 줄어들면서, 캐시 효율도 좋아져 실행 속도까지 빨라집니다.

블룸 필터

블룸 필터(Bloom Filter)는 BitSet을 기반으로 한 확률적 자료구조 입니다.

원리

  1. m 비트의 BitSet과 k개의 해시 함수를 준비합니다.
  2. 요소를 추가할 때: k개의 해시 함수로 k개의 위치를 계산하고, 해당 비트를 1로 설정합니다.
  3. 요소를 조회할 때: k개의 위치가 모두 1이면 "있을 수 있음", 하나라도 0이면 "확실히 없음"입니다.
JAVA
public class SimpleBloomFilter {
    private BitSet bitSet;
    private int size;
    private int hashCount;

    public SimpleBloomFilter(int size, int hashCount) {
        this.bitSet = new BitSet(size);
        this.size = size;
        this.hashCount = hashCount;
    }

    // 해시 함수 (더블 해싱)
    private int hash(String value, int i) {
        int hash1 = value.hashCode();
        int hash2 = hash1 >>> 16; // 상위 비트를 활용
        return Math.abs((hash1 + i * hash2) % size);
    }

    public void add(String value) {
        for (int i = 0; i < hashCount; i++) {
            bitSet.set(hash(value, i));
        }
    }

    public boolean mightContain(String value) {
        for (int i = 0; i < hashCount; i++) {
            if (!bitSet.get(hash(value, i))) {
                return false; // 확실히 없음
            }
        }
        return true; // 있을 수도 있음 (False Positive 가능)
    }
}

특성

  • **False Negative 없음 **: "없다"고 하면 진짜 없습니다
  • **False Positive 있음 **: "있다"고 해도 실제로는 없을 수 있습니다
  • ** 삭제 불가능 **: 비트를 0으로 바꾸면 다른 요소에 영향을 줄 수 있습니다

실무 활용

  • Redis: SETBIT, GETBIT 명령어로 블룸 필터 구현 가능
  • Cassandra: SSTable에서 읽기 전 블룸 필터로 불필요한 디스크 I/O 방지
  • Google Chrome: 악성 URL 사전 필터링
  • ** 캐시 전략 **: DB 쿼리 전에 데이터 존재 여부를 빠르게 판단
JAVA
// 캐시 관통(Cache Penetration) 방지 예시
@Service
public class ProductService {
    private final SimpleBloomFilter existFilter;

    public Product getProduct(Long id) {
        // 1. 블룸 필터로 빠르게 확인
        if (!existFilter.mightContain(id.toString())) {
            return null; // 확실히 없는 데이터 → DB 조회 불필요
        }

        // 2. 캐시 확인
        Product cached = cache.get(id);
        if (cached != null) return cached;

        // 3. DB 조회
        return productRepository.findById(id).orElse(null);
    }
}

비트 플래그 패턴

비트셋의 원리를 작은 규모로 적용한 것이 비트 플래그입니다.

JAVA
// 권한 시스템 — 비트 플래그
public class Permission {
    public static final int READ    = 1;       // 0001
    public static final int WRITE   = 1 << 1;  // 0010
    public static final int EXECUTE = 1 << 2;  // 0100
    public static final int ADMIN   = 1 << 3;  // 1000

    private int flags;

    public void grant(int permission) {
        flags |= permission;  // OR로 권한 추가
    }

    public void revoke(int permission) {
        flags &= ~permission; // AND NOT으로 권한 제거
    }

    public boolean has(int permission) {
        return (flags & permission) == permission;
    }
}

// 사용 예시
Permission perm = new Permission();
perm.grant(Permission.READ | Permission.WRITE); // 읽기 + 쓰기 권한
perm.has(Permission.READ);    // true
perm.has(Permission.ADMIN);   // false

리눅스 파일 권한(chmod 755)이 바로 이 패턴입니다. 7 = 111(이진) = 읽기 + 쓰기 + 실행.

Java EnumSet — 타입 안전한 비트셋

Java의 EnumSet은 내부적으로 비트셋으로 구현되어 있습니다.

JAVA
enum Permission { READ, WRITE, EXECUTE, ADMIN }

// 내부적으로 long 하나의 비트를 사용 (enum 원소가 64개 이하일 때)
EnumSet<Permission> permissions = EnumSet.of(Permission.READ, Permission.WRITE);

permissions.add(Permission.EXECUTE);
permissions.contains(Permission.ADMIN); // false

// 합집합, 교집합 등 집합 연산도 가능
EnumSet<Permission> all = EnumSet.allOf(Permission.class);

EnumSet은 비트 연산의 성능을 유지하면서 타입 안전성을 보장하므로, enum 기반 플래그에는 EnumSet을 사용하는 것이 좋습니다.

정리

  • BitSet은 ** 비트 하나로 Boolean 값 하나를 표현 **하여 메모리를 8배 절약합니다.
  • 내부적으로 long 배열 을 사용하며, 64개의 값을 단일 CPU 명령어로 처리합니다.
  • 에라토스테네스의 체 를 BitSet으로 최적화하면 메모리와 속도 모두 개선됩니다.
  • 블룸 필터 는 BitSet 기반의 확률적 자료구조로, 캐시 관통 방지 등에 활용됩니다.
  • Java에서는 BitSet, EnumSet, 비트 플래그 등으로 비트 연산을 활용할 수 있습니다.
댓글 로딩 중...