Skip to main content

Command Palette

Search for a command to run...

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

Published
•6 min read•View as Markdown

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

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

여러 스레드가 동시에 데이터를 읽고 쓰는 상황에서, 단순히 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)를 작성해 실제 워크로드에서 성능 차이를 검증하는 것이 가장 확실한 방법입니다.

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

More from this blog

개발자 vs AI: 연말에 되돌아보는 내가 되고 싶은 개발자 유형은 무엇일까?

# 서론 – 연말, 개발자의 일상이 왜 달라지는가 12월이 되면 많은 개발자들은 데드라인이 느려지고, 팀 채팅은 조용해지며, 풀 리퀘스트(PR)는 한가해지는 것을 경험합니다. “올해는 정말 바빴다”는 감회와 함께, 동시에 “다음 해에는 어떤 개발자가 되고 싶을까?” 하는 고민이 자연스럽게 떠오릅니다. 특히 최근 AI 기술이 급격히 발전하면서, “AI가 내 일을 빼앗을까?” 라는 불안감과 “AI와 협업하면 내 역량을 어떻게 강화할 수 있을까?” ...

Dec 29, 20255 min read

인사이트 부채(Insight Debt)란? 초보 개발자를 위한 쉬운 이해와 해결 방법

# 서론 – 왜 우리는 “인사이트 부채”에 주목해야 할까? 소프트웨어 개발 현장에서 인사이트 부채(Insight Debt) 라는 용어를 들어본 적이 있나요? 프로젝트를 진행하면서 “이 부분은 나중에 다시 살펴볼게” 라는 생각을 자주 하게 됩니다. 하지만 이런 미뤄진 고민이 쌓이면 결국 코드 품질 저하, 버그 증가, 팀 커뮤니케이션 비용 등 다양한 문제를 야기합니다. 이번 포스트에서는 인사이트 부채가 정확히 무엇인지, 왜 발생하는지, 그리고 초보...

Dec 29, 20253 min read

블록체인 활용: 일상 속 5가지 실생활 적용 사례와 시작 가이드

# 서론 – “블록체인, 우리 생활에 뭐가 바뀔까?” 최근 뉴스와 SNS에서 블록체인이라는 단어를 자주 보지만, 실제로 우리 주변에 어떻게 적용되고 있는지는 잘 모르는 경우가 많습니다. “복잡하고 비싸다”는 인식이 남아 있지만, 최근는 블록체인 활용이 점점 쉬워지고 비용도 낮아져 일반인도 직접 체험할 수 있는 시대가 왔습니다. 이번 글에서는 블록체인 적용 사례 5가지를 중심으로, 초보자도 바로 이해하고 시작할 수 있는 방법을 알려드립니다. #...

Dec 29, 20254 min read

스크래핑 온 더 엣지: SvelteKit으로 만든 초고속 만화 뷰어 만들기 (50자)

# 서론 – “만화 읽기가 이렇게 불편할 수 있을까?” 우리 모두가 한 번쯤 겪어본 일입니다. 만화를 읽고 싶을 땐 원하는 사이트가 로딩이 오래 걸리거나, 페이지를 넘길 때마다 광고가 끊임없이 뜨고, 이미지가 깨져서 눈이 피로해지는 경우가 있죠. 이런 불편함을 그냥 견뎌야 할까요? 개발자라면 “코드로 해결할 수 있지 않을까?” 하는 생각이 떠오릅니다. 이번 포스트에서는 SvelteKit과 웹 스크래핑을 활용해, 초고속 만화 뷰어를 직접 만드는 ...

Dec 29, 20254 min read

Tim van der Lippe가 Mockito 유지보수자를 떠나다 – Java 테스트 생태계에 미치는 영향은?

SEO 키워드: Mockito, Java 단위 테스트, 테스트 자동화 # 서론 – “왜 이 소식이 중요한가?” 최근 Reddit의 r/programming 커뮤니티에 올라온 글에서 Tim van der Lippe가 오랜 기간 담당해 온 Mockito 프로젝트의 유지보수자를 물러난다는 소식이 전해졌습니다. Mockito는 Java 개발자 사이에서 가장 널리 쓰이는 테스트 자동화 라이브러리 중 하나이며, 수많은 오픈소스 프로젝트와 기업 서비스의 ...

Dec 29, 20253 min read2
H

HA JIN-YOUNG's blog

967 posts