Java에서 키-값 데이터를 다루는 방법: Map과 HashMap

Java에서는 데이터를 효율적으로 관리하기 위해 다양한 컬렉션 프레임워크를 제공합니다.
그중 Map 인터페이스와 HashMap 클래스는 키와 값의 쌍을 저장하고 관리하는 데 최적화된 데이터 구조입니다. 이번 포스팅에서는 Map과 HashMap의 개념, 주요 특징, 사용법, 그리고 예제 코드를 통해 자세히 알아보겠습니다.
<목차>1. Map 인터페이스란?2. HashMap 클래스란?2-1. Collections.synchronizedMap()2-2. ConcurrentHashMap
|
1. Map 인터페이스란?
Map은 키(key)와 값(value)의 쌍으로 데이터를 저장하는 Java의 인터페이스입니다.
- 키는 고유해야 하며, 중복을 허용하지 않습니다.
- 값은 중복을 허용합니다.
- (만약, 키 값이 이미 존재한다면, 값을 갱신합니다.)
- 키를 사용하여 값을 빠르게 검색할 수 있습니다.
- 저장순서를 유지하지 않습니다.
Map은 인터페이스이기 때문에 이를 구현한 다양한 클래스가 존재하며, 대표적으로 HashMap, LinkedHashMap, TreeMap 등이 있습니다.
2. HashMap 클래스란?
HashMap은 Map 인터페이스를 구현한 클래스입니다. 내부적으로 해시 테이블을 사용하여 키를 기반으로 값을 저장하며, 빠른 검색 속도를 제공합니다.
주요 특징:
- 순서를 보장하지 않음: 데이터 삽입 순서를 유지하지 않습니다.
- null 허용: 하나의 null 키와 여러 개의 null 값을 저장할 수 있습니다.
- 비동기적: 멀티스레드 환경에서 안전하지 않으므로, 동기화가 필요하면 Collections.synchronizedMap() 또는 ConcurrentHashMap을 사용해야 합니다.
1. Collections.synchronizedMap() | 2. ConcurrentHashMap | |
정의 | Collections.synchronizedMap()은 기존의 Map 구현체(HashMap 등)를 동기화된 Map으로 감싸는 래퍼(wrapper)를 제공합니다. 이를 통해 모든 메서드 호출을 동기화하여 멀티스레드 환경에서 안전하게 사용할 수 있습니다. | ConcurrentHashMap은 동시성을 위해 설계된 Map 구현체로, 락 분할(lock striping) 기법을 사용하여 성능과 동기화를 모두 확보합니다. Java 1.5부터 제공되며, HashMap과 유사한 API를 제공합니다. |
특징 | - synchronized 키워드를 사용하여 각 메서드 호출을 동기화합니다. - 동기화 블록을 통해 put(), get() 등 Map 메서드를 호출하는 동안 다른 스레드가 접근하지 못하도록 잠금(lock)을 걸어줍니다. |
동기화 방식: - 내부적으로 락 분할(Lock Striping) 기법을 사용합니다. - 데이터를 여러 개의 버킷(bucket)으로 나누고, 각 버킷마다 개별적으로 락을 걸어줍니다. - 특정 키에만 접근하는 경우, 해당 버킷에만 락을 걸기 때문에 다른 스레드는 동시에 다른 버킷에 접근 가능합니다. *장점* Collections.synchronizedMap()보다 성능이 우수합니다. 반복문(keySet(), entrySet() 등)을 사용할 때도 별도 동기화가 필요 없습니다. |
단점 | - 동기화 범위가 넓기 때문에 성능 저하가 발생할 수 있습니다. - 반복문 사용 시 주의: 반복문 (keySet(), entrySet(), values() 등)을 사용할 때는 별도로 synchronized 블록으로 감싸야 합니다. 그렇지 않으면 ConcurrentModificationException이 발생할 수 있습니다. |
- 메모리 사용량이 더 많습니다. - 읽기와 쓰기가 동시에 이루어지는 경우 최신 상태를 정확히 보장하지는 않습니다. (Weak Consistency) |
<한눈에 정리> | ||
동기화 수준 | 메서드 전체 | 락분할(버킷 단위로 락을 걸어 동시성향상) |
반복문 사용시 추가동기화 필요어부 | 필요함 | 불필요함 |
성능 | 동기화 범위가 넓어 성능 저하 가능 | 락 분할로 성능이 좋음 |
메모리사용 | 비교적 적음 | 상대적으로 더 많은 메모리 사용 |
동시 읽기/쓰기 | 모든 접근이 차단됨 | 여러 스레드가 동시에 읽기/쓰기 가능 |
적합환경 | 간단한 멀티스레드 환경 | 고성능 멀티스레드 환경 |
1. Collections.synchronizedMap() 사용예시
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
Map<String, String> synchronizedMap = Collections.synchronizedMap(map);
synchronizedMap.put("key1", "value1");
synchronizedMap.put("key2", "value2");
//synchronized 키워드 사용해서 각 메서드 호출을 동기화함.
synchronized (synchronizedMap) {
for (String key : synchronizedMap.keySet()) {
//반복문 사용시 sy..생략 블록으로 감싸야함. 안그럼
// ConcurrentModificationException 오류 날 수 있음.
System.out.println(key + ": " + synchronizedMap.get(key));
}
}
}
}
2. ConcurrentHashMap 사용예시
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) {
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", "value1");
concurrentMap.put("key2", "value2");
System.out.println(concurrentMap.get("key1"));
// 반복문에서도 별도 동기화 필요 없음
for (String key : concurrentMap.keySet()) {
System.out.println(key + ": " + concurrentMap.get(key));
}
}
}
3. Map과 HashMap 사용법
- Map 선언 및 HashMap 생성
import java.util.Map;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
// Map 선언 및 HashMap 객체 생성
Map<String, Integer> map = new HashMap<>();
// 데이터 삽입
map.put("apple", 5);
map.put("banana", 2);
map.put("orange", 7);
// 데이터 출력
System.out.println("apple의 값: " + map.get("apple")); // apple의 값: 5
System.out.println("키가 'banana'인지 확인: " + map.containsKey("banana")); // true
// 데이터 삭제
map.remove("orange");
// 모든 키-값 출력
for (String key : map.keySet()) {
System.out.println(key + " : " + map.get(key));
}
}
}
============================================================================
실행결과 :
apple의 값: 5
키가 'banana'인지 확인: true
apple : 5
banana : 2
4. 주요 메서드 정리
메서드 이름 | 설명 |
put(key, value) | 키와 값을 추가하거나, 키가 존재하면 값을 갱신(덮어씀) |
get(key) | 키에 해당하는 값을 반환, 값 없으면 null |
remove(key) | 키에 해당하는 키-값 쌍을 제거한다. |
containsKey(key) | 키가 존재하는지 확인 |
containValue(value) | 값이 존재하는지 확인 |
keySet() | 모든키를 반환 |
values() | 모든 값을 반환 |
size() | 저장된 키-값 쌍의 개수를 반환 |
5. HashMap 사용 시 주의점
1. 순서를 보장하지 않음
: HashMap은 삽입 순서를 유지하지 않음. 만약 순서가 중요하다면? LinkedHashMap을 사용하세요
2. 멀티스레드 환경에서의 문제
: HashMap은 비동기적임 -> 따라서 위에 있는 ConcurrentHashMap 또는 Collections.synchronizedMap()을 사용해야 안전함
3. null키와 값
: 하나의 null키와 여러개의 null값을 허용함.
6. 활용 예제: 빈도수 계산
//HashMap을 활용해 문자열의 빈도수를 계산하는 간단한 예제
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String[] fruits = {"apple", "banana", "apple", "orange", "banana", "apple"};
Map<String, Integer> frequencyMap = new HashMap<>();
for (String fruit : fruits) {
frequencyMap.put(fruit, frequencyMap.getOrDefault(fruit, 0) + 1);
}
// 결과 출력
for (String key : frequencyMap.keySet()) {
System.out.println(key + " : " + frequencyMap.get(key));
}
}
}
=================================
apple : 3
banana : 2
orange : 1
7. 마무리
HashMap은 빠르고 효율적인 키-값 데이터 구조로, Java 개발에서 자주 사용됩니다.
데이터의 삽입 순서가 중요하지 않고, 빠른 검색이 필요할 때 유용합니다.
그러나 멀티스레드 환경에서는 동기화를 고려해야 합니다. 알고리즘 테스트에도 사용할 수 있어서
배우게된 Map인터페이스 HashMap 클래스 였습니다.