Theme:

"오늘 사이트 방문자가 몇 명이었나요?"라는 질문에 정확한 답을 구하려면 수백 MB의 메모리가 필요합니다. 그런데 12KB만으로도 꽤 정확한 답을 얻을 수 있다면?

확률적 자료구조란

확률적 자료구조(Probabilistic Data Structure)는 정확성을 약간 포기하는 대신, 메모리와 시간을 대폭 절약 하는 자료구조입니다.

문제정확한 방법확률적 방법
고유 개수 세기HashSet → O(n) 메모리HyperLogLog → 12KB
존재 여부 확인HashSet → O(n) 메모리블룸 필터 → ** 수 KB**
빈도 추정HashMap → O(n) 메모리Count-Min Sketch → ** 수 KB**

HyperLogLog — 고유 개수 추정

문제

"오늘 우리 사이트의 고유 방문자(UV)가 몇 명인가?"

정확하게 세려면 모든 방문자 ID를 HashSet에 저장해야 합니다. 방문자가 1억 명이면 수백 MB가 필요합니다.

HyperLogLog(HLL)는 약 12KB 의 메모리로 이를 0.81% 오차 이내로 추정합니다.

핵심 아이디어

동전을 던져서 연속으로 앞면이 나오는 최대 횟수를 기록한다고 가정합니다.

  • 연속 3번 앞면이 나왔다면 → 아마 8번 정도(2^3) 던졌을 것
  • 연속 10번 앞면이 나왔다면 → 아마 1024번 정도(2^10) 던졌을 것

HyperLogLog도 비슷합니다. 해시 값의 이진 표현에서 ** 선행 0의 최대 개수 **를 추적합니다.

동작 원리

PLAINTEXT
1. 각 요소를 해시합니다
   hash("Alice") = 0001 0110 1011 ...
                    ^^^^ 선행 0이 3개 → 최대 3

   hash("Bob")   = 0000 0010 1101 ...
                    ^^^^^^ 선행 0이 5개 → 최대 5로 갱신

2. 선행 0의 최대값 R이면, 추정 카디널리티 ≈ 2^R
   R = 5 → 약 32개의 고유 값

3. 정확도를 높이기 위해 여러 버킷(레지스터)으로 나눠서 추정
   해시의 앞쪽 비트로 버킷 결정, 나머지로 선행 0 계산
   각 버킷의 추정치를 조화 평균으로 합산

간략 구현

JAVA
public class SimpleHyperLogLog {
    private static final int BUCKET_COUNT = 16384; // 2^14 = 16384 버킷
    private int[] registers;

    public SimpleHyperLogLog() {
        registers = new int[BUCKET_COUNT];
    }

    public void add(String element) {
        long hash = murmurhash(element);

        // 상위 14비트로 버킷 결정
        int bucket = (int) (hash >>> 50); // 상위 14비트

        // 나머지 50비트에서 선행 0 개수 + 1
        long remaining = hash & ((1L << 50) - 1);
        int leadingZeros = Long.numberOfLeadingZeros(remaining) - 14 + 1;

        // 최댓값 갱신
        registers[bucket] = Math.max(registers[bucket], leadingZeros);
    }

    public long estimate() {
        // 조화 평균으로 추정
        double harmonicMean = 0;
        int zeroCount = 0;

        for (int reg : registers) {
            harmonicMean += Math.pow(2, -reg);
            if (reg == 0) zeroCount++;
        }

        double alpha = 0.7213 / (1 + 1.079 / BUCKET_COUNT); // 보정 상수
        double estimate = alpha * BUCKET_COUNT * BUCKET_COUNT / harmonicMean;

        // 작은 값 보정
        if (estimate <= 2.5 * BUCKET_COUNT && zeroCount > 0) {
            estimate = BUCKET_COUNT * Math.log((double) BUCKET_COUNT / zeroCount);
        }

        return Math.round(estimate);
    }
}

Redis에서의 HyperLogLog

BASH
# 요소 추가: O(1)
PFADD visitors "user:1001"
PFADD visitors "user:1002"
PFADD visitors "user:1001"  # 중복 — 카운트에 영향 없음

# 고유 개수 추정: O(1)
PFCOUNT visitors  # 2 (추정치)

# 여러 HLL 병합: 일별 UV를 주별 UV로 합산
PFMERGE weekly:visitors daily:mon daily:tue daily:wed ...
PFCOUNT weekly:visitors  # 주간 고유 방문자 추정

# 메모리: 최대 12KB (정확한 카운팅은 수백 MB 필요)

실무 활용

JAVA
@Service
public class UniqueVisitorService {
    private final StringRedisTemplate redis;

    // 방문자 기록
    public void recordVisit(String userId) {
        String key = "uv:" + LocalDate.now();
        redis.opsForHyperLogLog().add(key, userId);
    }

    // 오늘의 UV 추정
    public long getTodayUV() {
        String key = "uv:" + LocalDate.now();
        return redis.opsForHyperLogLog().size(key);
    }

    // 이번 주 UV 추정
    public long getWeeklyUV() {
        String[] keys = IntStream.range(0, 7)
            .mapToObj(i -> "uv:" + LocalDate.now().minusDays(i))
            .toArray(String[]::new);

        String mergedKey = "uv:weekly:" + LocalDate.now();
        redis.opsForHyperLogLog().union(mergedKey, keys);
        return redis.opsForHyperLogLog().size(mergedKey);
    }
}

Count-Min Sketch — 빈도 추정

문제

"이 URL이 오늘 몇 번 요청되었는가?"

정확하게 세려면 HashMap<URL, Integer>가 필요합니다. URL이 수백만 종류면 메모리가 많이 필요합니다.

Count-Min Sketch는 고정 크기 메모리 로 빈도를 추정합니다.

구조

d개의 해시 함수와 w개의 카운터로 구성된 2차원 배열 입니다.

PLAINTEXT
Hash 1: [  2  |  0  |  3  |  0  |  1  |  0  |  0  |  4  ]
Hash 2: [  1  |  3  |  0  |  0  |  2  |  0  |  1  |  0  ]
Hash 3: [  0  |  0  |  2  |  1  |  0  |  3  |  0  |  0  ]

동작

JAVA
public class CountMinSketch {
    private int[][] table;
    private int width;
    private int depth;
    private int[] hashSeeds;

    public CountMinSketch(int width, int depth) {
        this.width = width;
        this.depth = depth;
        this.table = new int[depth][width];
        this.hashSeeds = new int[depth];

        Random random = new Random(42);
        for (int i = 0; i < depth; i++) {
            hashSeeds[i] = random.nextInt();
        }
    }

    // 빈도 증가: O(d)
    public void increment(String item) {
        for (int i = 0; i < depth; i++) {
            int index = hash(item, i) % width;
            table[i][index]++;
        }
    }

    // 빈도 추정: O(d) — 모든 행의 최솟값을 반환
    public int estimate(String item) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < depth; i++) {
            int index = hash(item, i) % width;
            min = Math.min(min, table[i][index]);
        }
        return min;
    }

    private int hash(String item, int i) {
        return Math.abs((item.hashCode() ^ hashSeeds[i]) * 31 + hashSeeds[i]);
    }
}

특성

  • **과대평가는 가능 **, 과소평가는 불가능
  • 해시 충돌로 인해 실제보다 큰 값이 나올 수 있음
  • 오차 한계: ε(epsilon)과 δ(delta)로 조절
    • width = ⌈e/ε⌉ (오차 범위)
    • depth = ⌈ln(1/δ)⌉ (신뢰도)

실무 활용: 인기 URL 추적

JAVA
@Component
public class PopularUrlTracker {
    private final CountMinSketch sketch;
    private final PriorityQueue<Map.Entry<String, Integer>> topK;

    public PopularUrlTracker() {
        this.sketch = new CountMinSketch(10000, 5); // w=10000, d=5
        this.topK = new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue));
    }

    public void recordAccess(String url) {
        sketch.increment(url);
    }

    public int getEstimatedCount(String url) {
        return sketch.estimate(url);
    }
}

확률적 자료구조 비교

자료구조용도오차 유형메모리
** 블룸 필터**존재 여부False Positive수 KB
HyperLogLog고유 개수±0.81%12 KB
Count-Min Sketch빈도 추정과대평가수 KB
Min-Hash집합 유사도확률적 추정가변

언제 확률적 자료구조를 사용하는가

사용이 적합한 경우

  • 데이터가 매우 크고, 정확한 답이 필요 없을 때
  • "대략 얼마나?"가 "정확히 얼마"보다 중요한 상황
  • 메모리 제약이 있는 스트리밍 환경
  • 실시간 통계/모니터링

사용이 부적합한 경우

  • 정확한 값이 반드시 필요한 경우 (금융 거래, 인증 등)
  • 데이터 크기가 작아서 정확한 방법의 비용이 수용 가능한 경우

정리

  • HyperLogLog 는 약 12KB로 수십억 개의 고유 값을 0.81% 오차로 추정합니다. Redis의 PFADD/PFCOUNT로 쉽게 사용할 수 있습니다.
  • Count-Min Sketch 는 고정 메모리로 빈도를 추정하며, 과대평가는 가능하지만 과소평가는 하지 않습니다.
  • 확률적 자료구조의 핵심은 정확성을 약간 포기하고 메모리와 시간을 대폭 절약 하는 트레이드오프입니다.
  • 실무에서는 UV 측정, 인기 콘텐츠 추적, 네트워크 모니터링 등 대규모 스트리밍 통계 에 활용됩니다.
  • 정확한 값이 필요한 곳에는 사용하면 안 됩니다. 어디까지나 "대략적인 추정"입니다.
댓글 로딩 중...