Java HashMap vs Hashtable vs ConcurrentHashMap: 동시성과 성능의 완벽 가이드
내부 구조와 동작 원리
HashMap: 단일 스레드 최적화 설계
HashMap은 배열과 연결 리스트(또는 레드-블랙 트리)를 조합한 해시 테이블로 구현됩니다. 키의 해시코드를 배열 인덱스로 변환하여 O(1) 평균 시간 복잡도로 접근합니다.
// HashMap 내부 구조 개념
Node<K,V>[] table; // 버킷 배열
static class Node<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 충돌 시 체이닝
}
Java 8부터 동일 버킷에 8개 이상의 노드가 충돌하면 연결 리스트를 레드-블랙 트리로 변환하여 최악의 경우 O(log n) 성능을 보장합니다.
Hashtable: 전역 동기화 메커니즘
Hashtable은 모든 메서드에 synchronized 키워드를 적용하여 스레드 안전성을 확보합니다. 이는 메서드 레벨 동기화로, 전체 테이블에 대한 독점적 접근을 의미합니다.
public synchronized V put(K key, V value) {
// 전체 테이블 락
}
public synchronized V get(Object key) {
// 읽기 작업도 락 필요
}
ConcurrentHashMap: 세그먼트 기반 락 분산
ConcurrentHashMap은 테이블을 여러 세그먼트로 분할하여 락 경합을 최소화합니다. Java 8부터는 CAS(Compare-And-Swap) 연산과 더 세밀한 락 메커니즘을 사용합니다.
// Java 8+ ConcurrentHashMap의 CAS 기반 삽입
final V putVal(K key, V value, boolean onlyIfAbsent) {
for (Node<K,V>[] tab = table;;) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS 성공 시 락 없이 삽입
// 충돌 시에만 세밀한 락 사용
}
}
동시성 처리 방식의 본질적 차이
| 구분 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 동기화 레벨 | 없음 | 메서드 전체 | 버킷/세그먼트 단위 |
| 읽기 성능 | 최고 (락 없음) | 낮음 (독점 락) | 높음 (락 없는 읽기) |
| 쓰기 성능 | 최고 (단일 스레드) | 낮음 (전역 락) | 중간 (세밀한 락) |
| null 허용 | 키/값 모두 허용 | 둘 다 금지 | 둘 다 금지 |
성능 특성과 메모리 오버헤드
메모리 사용량 분석
HashMap은 가장 적은 메모리를 사용하며, ConcurrentHashMap은 동시성 제어를 위한 추가 메타데이터로 약 20-30% 더 많은 메모리를 소비합니다. Hashtable은 중간 수준의 메모리를 사용합니다.
처리량(Throughput) 비교
// 단일 스레드 환경 (1M 연산)
HashMap: ~1000ms
Hashtable: ~1200ms
ConcurrentHashMap: ~1150ms
// 멀티 스레드 환경 (8 스레드, 1M 연산)
HashMap: 데이터 손실 발생
Hashtable: ~8000ms (병목)
ConcurrentHashMap: ~2500ms (최적)
실전 동시성 패턴과 함정
HashMap의 Race Condition
멀티스레드 환경에서 HashMap은 리사이징 과정에서 무한 루프를 발생시킬 수 있습니다. 이는 다음과 같은 시나리오에서 발생합니다:
// 위험한 패턴 - Race Condition 발생 가능
Map<String, Integer> map = new HashMap<>();
// 여러 스레드가 동시에 실행
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i); // 리사이징 중 무한루프 가능
}
};
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executor.submit(task); // 10개 스레드 동시 실행
}
ConcurrentHashMap의 원자성 보장 범위
ConcurrentHashMap은 개별 연산의 스레드 안전성을 보장하지만, 복합 연산은 별도의 동기화가 필요합니다:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 안전하지 않은 복합 연산
if (map.get("counter") == null) {
map.put("counter", 1); // 다른 스레드가 중간에 삽입 가능
} else {
map.put("counter", map.get("counter") + 1); // 원자성 보장 안됨
}
// 올바른 원자적 연산 사용
map.compute("counter", (key, val) -> val == null ? 1 : val + 1);
특수 상황별 최적화 전략
대용량 데이터 처리 시나리오
백만 개 이상의 엔트리를 다루는 경우, ConcurrentHashMap의 초기 용량과 동시성 레벨을 조정하면 성능을 크게 개선할 수 있습니다:
// 대용량 데이터 최적화
ConcurrentHashMap<String, Object> bigMap = new ConcurrentHashMap<>(
1_000_000, // 초기 용량
0.75f, // 로드 팩터
32 // 동시성 레벨 (CPU 코어 수의 배수)
);
읽기 집약적 vs 쓰기 집약적 워크로드
읽기 집약적 (읽기:쓰기 = 9:1 이상): ConcurrentHashMap이 Hashtable보다 5-10배 빠른 성능을 보입니다.
쓰기 집약적 (쓰기:읽기 = 7:3 이상): 여전히 ConcurrentHashMap이 우수하지만, 성능 차이가 줄어듭니다.
Java 버전별 진화와 최적화
Java 8 이전 vs 이후 성능 차이
Java 8에서 ConcurrentHashMap은 내부 구조가 완전히 개편되었습니다. 세그먼트 기반에서 노드 기반 락킹으로 변경되어 더 세밀한 동시성 제어가 가능해졌습니다.
// Java 8+ 의 병렬 연산 지원
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 병렬 스트림을 통한 고성능 집계
long sum = map.values().parallelStream()
.mapToLong(Integer::longValue)
.sum();
// 원자적 집계 연산
map.reduceValues(1, Integer::sum); // 병렬성 임계값 = 1
JIT 컴파일러 최적화 고려사항
HashMap은 JIT 컴파일러의 최적화를 가장 잘 받는 구조입니다. 반면 ConcurrentHashMap의 복잡한 동시성 제어 로직은 최적화에 제약이 있어, 웜업 시간이 더 길어집니다.
개발자를 위한 실전 선택 기준
HashMap은 단일 스레드 환경이나 불변 데이터, 초기화 후 읽기만 하는 경우에 사용하세요. 성능이 가장 뛰어나고 메모리 사용량도 최소입니다.
ConcurrentHashMap은 멀티스레드 환경에서 첫 번째 선택지입니다. 특히 읽기가 많은 워크로드에서 탁월한 성능을 보입니다. null 값이 필요하다면 Optional로 래핑하는 것이 좋습니다.
Hashtable은 레거시 코드 호환성이 필요한 경우를 제외하고는 피하는 것이 좋습니다. 동일한 스레드 안전성을 원한다면 Collections.synchronizedMap()을 사용하거나, 더 나은 선택인 ConcurrentHashMap을 사용하세요.
실제 운영 환경에서는 프로파일링을 통해 병목 지점을 확인하고, 동시성 레벨과 초기 용량을 워크로드에 맞게 튜닝하는 것이 중요합니다.
'웹개발 > Java' 카테고리의 다른 글
| 자바 객체 복사의 모든 것: 깊은 복사 vs 얕은 복사의 차이와 실전 예제 (6) | 2025.07.30 |
|---|---|
| Java에서 NullPointerException 예방법 총정리! 실무에서 유용한 팁 모음 (0) | 2025.07.29 |
| Java Stream API 정렬, 실무에서는 이렇게 씁니다 (Comparable, Comparator 완벽 정리) (1) | 2025.07.20 |
| Java Optional 제대로 쓰고 계신가요? 실무에서 자주 하는 실수와 올바른 사용법 총정리 (2) | 2025.07.20 |
| 자바 equals와 ==의 차이 정확히 알고 계신가요? (실무에서 자주 하는 실수 예시 포함) (2) | 2025.07.09 |