# 동시성 해시맵 설계 비교: synchronized, 샤딩, Java ConcurrentHashMap·Rust DashMap 완전 가이드 (55자)

---

# # 서론 – “왜 동시성 해시맵이 필요할까?”

멀티코어 시대에 **동시성 해시맵**은 거의 모든 서버‑사이드 애플리케이션의 핵심 인프라가 되었습니다.

여러 스레드가 동시에 데이터를 읽고 쓰는 상황에서, 단순히 `HashMap`을 공유하면 **데이터 레이스**와 **예측 불가능한 오류**가 발생합니다.

그렇다면 개발자는 어떤 설계 방식을 선택해야 할까요?

- 가장 직관적인 **전역 락(synchronized)**

- **샤딩(Sharding)** 방식, 흔히 Rust의 `DashMap`처럼 파티션을 나누는 방법

- 그리고 Java 표준 라이브러리에서 제공하는 **ConcurrentHashMap**

각 접근법은 **성능**, **구현 복잡도**, **메모리 사용량**에서 서로 다른 트레이드‑오프를 가집니다. 이번 포스팅에서는 위 세 가지 설계를 **초보자도 이해하기 쉬운 수준**으로 풀어보고, 실제 프로젝트에 적용할 때 고려해야 할 포인트를 정리합니다.

> **SEO 키워드**: 동시성 해시맵, Java ConcurrentHashMap, Rust DashMap

---

# # 1️⃣ 전역 락 – `synchronized` 로 구현하는 가장 단순한 동시성 해시맵

## # 1.1 개념과 구현

`java.util.Hashtable`이나 `Collections.synchronizedMap`이 내부적으로 사용하는 방식이 바로 **전역 락**입니다.

```
java

public class SyncHashMap<K,V> {

    private final Map<K,V> map = new HashMap<>();

    public synchronized V get(K key) {

        return map.get(key);

    }

    public synchronized V put(K key, V value) {

        return map.put(key, value);

    }

}

```

- **모든 메서드**에 `synchronized` 키워드를 붙여 **한 번에 하나의 스레드만** 접근하도록 제한합니다.

- 구현이 **매우 간단**하고, **데이터 일관성**을 보장합니다.

## # 1.2 장점

| 장점 | 설명 |

|------|------|

| **구현 난이도 낮음** | 락을 한 줄만 추가하면 동시성 보장이 완료됩니다. |

| **디버깅 용이** | 락 경합이 명확해 deadlock 상황을 추적하기 쉽습니다. |

| **예측 가능한 성능** | 단일 락 때문에 스레드 수가 늘어나면 성능이 선형적으로 감소합니다. |

## # 1.3 단점 – 병목 현상의 근원

- **스레드 경쟁**이 심해지면 **잠금 대기 시간**이 급격히 늘어나 전체 처리량이 크게 감소합니다.

- **읽기 전용 작업**도 쓰기와 동일한 락을 사용하므로, 읽기 비중이 높은 워크로드에선 **극심한 성능 저하**가 발생합니다.

- **스케일 아웃**(CPU 코어 수 증가) 효과를 거의 기대할 수 없습니다.

## # 1.4 언제 사용하면 좋은가?

- **프로토타입**이나 **테스트 코드**처럼 동시성 요구가 낮은 경우.

- **읽기/쓰기 비율이 1:1**에 가깝고, **코어 수가 적은 환경**(예: 작은 데스크톱 앱).

---

# # 2️⃣ 샤딩(분할) 방식 – `DashMap` 스타일 동시성 해시맵

## # 2.1 기본 아이디어

**샤딩**은 전체 해시 테이블을 **N개의 파티션(버킷)** 으로 나누고, 각 파티션마다 **독립적인 락**을 부여하는 설계입니다.

- **키**를 해시한 뒤 `hash % N` 로 어느 파티션에 저장할지 결정합니다.

- 파티션마다 **별도의 `HashMap`** 와 **뮤텍스**(또는 `RwLock`)가 존재합니다.

Rust의 `DashMap`이 대표적인 구현이며, Java에서도 `StripedLock`이나 `SegmentedConcurrentHashMap` 형태로 구현할 수 있습니다.

## # 2.2 구현 예시 (Rust)

```
rust

use dashmap::DashMap;

let map = DashMap::new(); // 내부적으로 64개의 샤드 사용

map.insert('key1', 10);

if let Some(v) = map.get('key1') {

    println!('{}', *v);

}

```

- **각 샤드**는 독립적인 락을 가지고 있어, 서로 다른 키에 대한 동시 접근이 **충돌 없이** 진행됩니다.

## # 2.3 장점

| 장점 | 설명 |

|------|------|

| **높은 병렬성** | 서로 다른 샤드에 대한 작업은 동시에 수행 가능, 코어 수가 늘어날수록 처리량 증가. |

| **읽기‑쓰기 구분** | `RwLock`을 사용하면 읽기 전용 스레드가 락을 공유해 **읽기 스루풋**을 크게 향상. |

| **스케일링 용이** | 파티션 수(N)를 조정하면 메모리·성능 트레이드‑오프를 손쉽게 맞출 수 있음. |

## # 2.4 단점

- **구현 복잡도**: 파티션 관리, 해시 충돌 처리, 락 해제 시점 등을 직접 설계해야 함.

- **메모리 오버헤드**: 각 파티션마다 별도 `HashMap`과 락 객체가 필요해 **총 메모리 사용량**이 증가.

- **키 분포에 민감**: 해시가 특정 파티션에 집중되면 **핫스팟**이 발생, 기대한 병렬성이 사라짐.

## # 2.5 언제 사용하면 좋은가?

- **읽기 비중이 높고, 키가 고르게 분포**된 대규모 서비스(예: 캐시 서버, 세션 스토어).

- **멀티코어 환경**에서 **수천~수만** 개의 동시 요청을 처리해야 할 때.

---

# # 3️⃣ Java ConcurrentHashMap – 실전에서 가장 많이 쓰이는 동시성 해시맵

## # 3.1 내부 구조 살펴보기

`java.util.concurrent.ConcurrentHashMap`은 **샤딩**과 **락 스트리핑**을 결합한 복합 설계입니다.

1. **버킷 배열**(Segment) → 초기에는 16개의 버킷을 갖고, 필요 시 **리사이징**.

2. 각 버킷은 **`Node<K,V>`** 체인(또는 트리) 형태로 충돌을 처리.

3. **읽기 작업**은 **락 없이**(`volatile` 읽기) 수행 가능 → **읽기 스루풋**이 매우 높음.

4. **쓰기 작업**은 해당 버킷에만 **`synchronized`** 락을 잡아 다른 버킷과는 독립적.

> **핵심 포인트**: `ConcurrentHashMap`은 **버킷 단위 락**을 사용해 **전역 락**보다 훨씬 높은 병렬성을 제공합니다.

## # 3.2 주요 메서드 동작

| 메서드 | 락 사용 여부 | 비고 |

|-------|--------------|------|

| `get(key)` | **X** (락 없음) | `volatile` 읽기, 최신값 보장 |

| `put(key, value)` | **O** (버킷 락) | 충돌 시 체인/트리 업데이트 |

| `computeIfAbsent` | **O** (버킷 락) | 원자적 연산, 락 경합 최소화 |

| `size()` | **O** (전역 락) | 정확한 크기 계산 시 전체 락 필요 (대체: `mappingCount`) |

## # 3.3 장점

| 장점 | 설명 |

|------|------|

| **검증된 구현** | JDK 표준 라이브러리, 광범위한 테스트와 실전 검증. |

| **읽기‑쓰기 균형** | 읽기는 락 없이, 쓰기는 버킷 락만 사용해 **높은 처리량** 확보. |

| **자동 리사이징** | 내부적으로 **동시 리사이징**을 수행해 메모리 효율 유지. |

| **API 풍부** | `compute`, `merge`, `forEach` 등 **원자적 연산** 메서드 제공. |

## # 3.4 단점

- **버킷 수 고정**(디폴트 16) → 초기 용량이 작으면 **리사이징 비용**이 크게 발생할 수 있음.

- **읽기 스냅샷**이 아니라 **실제 최신값**을 반환하므로, **읽기 일관성**이 완전하지 않을 수 있음(예: `size()`와 `isEmpty()` 사이에 변동).

- **특정 상황**(예: 매우 큰 트랜잭션)에서는 **버킷 락**이 여전히 병목이 될 수 있음.

## # 3.5 언제 사용하면 좋은가?

- **일반적인 서버‑사이드 애플리케이션**(웹 서버, 마이크로서비스)에서 **표준적인 동시성 요구**가 있을 때.

- **읽기 비중이 높고, 쓰기도 빈번히 발생**하는 경우(예: 세션 캐시, 메트릭 수집).

---

# # 4️⃣ 성능·안전성 비교 – 어떤 설계가 내 서비스에 적합할까?

| 항목 | 전역 락 (`synchronized`) | 샤딩 (`DashMap` 스타일) | Java ConcurrentHashMap |

|------|--------------------------|--------------------------|------------------------|

| **동시성 수준** | ★☆☆☆☆ (심각한 병목) | ★★★★★ (높은 병렬성) | ★★★★☆ (버킷 락) |

| **구현 난이도** | ★☆☆☆☆ (매우 쉬움) | ★★★★☆ (복잡) | ★★☆☆☆ (표준 API) |

| **메모리 오버헤드** | ★☆☆☆☆ (낮음) | ★★★★☆ (파티션·락) | ★★☆☆☆ (버킷 배열) |

| **읽기 스루풋** | ★★☆☆☆ (전역 락) | ★★★★★ (읽기 전용 락 공유) | ★★★★☆ (락 없음) |

| **쓰기 스루풋** | ★★☆☆☆ (전역 락) | ★★★★★ (버킷 별 락) | ★★★★☆ (버킷 락) |

| **리사이징 비용** | ★★☆☆☆ (전체 잠금) | ★★★★☆ (파티션 별) | ★★★★☆ (동시 리사이징) |

| **추천 사용 사례** | 프로토타입, 작은 툴 | 대규모 캐시, 고성능 서비스 | 일반적인 비즈니스 로직 |

> **핵심 정리**

- **전역 락**은 **가장 간단하지만**, **성능이 크게 제한**됩니다.

- **샤딩**은 **최고의 병렬성**을 제공하지만 **구현·운영 비용**이 높습니다.

- **ConcurrentHashMap**은 **표준 구현**으로 **대부분의 상황**에 충분히 좋은 성능을 보이며, **추가적인 튜닝**(initial capacity, load factor)만으로도 최적화가 가능합니다.

---

# # 5️⃣ 실제 적용 팁 – 프로젝트에 맞는 동시성 해시맵 선택하기

1. **워크로드 분석**

   - **읽기 비중**이 80% 이상이면 `ConcurrentHashMap` 혹은 `DashMap`을 권장.

   - **쓰기 비중**이 매우 높고, **키 충돌**이 빈번하면 파티션 수를 늘린 **샤딩**이 유리.

2. **키 해시 분포 확인**

   - `hash(key) % N` 결과가 **고르게** 나오는지 테스트(예: `java.util.Random`을 이용한 시뮬레이션) 후 파티션 수(N)를 결정.

   - 불균형이면 **커스텀 해시 함수**(MurmurHash3 등)를 적용.

3. **초기 용량 설정**

   - `ConcurrentHashMap`은 **initialCapacity**와 **loadFactor**를 적절히 지정해 **리사이징 비용**을 최소화.

   - 예시: `new ConcurrentHashMap<>(1_024, 0.75f)`

4. **메모리 제한**

   - **샤딩**은 파티션당 `HashMap`과 락 객체가 존재하므로, 메모리 사용량이 **수배** 증가할 수 있음.

   - 메모리 제한이 엄격한 임베디드 환경에서는 **전역 락**이나 **작은 크기의 ConcurrentHashMap**을 선택.

5. **프로파일링**

   - JDK 11+에서는 `-XX:+PrintConcurrentLocks` 옵션으로 **락 경합**을 로그에 출력해 병목을 시각화.

   - Rust에서는 `cargo bench`와 `perf`를 이용해 **샤드 별 락 대기 시간**을 측정.

---

# # 결론 – 선택은 “필요와 상황”에 달렸다

동시성 해시맵 설계는 **‘성능 vs 구현 난이도 vs 메모리 비용’**이라는 세 축을 저울질하는 작업입니다.

- **초보자**라면 **전역 락**으로 시작해 동시성 개념을 익힌 뒤, 필요에 따라 **ConcurrentHashMap**으로 전환하는 것이 가장 안전합니다.

- **고성능 서비스**라면 **샤딩(DashMap 스타일)**을 도입해 **코어 수에 비례하는 스루풋**을 확보하고, **키 해시 분포**와 **파티션 수**를 정밀 튜닝하세요.

- **표준적인 비즈니스 로직**에서는 **Java ConcurrentHashMap**이 대부분의 요구를 만족시키며, **API 풍부함**과 **JDK 검증**이라는 큰 장점을 제공합니다.

> **행동 유도**

1️⃣ 지금 바로 프로젝트에 `ConcurrentHashMap`을 도입해 **읽기‑쓰기 성능**을 측정해 보세요.

2️⃣ 키 해시 분포가 의심된다면 **샤딩 구현**을 작은 프로토타입으로 실험해 보고, **파티션 수**를 조정해 최적의 병렬성을 찾아보세요.

3️⃣ 최종 선택이 어려우면 **벤치마크 코드**(JMH, Criterion)를 작성해 실제 워크로드에서 **성능 차이를 검증**하는 것이 가장 확실한 방법입니다.

동시성 해시맵을 올바르게 선택하고 튜닝한다면, **수천 개의 동시 요청**도 거뜬히 처리할 수 있는 **탄탄한 시스템**을 만들 수 있습니다. 오늘 배운 설계 원칙을 바탕으로 여러분의 서비스에 가장 알맞은 해시맵을 적용해 보세요! 🚀
