본문 바로가기

해시 테이블 (Hash Table)

@iamrain2025. 9. 24. 20:16

해시 테이블은 키(Key)를 값(Value)에 매핑할 수 있는 데이터 구조로, 평균적으로 O(1) 의 매우 빠른 시간 복잡도로 데이터에 접근(삽입, 삭제, 탐색)할 수 있다. 내부적으로는 배열(버킷, Bucket)을 사용하여 데이터를 저장하며, 해시 함수(Hash Function)를 통해 키를 배열의 특정 인덱스로 변환하여 해당 값에 접근한다.

이러한 특징 때문에 해시 테이블은 연관 배열(Associative Array)이나 딕셔너리(Dictionary)와 같은 자료구조를 구현하는 데 널리 사용됩니다.

1. 내부 구조 및 핵심 구성 요소

해시 테이블은 크게 세 가지 핵심 요소로 구성된다.

  1. 해시 함수 (Hash Function): 임의의 길이의 데이터를 고정된 길이의 데이터(해시 값 또는 해시)로 매핑하는 함수.
  2. 해시 버킷 (Hash Buckets): 해시 함수에 의해 계산된 해시 값을 인덱스로 사용하는 배열. 실제 데이터가 저장되는 공간.
  3. 충돌 해결 메커니즘 (Collision Resolution Mechanism): 서로 다른 두 개 이상의 키가 동일한 해시 값으로 매핑될 때(이를 '해시 충돌'이라 함) 이를 해결하기 위한 방법.

Hash Table Structure

1.1. 해시 함수 (Hash Function)

좋은 해시 함수는 다음과 같은 특징을 가진다.

  • 결정론적 (Deterministic): 동일한 입력 키에 대해서는 항상 동일한 해시 값을 반환해야 한다.
  • 빠른 계산 속도: 해시 값을 계산하는 과정이 빨라야 O(1)의 성능을 보장할 수 있다.
  • 균등 분포 (Uniform Distribution): 키들이 해시 테이블의 버킷에 최대한 균일하게 분포되도록 해야 한다. 특정 영역에 데이터가 집중되면 충돌이 잦아져 성능이 저하된다.

대표적인 해시 함수 기법은 다음과 같다.

  • 나눗셈법 (Division Method): h(k) = k mod m. 키 k를 테이블 크기 m으로 나눈 나머지를 해시 값으로 사용한다. m은 보통 소수(prime number)를 사용하여 분포도를 높인다.
  • 곱셈법 (Multiplication Method): h(k) = floor(m * (k * A mod 1)). k에 0과 1 사이의 상수 A를 곱한 후 소수 부분에 m을 곱하고, 그 결과의 정수 부분을 해시 값으로 사용한다.
  • 유니버설 해싱 (Universal Hashing): 여러 해시 함수 중 하나를 무작위로 선택하여 사용하는 기법. 악의적인 입력에 대해 해시 충돌을 유발하기 어렵게 만들어 보안성을 높인다.

1.2. 충돌 해결 메커니즘 (Collision Resolution)

해시 충돌은 비둘기집 원리에 따라 필연적으로 발생할 수밖에 없다. 이를 해결하는 방법은 크게 두 가지로 나뉜다.

1.2.1. 체이닝 (Chaining, 폐쇄 주소법)

  • 개념: 각 버킷을 단일 데이터 슬롯이 아닌, 연결 리스트(Linked List)나 트리(Tree, 예: 레드-블랙 트리)와 같은 다른 자료구조의 헤드로 사용하는 방식. 충돌이 발생하면 해당 버킷의 리스트에 새로운 노드를 추가하여 데이터를 저장한다.
  • 장점:
    • 구현이 비교적 간단하다.
    • 테이블의 적재율(Load Factor)이 1을 초과해도 데이터를 계속 저장할 수 있다.
    • 데이터 삭제가 용이하다.
  • 단점:
    • 포인터 기반의 연결 리스트를 사용하므로, 캐시 지역성(Cache Locality)이 좋지 않아 성능 저하가 발생할 수 있다.
    • 각 노드에 다음 노드를 가리키는 포인터를 저장해야 하므로 추가적인 메모리 오버헤드가 있다.
  • 사용: C++의 std::unordered_map, Java의 HashMap 등 많은 표준 라이브러리에서 기본 방식으로 채택하고 있다.

1.2.2. 개방 주소법 (Open Addressing)

  • 개념: 충돌이 발생했을 때, 미리 정해진 규칙에 따라 비어있는 다른 버킷을 찾아 데이터를 저장하는 방식. 모든 데이터는 해시 테이블 배열 내에 직접 저장된다.
  • 탐사(Probing) 기법:
    1. 선형 탐사 (Linear Probing): h(k, i) = (h'(k) + i) mod m. 충돌 시, 바로 다음 버킷(i=1), 그 다음 버킷(i=2) 순으로 비어있는 공간을 찾는다.
      • 장점: 캐시 효율성이 높다.
      • 단점: 데이터가 연속된 군집(Primary Clustering)을 이루는 경향이 있어, 특정 영역에 데이터가 몰리면 탐사 시간이 길어진다.
    2. 제곱 탐사 (Quadratic Probing): h(k, i) = (h'(k) + c1*i + c2*i^2) mod m. 충돌 시, i^2만큼 떨어진 버킷을 탐사한다.
      • 장점: 선형 탐사의 1차 군집 문제를 완화한다.
      • 단점: 동일한 해시 값을 갖는 키들은 모두 같은 순서로 탐사를 진행하는 2차 군집(Secondary Clustering) 문제가 발생할 수 있다.
    3. 이중 해싱 (Double Hashing): h(k, i) = (h1(k) + i*h2(k)) mod m. 두 개의 다른 해시 함수를 사용하여, 하나는 초기 해시 값 결정에, 다른 하나는 탐사 폭 결정에 사용한다.
      • 장점: 군집 문제에 가장 강인한 모습을 보인다.
      • 단점: 두 개의 해시 함수를 계산해야 하므로 연산량이 많다.
  • 개방 주소법의 장단점:
    • 장점: 포인터를 사용하지 않아 메모리 오버헤드가 적고, 캐시 지역성이 좋아 데이터가 적을 때 체이닝보다 빠를 수 있다.
    • 단점: 데이터 삭제가 복잡하다(단순히 삭제하면 탐사 경로가 끊길 수 있어, '삭제됨' 상태를 표시해야 함). 테이블이 거의 찼을 때(적재율이 높을 때) 성능이 급격히 저하된다.

1.3. 비교: 체이닝 vs 개방 주소법

특징 체이닝 (Separate Chaining) 개방 주소법 (Open Addressing)
데이터 저장 위치 버킷에 연결된 외부 자료구조 (주로 연결 리스트) 해시 테이블 배열 내부
메모리 효율성 포인터로 인한 추가 오버헤드 발생 포인터가 없어 오버헤드가 적음
캐시 효율성 낮음 (데이터가 흩어져 있음) 높음 (데이터가 인접해 있을 가능성 높음)
적재율(Load Factor) 1 이상 가능 항상 1 미만이어야 함
삭제 연산 간단함 복잡함 (삭제 표시 필요)
성능 적재율이 높아져도 비교적 안정적인 성능 적재율이 낮을 때 빠르지만, 높아지면 급격히 저하
주요 사용처 데이터의 개수를 예측하기 어려울 때, 삽입/삭제가 빈번할 때 데이터 개수가 예측 가능하고, 메모리 제한이 중요할 때

2. 동작 계층 구조

게임 애플리케이션에서 해시 테이블을 사용하는 경우, 다음과 같은 계층을 거쳐 동작한다.

  1. 사용자 영역 (Application Layer)
    • 게임 개발자가 std::unordered_map<std::string, Player*> 같은 C++ 표준 라이브러리의 해시 테이블을 사용하여 플레이어 ID로 플레이어 객체를 관리.
    • playerMap["Player123"] = new Player(); 와 같은 코드를 실행.
  2. 라이브러리 계층 (C++ Standard Library)
    • std::unordered_map은 내부적으로 키("Player123")를 std::hash<std::string> 객체를 통해 해시 값으로 변환.
    • 계산된 해시 값을 버킷 배열의 크기로 모듈러 연산하여 인덱스를 얻음.
    • 해당 인덱스의 버킷에 접근하여 충돌 여부를 확인.
    • std::unordered_map은 주로 체이닝 방식을 사용하므로, 해당 버킷의 연결 리스트를 순회하며 동일한 키가 있는지 확인.
    • 키가 없으면 새로운 노드를 생성하여 Player 객체의 포인터를 저장하고 리스트에 추가.
  3. 런타임/메모리 관리 계층 (C++ Runtime & OS Memory Manager)
    • 새로운 노드나 Player 객체를 new 키워드로 생성할 때, C++ 런타임은 힙(Heap) 메모리 할당을 요청.
    • 운영체제(OS)의 메모리 관리자는 이 요청을 받아, 프로세스의 가상 주소 공간에서 사용 가능한 영역을 찾아 할당, 이를 물리적 RAM에 매핑.
  4. 운영체제 커널 계층 (OS Kernel Layer)
    • 메모리 할당/해제, 프로세스 스케줄링 등 시스템의 핵심 자원을 관리. 애플리케이션의 메모리 접근이 유효한지(예: 다른 프로세스의 메모리를 침범하지 않는지) 감시.
  5. 하드웨어 계층 (Hardware Layer)
    • CPU는 해시 함수 계산, 메모리 주소 변환, 데이터 읽기/쓰기 등의 명령을 실행.
    • CPU 캐시는 자주 접근하는 데이터(예: 해시 테이블의 일부, 연결 리스트의 최근 노드)를 저장하여 RAM에 직접 접근하는 횟수를 줄여 성능을 향상시키. 개방 주소법이 캐시 효율성이 좋은 이유.

4. 요약

해시 테이블이 무엇이고 어떻게 동작하는지 설명해주시겠어요?

 

해시 테이블은 키와 값을 짝지어 저장하는 자료구조로, 평균적으로 O(1)이라는 매우 빠른 속도로 데이터를 삽입, 삭제, 검색할 수 있는 것이 가장 큰 특징입니다.

내부적으로는 '버킷'이라고 불리는 배열을 사용하는데, '해시 함수'라는 것을 이용해서 주어진 키를 이 배열의 특정 인덱스, 즉 해시 값으로 변환합니다. 그리고 이 인덱스를 통해 데이터가 저장된 버킷에 곧바로 접근하는 원리입니다.

 

서로 다른 키가 우연히 같은 인덱스로 변환되는 '해시 충돌'이 발생할 수 있습니다. 이를 해결하기 위한 방법으로는 크게 '체이닝'과 '개방 주소법'이 있습니다. '체이닝'은 같은 인덱스에 여러 데이터가 충돌하면, 그들을 연결 리스트 같은 별도의 자료구조로 연결해서 관리하는 방식입니다. C++의 std::unordered_map이 이 방식을 사용합니다. 반면 '개방 주소법'은 충돌이 나면, 정해진 규칙에 따라 다른 비어있는 버킷을 찾아서 데이터를 저장하는 방식입니다.

 

체이닝은 구현이 쉽고 데이터가 많아져도 유연하게 대처할 수 있지만, 포인터 사용으로 캐시 효율이 떨어질 수 있습니다. 개방 주소법은 캐시 효율이 좋고 메모리를 적게 쓰지만, 데이터가 많아지면 성능이 급격히 나빠지고 삭제가 복잡하다는 단점이 있습니다. 따라서 상황에 맞는 적절한 충돌 해결 전략을 선택하는 것이 중요합니다.

'Computer Science' 카테고리의 다른 글

캐시  (0) 2025.09.19
스택, 큐, 연결 리스트  (0) 2025.09.04
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차