포스트

해시 테이블 — 평균 O(1)의 비밀과 그 평균이 깨지는 순간

해시 테이블의 충돌 해결(체이닝·오픈 어드레싱), 로드 팩터와 리해싱, 평균 O(1)이 최악 O(n)으로 무너지는 HashDoS까지. DynamoDB 파티셔닝과 함께 이해합니다.

해시 테이블 — 평균 O(1)의 비밀과 그 평균이 깨지는 순간

키를 주면 곧장 닿는다

배열은 정수 인덱스로 O(1)에 닿았습니다. 그런데 키가 "user:1024" 같은 문자열이면? 해시 테이블의 발상은 단순합니다 — 키를 함수에 넣어 정수 인덱스로 바꾸고, 그 자리에 값을 둔다. 조회도 같은 함수로 자리를 계산해 곧장 닿습니다. 이게 평균 O(1) 조회의 정체입니다.

이 단순한 아이디어가 가장 많이 쓰이는 자료구조(HashMap, dict, Redis, DB 인덱스의 한 축)이면서, 동시에 평균이 깨지는 순간(충돌·HashDoS·리해싱 멈춤)을 모르면 프로덕션에서 발등을 찍습니다.

해시 함수와 버킷

해시 함수 h(key)는 임의의 키를 정수로 흩뿌리고, h(key) % capacity로 버킷(bucket) 인덱스를 정합니다. 좋은 해시 함수의 조건은 균등 분포(키가 골고루 흩어짐)와 빠른 계산입니다. 아래는 키들이 해시되어 각자의 버킷으로 날아가 꽂히고, 같은 자리에 부딪히면(충돌) 체인에 매달리는 모습입니다.

h(key) % capacity 키들 0 1 2 3 A B C A·C가 버킷1 충돌 → 체인 연결

A와 C가 같은 버킷 1로 갔습니다 — 충돌(collision). 버킷 수는 유한하고 키는 무한하니 충돌은 반드시 생깁니다(비둘기집 원리). 관건은 “충돌을 없애기”가 아니라 “충돌을 싸게 처리하기“입니다.

충돌 해결 — 체이닝 vs 오픈 어드레싱

체이닝(separate chaining): 각 버킷이 연결 리스트(또는 트리)를 들고, 충돌한 키들을 거기 매답니다. Java HashMap이 이 방식이고, 한 버킷의 체인이 너무 길어지면(기본 8개) 레드블랙 트리로 전환해 최악을 O(n)에서 O(log n)으로 막습니다.

오픈 어드레싱(open addressing): 체인 없이, 충돌하면 다음 빈 버킷을 탐사(probing) 해 거기 둡니다(선형 탐사·이차 탐사·이중 해싱). 포인터가 없어 캐시 친화적이지만, 삭제가 까다롭고 로드팩터에 민감합니다(Python dict가 오픈 어드레싱 계열).

항목체이닝오픈 어드레싱
충돌 처리버킷별 리스트/트리다른 버킷 탐사
캐시 지역성낮음(포인터)높음(연속 배열)
로드팩터 한계1 넘어도 동작1 미만 필수(보통 ≤0.7)
삭제단순tombstone 필요
대표Java HashMapPython dict, 구글 dense_hash

로드팩터와 리해싱 — 평균을 지키는 비용

로드팩터(load factor) = 원소 수 / 버킷 수. 이게 커지면 버킷당 충돌이 늘어 O(1)이 무너집니다. 그래서 임계값(Java 기본 0.75)을 넘으면 버킷 배열을 2배로 늘리고 전 원소를 재배치합니다 — 리해싱(rehashing). 이 순간만 O(n)이지만, 분할상환으로 보면 삽입당 평균 O(1)을 유지합니다.

로드팩터 0.75 초과 → 버킷 2배 + 전체 재배치 (리해싱) 기존 4 버킷 (꽉 참) 새 8 버킷 (재분배)

복잡도를 정리하면 이렇습니다.

연산평균최악
삽입O(1)O(n) (전부 충돌) / 리해싱 순간
조회O(1)O(n) → 트리화 시 O(log n)
삭제O(1)O(n)

평균이 깨지는 순간 — HashDoS

해시 테이블의 최악 O(n)은 이론적 호기심이 아닙니다. 공격자가 의도적으로 같은 버킷에 몰리는 키를 대량 전송하면, 모든 조회가 한 체인을 선형 탐색해 서버가 O(n²)로 마비됩니다 — HashDoS. 대응은 두 가지입니다.

  • 랜덤 시드 해시: 프로세스마다 무작위 시드를 섞어(SipHash 등) 공격자가 충돌 키를 예측 못 하게. Python·Rust·Java가 채택.
  • 트리화: 한 버킷이 길어지면 트리로 전환해 최악을 O(log n)으로 제한(Java 8 HashMap).
flowchart LR
    A["악의적 키 다발<br/>같은 버킷 유도"] --> B["한 체인에 폭주"]
    B --> C["조회마다 O(n) 선형탐색"]
    C --> D["서버 O(n²) 마비"]
    D -.대응.-> E["랜덤시드 해시 + 트리화"]

실무·AWS 연결

해시는 분산 시스템의 척추입니다. DynamoDB는 파티션 키를 해시해 데이터를 물리 파티션에 분산합니다 — 특정 키에 트래픽이 몰리면(hot partition) 해당 파티션만 throttle되는, 로드팩터 불균형의 분산 버전입니다. 노드를 추가/삭제할 때 전체 재배치(리해싱) 를 피하려고 분산 캐시는 단순 % N 대신 일관된 해싱을 씁니다.

프로덕션에서 마주치는 함정

함정증상해법
가변 객체를 키로키 변경 후 영영 못 찾음불변 키 사용
equals만 재정의, hashCode 누락같은 키인데 다른 버킷둘을 함께 재정의
초기 용량 미지정리해싱 반복으로 지연 스파이크예상 크기로 초기화
외부 입력을 키로 무방비HashDoS랜덤시드 해시·입력 검증
hot key/partition일부 노드만 과부하키 분산·샤딩·캐싱

면접/리뷰 단골 질문

  • Q. 해시 테이블이 평균 O(1)인 이유와, 깨지는 경우? → 균등 분포 가정 하에 버킷당 원소가 상수. 충돌이 한 버킷에 몰리면(또는 HashDoS) 최악 O(n).
  • Q. 체이닝과 오픈 어드레싱 차이? → 체이닝은 버킷별 리스트/트리, 오픈 어드레싱은 빈 버킷 탐사. 후자가 캐시 친화적이나 로드팩터·삭제에 민감.
  • Q. 로드팩터와 리해싱? → 원소/버킷 비율. 임계값 넘으면 버킷 2배로 늘려 전체 재배치, 분할상환 O(1) 유지.
  • Q. hashCode와 equals를 함께 재정의해야 하는 이유? → 같다고 판단되는 두 객체는 같은 해시를 내야 같은 버킷에서 찾힌다. 어기면 조회 실패.
  • Q. HashDoS 방어? → 랜덤 시드 해시(SipHash)로 충돌 예측 차단 + 긴 체인 트리화.

정리

  • 해시 테이블은 키를 정수 인덱스로 바꿔 평균 O(1) 조회를 준다. 충돌은 필연이라 “싸게 처리”가 핵심.
  • 충돌 해결은 체이닝(버킷별 리스트/트리)과 오픈 어드레싱(빈 버킷 탐사). 로드팩터가 임계값을 넘으면 리해싱으로 평균을 지킨다.
  • 최악 O(n)은 현실 위협(HashDoS)이다 — 랜덤 시드 해시 + 트리화로 막는다.
  • 분산 환경에선 % N 리해싱을 피하려 일관된 해싱으로 확장한다.

다음 글부터는 자료구조에 데이터를 정렬해 담는 이야기로 넘어갑니다 — 비교 정렬의 퀵·머지·힙과 “왜 O(n log n)보다 빠를 수 없는가”라는 하한을 봅니다.

</content> </invoke>

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 알고리즘 A-Z (20편)

  1. 복잡도 분석 — Big-O, 점근 표기, 그리고 분할상환이라는 거짓말 같은 진실
  2. 배열과 연결 리스트 — 같은 O(n)인데 왜 한쪽이 수십 배 느린가
  3. 스택·큐·힙 — 접근 규칙 하나가 만드는 강력함
  4. 해시 테이블 — 평균 O(1)의 비밀과 그 평균이 깨지는 순간 지금 보는 글
  5. 비교 정렬 — 퀵·머지·힙, 그리고 Ω(n log n)이라는 넘을 수 없는 벽
  6. 선형 시간 정렬 — 비교를 버리면 Ω(n log n) 벽이 사라진다
  7. 이진 탐색 — 반씩 버리는 마법, 그리고 "답을 이분하라"
  8. 재귀와 분할정복 — 콜스택에 쌓이는 신뢰, 마스터 정리로 푸는 비용
  9. 이진 탐색 트리와 균형 — 한쪽으로 쏠리는 트리를 회전으로 바로 세우기
  10. 그래프 순회 — BFS의 물결, DFS의 깊이, 그리고 위상정렬
  11. 최단 경로 — 다익스트라의 확신, A*의 직감, 벨만-포드의 인내
  12. 최소 신장 트리와 Union-Find — 가장 싸게 잇기, 그리고 거의 O(1)의 마법
  13. 동적계획법 — 같은 계산을 두 번 하지 않는다는 단 하나의 원칙
  14. 그리디 — 눈앞의 최선만 집었는데 전체가 최적일 때
  15. 문자열 매칭 — 어긋났을 때 얼마나 똑똑하게 다시 시작하는가
  16. 확률적 자료구조 — 정확함을 메모리와 맞바꾸는 기술 (Bloom·HyperLogLog·Count-Min)
  17. 일관된 해싱 — 노드가 들고날 때 데이터를 통째로 옮기지 않는 법
  18. 합의 알고리즘 — 죽는 서버들 사이에서 하나의 진실을 정하는 법 (Raft·Paxos)
  19. 레이트 리미팅과 스트리밍 알고리즘 — 폭주를 막고, 끝없는 흐름에서 표본을 뽑다
  20. 의미로 검색하다 — 벡터 검색(ANN·HNSW)과 PageRank, 그리고 시리즈의 끝