도입
자바에서 데이터를 다룰 때 가장 자주 만나는 자료구조는 보통 List, Set, Map입니다.
이 중 Map은 “순서대로 나열된 값의 모음”이 아니라, 특정 키를 이용해 값을 빠르게 찾기 위한 구조라는 점에서 매우 중요합니다.
예를 들어 회원 ID로 회원 정보를 찾거나, 상품 코드로 가격을 조회하거나, 설정 이름으로 설정값을 꺼내는 상황은 대부분 Map으로 해결할 수 있습니다. 그래서 Map은 알고리즘 문제뿐 아니라 실무 백엔드, 웹 개발, 데이터 처리, 캐싱, 설정 관리 등 거의 모든 자바 개발 영역에서 자주 사용됩니다.
정의
자바의 Map은 java.util.Map 인터페이스로 제공되며, 데이터를 key-value 형태로 저장합니다. 여기서 핵심은 키는 중복될 수 없고, 값은 중복될 수 있다는 점입니다.
즉, 같은 키로 값을 다시 넣으면 새로운 엔트리가 하나 더 생기는 것이 아니라, 기존 값이 덮어써집니다. 그래서 Map은 “중복 없는 식별자”를 기준으로 데이터를 관리할 때 특히 강력합니다.
- Map은 키와 값의 쌍으로 데이터를 저장한다.
- 키는 중복 불가, 값은 중복 가능하다.
- 같은 키로 put하면 새 데이터가 추가되는 것이 아니라 기존 값이 변경된다.
필요성
만약 사용자 10만 명의 정보를 List에 넣고 특정 ID를 가진 사용자를 찾는다면, 최악의 경우 끝까지 순회해야 합니다. 하지만 Map을 사용하면 보통 키를 통해 훨씬 빠르게 원하는 값을 찾을 수 있습니다.
그래서 Map은 단순 저장소가 아니라, 조회 성능을 개선하고 데이터 접근 방식을 바꾸는 핵심 도구입니다. 특히 “이름으로 찾기”, “번호로 찾기”, “코드로 찾기”, “설정 키로 찾기” 같은 패턴은 거의 전부 Map과 잘 맞습니다.
- 빠른 조회: 키를 이용해 값을 빠르게 찾을 수 있다.
- 명확한 구조: “무엇을 기준으로 찾는가”가 분명하다.
- 실무 활용성: 캐시, 설정, 집계, 카운팅, 인덱싱 등에 광범위하게 쓰인다.
- 알고리즘 활용성: 빈도 수 계산, 중복 제거 보조, 해시 기반 탐색 등에 자주 등장한다.
List, Set과 차이
| 구조 | 저장 방식 | 중복 | 주요 목적 |
|---|---|---|---|
| List | 값의 순차 저장 | 허용 | 순서 있는 데이터 관리 |
| Set | 값 집합 저장 | 불가 | 중복 없는 데이터 관리 |
| Map | 키-값 쌍 저장 | 키 중복 불가 | 키 기반 조회 |
특히 Map은 Collection 인터페이스를 직접 상속하지 않는다는 점도 중요합니다. List와 Set은 “값들의 모음”이지만, Map은 “키와 값의 관계”를 다루는 별도의 계열입니다.
기본 사용법
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
scores.put("kim", 90);
scores.put("lee", 85);
scores.put("park", 95);
System.out.println(scores.get("kim")); // 90
System.out.println(scores.containsKey("lee")); // true
scores.put("kim", 100); // 기존 값 덮어쓰기
scores.remove("park");
System.out.println(scores);
put(key, value): 데이터 저장get(key): 키에 대응하는 값 조회remove(key): 해당 키 삭제containsKey(key): 키 존재 여부 확인containsValue(value): 값 존재 여부 확인size(): 저장된 엔트리 개수isEmpty(): 비어 있는지 확인clear(): 전체 삭제
주요 구현체
| 구현체 | 특징 | 순서 보장 | 정렬 | 스레드 안전 |
|---|---|---|---|---|
| HashMap | 가장 많이 쓰는 기본 구현체 | 보장 안 함 | 안 함 | 아님 |
| LinkedHashMap | 삽입 순서 또는 접근 순서 유지 | 보장 | 안 함 | 아님 |
| TreeMap | 키 기준 정렬 유지 | 정렬 순서 | 함 | 아님 |
| ConcurrentHashMap | 동시성 환경용 | 보장 안 함 | 안 함 | 지원 |
| Hashtable | 구식 동기화 구현체 | 보장 안 함 | 안 함 | 지원 |
HashMap
도입자바에서 가장 널리 쓰이는 키-값 저장소이자, 빠른 조회를 가능하게 하는 대표 자료구조다자바에서 Map을 사용할 때 가장 먼저 떠올리는 구현체는 보통 HashMap입니다. 이유는 단순합니다. 대
develop-enchantment.tistory.com
HashMap의 성격을 유지하면서도 삽입 순서를 보장합니다. LRU 캐시 같은 구조를 만들 때 접근 순서(access order) 옵션과 함께 자주 활용됩니다.
키를 자동 정렬된 상태로 유지합니다. 내부적으로 보통 레드-블랙 트리 기반으로 동작하며, 정렬된 순서가 중요한 경우에 유용합니다.
여러 스레드가 동시에 Map을 다루는 환경에서 사용합니다. 단순히 HashMap을 공유하는 것보다 훨씬 안전하며, Hashtable보다 일반적으로 더 현대적인 선택입니다.
과거부터 존재하던 동기화 Map이지만, 지금은 보통 ConcurrentHashMap이 더 많이 권장됩니다.
내부 동작 원리
자바에서 가장 자주 쓰는 Map은 보통 HashMap입니다.
키 객체의 hashCode() 결과를 바탕으로 저장 위치를 계산하고, 그 위치에 값을 저장하거나 찾습니다.
- 키의 hashCode 계산
- 배열 인덱스 결정
- 해당 버킷(bucket) 확인
- 같은 키가 있으면 값 변경
- 없으면 새 엔트리 추가
만약 서로 다른 키가 같은 해시 위치로 들어오면 해시 충돌이 발생합니다. 이때 버킷 내부에서 추가 구조를 이용해 데이터를 관리합니다. 충돌이 많아지면 성능이 나빠질 수 있기 때문에, 키 객체의 equals()와 hashCode()를 올바르게 구현하는 것이 매우 중요합니다.
equals와 hashCode
Map의 키 비교는 단순히 ==만으로 이루어지지 않습니다. 특히 HashMap은 먼저 hashCode()로 저장 위치를 찾고, 그 다음 equals()로 진짜 같은 키인지 판단합니다.
따라서 사용자 정의 객체를 키로 사용할 때 equals()만 재정의하거나, hashCode()만 재정의하면 Map이 예상대로 동작하지 않을 수 있습니다.
import java.util.Objects;
public class UserKey {
private final String id;
public UserKey(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserKey)) return false;
UserKey userKey = (UserKey) o;
return Objects.equals(id, userKey.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
순회 방법
for (String key : scores.keySet()) {
System.out.println(key + " : " + scores.get(key));
}
for (Integer value : scores.values()) {
System.out.println(value);
}
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
보통 키와 값을 함께 써야 한다면 entrySet() 순회가 가장 효율적이고 자연스럽습니다. keySet으로 순회하면서 get을 반복하는 방식은 불필요한 조회가 추가될 수 있습니다.
null 처리
| 구현체 | null 키 | null 값 |
|---|---|---|
| HashMap | 허용 | 허용 |
| LinkedHashMap | 허용 | 허용 |
| TreeMap | 일반적으로 허용 안 함 | 허용 가능 |
| ConcurrentHashMap | 허용 안 함 | 허용 안 함 |
| Hashtable | 허용 안 함 | 허용 안 함 |
특히 get(key)가 null을 반환했다고 해서 “키가 없음”인지 “값이 null임”인지 바로 단정하면 안 됩니다. 이런 경우에는 containsKey(key)로 함께 확인하는 습관이 좋습니다.
시간 복잡도
| 구현체 | 조회 | 삽입 | 삭제 |
|---|---|---|---|
| HashMap | 평균 O(1) | 평균 O(1) | 평균 O(1) |
| LinkedHashMap | 평균 O(1) | 평균 O(1) | 평균 O(1) |
| TreeMap | O(log n) | O(log n) | O(log n) |
따라서 정렬이 필요 없고 빠른 조회가 핵심이면 HashMap, 입력 순서 유지가 중요하면 LinkedHashMap, 정렬된 키 탐색이 중요하면 TreeMap이 자연스러운 선택입니다.
실전 패턴
Map<String, Integer> countMap = new HashMap<>();
for (String word : words) {
countMap.put(word, countMap.getOrDefault(word, 0) + 1);
}
Map<String, List<String>> groupMap = new HashMap<>();
for (String name : names) {
String firstLetter = name.substring(0, 1);
groupMap.computeIfAbsent(firstLetter, k -> new ArrayList<>()).add(name);
}
ID → 객체, URL → 응답, 코드 → 설정값처럼 “무언가를 기준으로 바로 찾아야 하는 경우”에는 거의 항상 Map이 등장합니다.
주의할 점
- 키 중복 덮어쓰기: 같은 키를 put하면 이전 값이 사라진다.
- 키 객체의 불변성: mutable 객체를 키로 쓰면 위험하다.
- equals/hashCode 불일치: 조회 실패나 중복 문제를 부를 수 있다.
- 순서 오해: HashMap은 순서를 보장하지 않는다.
- 멀티스레드 환경: 일반 HashMap을 공유하면 안전하지 않을 수 있다.
- null 처리 혼동: get 결과가 null일 때 의미를 정확히 구분해야 한다.
디버깅
get() 결과가 null일 때는 키 없음과 null 값을 구분한다.요약
- ✅ Map은 키와 값의 쌍을 저장하는 자료구조다.
- ✅ 키는 중복될 수 없고, 같은 키로 put하면 값이 덮어써진다.
- ✅ 일반적인 기본 선택은 HashMap이다.
- ✅ 순서가 필요하면 LinkedHashMap, 정렬이 필요하면 TreeMap, 동시성이 필요하면 ConcurrentHashMap을 고려한다.
- ✅ 사용자 정의 키를 쓸 때는 equals와 hashCode를 반드시 함께 설계해야 한다.