HashMap은 자바 컬렉션 프레임워크의 자료구조로 많이 사용이 됩니다. 하지만 코드를 작성함에 따라 동시성 문제에 따라 자연스럽게 ConcurrentHashMap를 사용해야 된다고 알게되는데, 해당 포스팅에서는 도대체 해당 Concurrent가 붙게 됨으로 무엇이 달라지고 어떠한 장점을 알아보려 합니다.
우선적으로 기본적인 개념을 알아야 하기에 HashMap을 간단하게 알아보자면 데이터를 키-값 쌍으로 저장하는 데 사용이 되는데 HashMap은 내부적으로 해시 테이블을 사용하여 데이터를 저장하며, 키를 이용해 값을 매우 빠르게 검색할 수 있는 특징을 지닙니다.
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("Apple", 10);
map.put("Banana", 15);
map.put("Orange", 20);
// 키를 사용하여 값 접근
int appleCount = map.get("Apple");
// 데이터 업데이트
map.put("Apple", 12);
// 특정 키가 있는지 확인
if (map.containsKey("Banana")) {
return 1;
}
// 모든 키-값 쌍 출력
for (String key : map.keySet()) {
System.out.println(map.get(key));
}
// 키와 값 제거
map.remove("Orange");
}
}
HashMap에는 get, put, containKey, keySet, remove 등으로 데이터를 조작할 수 있고 주로 String을 기준으로 데이터를 빠르게 접속해야 할 때 많이 사용을 했었습니다.
이러한 HashMap에는 몇 가지 특징이 존재하는데 "키와 값이 null을 허용", "순서가 없음" 이러한 특징들이 있지만 해당 포스팅에서 알아볼 주요한 특징은 "비동기적"이라는 것입니다.
동기적 vs 비동기적
"동기적" 은 작업이 순차적으로 실행되는 것을 의미합니다. 즉, 하나의 작업이 완료되기 전까지 다음 작업이 시작되지 않습니다. 예를 들어, A 작업이 끝나야만 B 작업이 시작됩니다.
"비동기적" 은 작업이 동시에 실행될 수 있는 것을 의미합니다. 즉, 하나의 작업이 완료될 때까지 기다리지 않고 다른 작업을 바로 시작할 수 있습니다. 예를 들어, A 작업이 완료되기 전에 B 작업이 동시에 진행될 수 있습니다.
동기적 처리는 순서대로 작업이 이루어지기 때문에 직관적이지만, 대기 시간이 길어질 수 있습니다. 반면 비동기적 처리는 효율적으로 여러 작업을 동시에 처리할 수 있지만, 관리가 복잡할 수 있습니다.
여기서 주목해야 할 점은 HashMap은 비동기적이라는 사실인데, 동기화를 지원하지 않으므로 즉 다시 말하면, 멀티스레드 환경에서 동시에 여러 스레드가 접근을 할 경우 데이터 일관성이 깨질 수 있습니다. 그러한 이유를 대표적으로 나타내 보자면
HashMap이 멀티스레드 환경에서 불안전한 이유
동기화 부재:
HashMap은 기본적으로 동기화 처리가 되어 있지 않기 때문에, 여러 스레드가 동시에 HashMap에 데이터를 추가하거나 수정하는 경우, 내부 구조가 손상될 수 있습니다. 예를 들어, 두 스레드가 동시에 같은 버킷에 데이터를 삽입하려고 하면, 올바르게 처리되지 않아 데이터가 유실되거나 순서가 어긋날 수 있습니다.
경쟁 상태:
멀티스레드 환경에서 두 개 이상의 스레드가 동일한 리소스에 동시에 접근할 때 발생할 수 있는 문제입니다. 예를 들어, 하나의 스레드가 put() 메서드를 사용하여 HashMap에 값을 추가하는 동시에, 다른 스레드가 get() 메서드를 통해 값을 읽으려고 하면, 제대로 된 결과를 얻지 못할 수 있습니다.
교착 상태:
스레드들이 서로의 자원에 접근하려고 기다리는 상태가 발생할 수 있습니다. HashMap 자체가 교착 상태를 유발하는 구조를 가지고 있지 않지만 잘못된 동기화 코드로 인해 교착 상태가 발생할 가능성이 있습니다.
이렇게 HashMap은 분명 유용한 자료구조이지만, 한계를 가지는 자료구조입니다. 이러한 문제를 해결하기 위한 방법은 비동기적인 HashMap을 동기화로 만들거나, 혹은 ConcurrentHasmMap을 사용해서 내부적으로 동기화 처리를 잘 하여서 스레드가 동시에 안전하게 접근을 허용하게 할 수 있습니다.
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
해당 방법은 HashMap을 동기화 시키는 코드인데, Collections.synchronizedMap() 메서드 사용하여 동기화 HashMap을 생성할 수 있습니다. 이렇게 하면 모든 HashMap의 메서드가 동기화되어, 한 번에 하나의 스레드만 HashMap에 접근할 수 있게 됩니다.
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
자바의 ConcurrentHashMap은 멀티스레드 환경에서 사용하기 위해 설계된 해시맵으로, 내부적으로 동기화 처리가 되어 있어 여러 스레드가 동시에 안전하게 접근할 수 있습니다. ConcurrentHashMap은 HashMap보다 동시성을 더 잘 관리하면서도 높은 성능을 제공합니다
여기까지 알아보았을 때 HashMap은 분명 유용하지만, 단일 스레드 환경에서는 장점만을 발휘합니다. 하지만 멀티스레드 환경에서 여러 가지 문제가 발생할 수 있습니다
이러한 문제를 해결하기 위해 자바에서는 ConcurrentHashMap이라는 클래스를 제공하는데, 이는 동시성 문제를 해결하고, 성능을 유지하면서 안전하게 사용할 수 있는 자료구조라는 점입니다.
그렇다면 ConcurrentHashMap의 특징과 동일화된 HashMap과 어떻게 차이가 나는지를 자세히 살펴보겠습니다.
HashMap VS ConcurrentHashMap

내부 구조의 차이:
ConcurrentHashMap의 모든 작업이 동기화되는 것은 아닙니다. 추가 및 삭제와 같은 수정 작업만 동기화되며, 읽기 작업은 동기화되지 않습니다. 이는 성능을 유지하기 위해 설계된 구조입니다.
동기화가 필요한 경우와 그렇지 않은 경우를 분리하여 처리합니다. 반면 HashMap을 Collections.synchronizedMap()으로 동기화하면, 모든 작업이 동기화되어 성능 저하가 발생할 수 있습니다. 이 점에서 ConcurrentHashMap은 보다 효율적입니다.
Null 키와 Null 값의 처리:
HashMap은 하나의 null 키와 여러 null 값을 허용합니다. 반면, ConcurrentHashMap은 null 키와 null 값을 허용하지 않습니다. 이 차이로 인해 ConcurrentHashMap을 사용할 때는 키와 값이 null이 되지 않도록 주의해야 합니다.
오류 감지 메커니즘:
HashMap의 Iterator는 fail-fast 방식을 사용합니다. 이는 Iterator가 생성된 이후에 Map이 수정되면, 즉시 ConcurrentModificationException 예외가 발생하여 프로그램이 중단됩니다.
반면, ConcurrentHashMap의 Iterator는 fail-safe 방식을 사용합니다. Iterator가 생성된 이후에 Map이 수정되어도 예외가 발생하지 않으며, Iterator는 생성 시점의 Map 상태를 기반으로 일관된 값을 반환합니다. 이는 수정된 값이 반영되지 않음을 의미합니다.
1) Fail-Fast:
Fail-Fast 메커니즘을 사용하는 컬렉션은 반복 중에 컬렉션이 수정되면 즉시 예외를 발생시킵니다. 이는 주로 ConcurrentModificationException이라는 예외로 나타납니다.
Fail-Fast 컬렉션은 반복자를 생성할 때, 컬렉션의 구조적인 변경을 감지하기 위해 mod count 라는 내부 변수를 사용합니다. 반복자가 반복 중일 때 이 카운트가 변경되면, 즉시 예외를 던집니다.
2) Fail-Safe:
Fail-Safe 메커니즘을 사용하는 컬렉션은 반복 중에 컬렉션이 수정되더라도 예외를 발생시키지 않습니다. 이 경우, 컬렉션이 수정된 상태를 바로 반영하지 않을 수도 있습니다.
Fail-Safe 컬렉션은 반복자가 컬렉션의 복사본을 기반으로 작업하기 때문에, 반복 중에 원본 컬렉션이 변경되더라도 영향을 받지 않습니다. 단, 반복자가 생성된 이후의 변경 사항은 반영되지 않기 때문에, 반복 중에 읽은 값이 최신 상태와 일치하지 않을 수 있습니다.
HashMap의 fail-fast 메커니즘 예시
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class HashMapFailFastExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Apple", 10);
map.put("Banana", 15);
map.put("Orange", 20);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
if (key.equals("Banana")) {
// ConcurrentModificationException 발생
map.put("Grapes", 25);
}
System.out.println(key + ": " + map.get(key));
}
}
}
HashMap의 Iterator가 동작 중일 때, Map을 수정하려고 하면 ConcurrentModificationException이 발생합니다. 이는 HashMap의 Iterator가 fail-fast 방식을 사용하기 때문입니다.
ConcurrentHashMap의 fail-safe 메커니즘 예시
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapFailSafeExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("Apple", 10);
map.put("Banana", 15);
map.put("Orange", 20);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
if (key.equals("Banana")) {
// ConcurrentModificationException 발생하지 않음
map.put("Grapes", 25);
}
System.out.println(key + ": " + map.get(key));
}
System.out.println(map);
}
}
ConcurrentHashMap에서는 Iterator가 동작 중일 때 수정 작업을 수행해도 예외가 발생하지 않습니다. Iterator는 생성 시점의 Map 상태를 기반으로 일관된 값을 반환합니다.
이 예시에서는 "Grapes" 키가 출력되지 않지만, 이후의 Map에는 "Grapes": 25가 포함되어 있습니다. 이는 ConcurrentHashMap의 fail-safe 특성 때문입니다.
마무리
HashMap은 내부에서 동기화를 제공하지 않기 때문에, 단일 스레드 애플리케이션에 적합하고 반면, ConcurrentHashMap은 멀티스레드 환경에서 동기화를 제공하여 여러 스레드가 동시에 Map에 접근하고 수정할 수 있도록 설계되었습니다. 따라서 동시 다중 스레드 애플리케이션에서는 ConcurrentHashMap을 사용하는 것이 적절합니다.
'Java' 카테고리의 다른 글
| 커스텀 예외 처리 방법 (0) | 2024.12.20 |
|---|---|
| Java (람다와 함수 인터페이스) (1) | 2024.01.07 |
| Java (익명 클래스) (1) | 2024.01.06 |
| Java (컬렉션 프레임워크) (0) | 2024.01.06 |
| Java (제네릭스) (1) | 2024.01.04 |