
자바 HashMap과 TreeMap 차이는 단순히 정렬되느냐 아니냐로만 끝나지 않습니다. 실제로는 순회 순서, 조회/삽입 비용, null key 허용 여부, Comparator 필요성까지 같이 봐야 제대로 선택할 수 있습니다.
많은 경우에는 그냥 Map이 필요하다는 이유로 HashMap부터 쓰게 됩니다. 이 선택이 틀린 것은 아니지만, 키를 정렬된 순서로 보여줘야 하거나 floorKey, ceilingKey 같은 범위 탐색이 필요하면 이야기가 달라집니다.
반대로 정렬이 전혀 필요 없는데도 습관적으로 TreeMap을 쓰면, 얻는 것보다 잃는 것이 더 많을 수 있습니다. 그래서 이 글에서는 정의 암기보다 언제 HashMap을 기본값으로 두고, 언제 TreeMap 비용을 감수할 가치가 있는지를 실무 기준으로 정리하겠습니다.
자바 HashMap과 TreeMap 차이, 먼저 한 줄로 정리하면
- <strong>정렬이 필요 없고 빠른 조회가 우선이면 <code>HashMap</code></strong>
- <strong>키를 정렬된 순서로 다뤄야 하거나 범위 탐색이 필요하면 <code>TreeMap</code></strong>
이 두 줄이 핵심입니다. 다만 실전에서는 여기서 끝나지 않습니다. 같은 Map이라도 iteration order를 보장하는지, get과 put 비용이 어느 정도인지, null key를 다룰 수 있는지, 키가 Comparable인지 같은 조건들이 꽤 다릅니다.
가장 먼저 봐야 할 것은 순서다
HashMap은 순서를 보장하지 않는다
HashMap 공식 문서는 순서를 보장하지 않는다고 분명히 말합니다. 즉, 지금 실행했을 때 출력 순서가 우연히 일정해 보여도 그 순서를 믿고 코드를 짜면 안 됩니다.
Map<Integer, String> map = new HashMap<>();
map.put(30, "C");
map.put(10, "A");
map.put(20, "B");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}이 코드는 어떤 순서로 출력될지 보장되지 않습니다. 환경과 내부 상태에 따라 다르게 보일 수 있습니다. 즉, HashMap은 조회용 저장소로는 좋지만, 보여주는 순서까지 기대하는 컨테이너는 아닙니다.
TreeMap은 키 기준으로 정렬된다
반면 TreeMap은 키의 natural ordering 또는 생성 시 넘긴 Comparator 기준으로 정렬됩니다. 그래서 같은 데이터를 넣어도 순회 결과를 예측할 수 있습니다.
Map<Integer, String> map = new TreeMap<>();
map.put(30, "C");
map.put(10, "A");
map.put(20, "B");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}10 -> A
20 -> B
30 -> C즉, 화면에 정렬된 목록을 바로 보여주거나, 로그를 키 순서대로 읽고 싶거나, 범위 기반 로직을 붙여야 하면 TreeMap 쪽이 훨씬 자연스럽습니다.
삽입 순서가 필요하다면 LinkedHashMap일 때도 많다
여기서 한 가지를 같이 기억하면 좋습니다. 정렬이 아니라 삽입 순서 유지가 목적이라면 TreeMap보다 LinkedHashMap이 더 맞는 경우가 많습니다. 즉, 순서 요구사항은 순서 필요 없음은 HashMap, 삽입 순서 유지는 LinkedHashMap, 키 정렬 순서는 TreeMap으로 나눠서 보는 편이 좋습니다.
성능은 HashMap이 기본값이 되는 이유다
HashMap은 적절한 해시 분산을 가정할 때 get, put이 상수 시간 성능으로 설명됩니다. 그래서 일반적인 조회와 저장에서는 보통 HashMap이 먼저 떠오릅니다. 반면 TreeMap은 red-black tree 기반이라 containsKey, get, put, remove가 보장된 O(log n)입니다.
- <code>HashMap</code>: 평균적으로 빠른 조회/삽입/삭제
- <code>TreeMap</code>: 정렬을 유지하는 대신 조회/삽입/삭제가 <code>log(n)</code>
여기서 중요한 건 TreeMap이 느리다기보다 정렬 기능 값을 비용으로 사는 구조라는 점입니다. 정렬 자체가 요구사항이면 그 비용은 낭비가 아닙니다. 반대로 정렬을 전혀 쓰지 않는데도 TreeMap을 쓰면 굳이 계속 비교 비용을 내면서 자료를 넣고 꺼내는 셈입니다.
HashMap은 순회 비용도 완전히 공짜는 아니다
공식 문서에는 HashMap 순회가 단순히 size뿐 아니라 capacity에도 영향을 받는다고 나옵니다. 대부분의 입문 글에서는 잘 안 다루지만, 대용량 데이터를 자주 순회하는 구조라면 내부 버킷 수와 초기 용량 설정이 순회 비용에도 영향을 줄 수 있다는 점은 기억할 만합니다.
null key와 null value 차이도 꽤 중요하다
HashMap은 null key와 null value를 허용한다
HashMap은 null key와 null value를 허용합니다. 그래서 아래 코드는 가능합니다.
Map<String, Integer> scores = new HashMap<>();
scores.put(null, 100);
scores.put("kim", null);
System.out.println(scores.get(null)); // 100
System.out.println(scores.get("kim")); // null다만 get() 결과가 null일 때는 정말 값이 null인 것인지, 아예 key가 없는 것인지 구분이 필요할 수 있으므로 이런 경우에는 containsKey()를 같이 보는 편이 안전합니다.
TreeMap은 기본 자연 정렬 기준에서 null key를 넣기 어렵다
TreeMap은 여기서 성격이 다릅니다. 공식 문서 기준으로, 지정한 key가 null이고 이 맵이 natural ordering을 사용하거나 comparator가 null key를 허용하지 않으면 NullPointerException이 날 수 있습니다.
보통 TreeMap은 null key에 보수적이고, 기본 자연 정렬 방식에서는 null key를 넣지 못한다고 생각하는 편이 안전합니다.
Map<String, Integer> scores = new TreeMap<>();
scores.put(null, 100); // NullPointerException다만 이걸 너무 강하게 절대 불가능이라고 쓰면 또 부정확해집니다. Comparator를 직접 만들어 null을 다루도록 설계하면 예외적으로 처리할 수 있기 때문입니다. 그래도 대부분의 코드에서는 TreeMap에 null key를 넣는 방향으로 설계하지 않는 편이 자연스럽습니다.
TreeMap은 정렬만이 아니라 비교 규칙 설계가 핵심이다
natural ordering
키가 String, Integer, LocalDate처럼 기본 비교 기준을 가진 타입이면 natural ordering으로 충분한 경우가 많습니다.
Map<String, Integer> map = new TreeMap<>();
map.put("banana", 2);
map.put("apple", 1);
map.put("carrot", 3);
System.out.println(map);
// {apple=1, banana=2, carrot=3}Comparator를 직접 줄 수도 있다
정렬 기준을 바꾸고 싶다면 Comparator를 넘기면 됩니다.
Map<String, Integer> map = new TreeMap<>(Comparator.reverseOrder());
map.put("banana", 2);
map.put("apple", 1);
map.put("carrot", 3);
System.out.println(map);
// {carrot=3, banana=2, apple=1}즉, TreeMap은 자료를 담는 그릇이 아니라 키 비교 규칙이 코드에 녹아 있는 자료구조에 가깝습니다.
compare 기준이 equals와 어긋나면 조심해야 한다
공식 문서는 정렬 기준이 equals와 일관적이어야 Map 계약을 올바르게 구현한다고 설명합니다. 실무에서는 두 키를 같은 것으로 볼지 다른 것으로 볼지 기준이 Comparator 안에서 애매하면, 예상과 다른 중복 처리나 덮어쓰기 동작을 만날 수 있다는 뜻으로 이해하면 됩니다.
TreeMap이 진짜 빛나는 순간은 범위 탐색이다
많은 사람이 TreeMap을 정렬된 출력용 Map 정도로만 생각합니다. 하지만 실무에서 더 큰 차이는 NavigableMap 계열 기능입니다. 특정 키보다 작거나 같은 가장 가까운 키, 특정 키보다 크거나 같은 가장 가까운 키, 최솟값과 최댓값 같은 탐색이 필요하면 TreeMap이 훨씬 자연스럽습니다.
TreeMap<Integer, String> map = new TreeMap<>();
map.put(10, "A");
map.put(20, "B");
map.put(30, "C");
System.out.println(map.floorKey(25)); // 20
System.out.println(map.ceilingKey(25)); // 30
System.out.println(map.firstKey()); // 10
System.out.println(map.lastKey()); // 30이런 기능이 필요하면 HashMap에 넣고 따로 정렬하거나 key를 뽑아 매번 다시 처리하는 것보다 TreeMap이 더 깔끔할 수 있습니다. 즉, 정렬된 상태가 계속 필요하다면 나중에 정렬하지 말고 처음부터 TreeMap을 쓰는 편이 낫다는 상황이 분명히 있습니다.
그래서 실무에서는 언제 무엇을 쓰면 될까
HashMap을 먼저 떠올려도 되는 경우
- 정렬된 순회가 필요 없다
- key로 빠르게 찾는 일이 핵심이다
- 범위 탐색이 없다
- 출력 순서를 보장할 필요가 없다
대표적인 예시는 아이디로 사용자 찾기, 상품 코드로 가격 찾기, 캐시성 조회 테이블, 단순 카운팅 맵 같은 경우입니다. 즉, Map을 lookup 용도로 쓰는 대부분의 평범한 상황에서는 HashMap이 더 자연스럽습니다.
TreeMap을 고르는 편이 맞는 경우
- 키가 정렬된 상태로 유지되어야 한다
- 최솟값, 최댓값, 바로 이전 값, 바로 다음 값 같은 탐색이 필요하다
- 범위 조건이 자주 나온다
- 매번 정렬해서 쓰는 코드가 반복된다
대표적인 예시는 점수 구간표, 날짜별 이벤트를 오름차순으로 관리하는 구조, 가격대별 nearest match 찾기, 정렬된 로그/통계 key 출력 같은 경우입니다. 즉, 정렬 결과가 우연히 한 번 필요한 것과 정렬 상태 자체가 요구사항인 것을 구분하는 것이 중요합니다.
한 번 정렬만 필요하면 HashMap + 정렬이 더 단순할 수도 있다
항상 TreeMap이 답은 아닙니다. 평소에는 빠른 조회만 하다가 마지막 화면 출력 시점에 딱 한 번만 정렬하면 된다면, 저장과 수정 단계는 HashMap으로 가고 출력 직전에 한 번 정렬하는 쪽이 더 단순하고 빠를 수 있습니다. 질문은 정렬이 필요하냐 하나로 끝나지 않고, 정렬이 언제 얼마나 자주 어느 시점에 필요한가까지 가야 합니다.
자주 하는 실수 5가지
- HashMap 순서가 우연히 일정하다고 해서 그 순서에 의존하는 실수
- 정렬된 출력이 필요하다는 이유만으로 모든 상황에 TreeMap을 쓰는 실수
- 삽입 순서 유지가 필요한데 TreeMap을 고르는 실수
- TreeMap에 null key를 넣을 수 있다고 가볍게 가정하는 실수
- Comparator 규칙을 대충 짜서 같은 키 처리 기준이 꼬이는 실수
한 번에 기억하는 선택 기준
- <mark style="background-color:var(–global-palette15)" class="has-inline-color">빠른 lookup이 우선이고 순서가 중요하지 않다 → HashMap</mark>
- <mark style="background-color:var(–global-palette15)" class="has-inline-color">키 정렬 상태와 범위 탐색이 중요하다 → TreeMap</mark>
- 삽입 순서를 유지하고 싶다 → <code>LinkedHashMap</code>도 같이 검토
- <code>null</code> key를 써야 한다 → <code>TreeMap</code>보다 <code>HashMap</code> 쪽이 훨씬 자연스럽다
- 정렬이 한 번만 필요하다 → <code>HashMap</code>에 담고 마지막에 정렬하는 방법도 고려
즉, HashMap과 TreeMap 차이는 자료구조 이름 차이가 아니라 요구사항이 무엇을 계속 보장해야 하느냐의 차이입니다.
마무리
자바 HashMap과 TreeMap 차이를 가장 실무적으로 요약하면 이렇습니다. HashMap은 빠른 조회를 위한 기본 Map이고, TreeMap은 정렬과 범위 탐색 기능까지 포함한 Map입니다. 그래서 대부분의 기본 선택은 HashMap이 맞습니다. 하지만 키 순서가 요구사항 자체라면, 또는 floorKey, ceilingKey, firstKey, lastKey 같은 정렬 기반 기능이 계속 필요하다면 TreeMap이 더 좋은 선택이 됩니다.
관련해서 자바 비교 글을 더 이어서 보고 싶다면 자바 equals와 == 차이, 자바 오버로딩과 오버라이딩 차이도 함께 읽어보면 좋습니다. 공식 기준은 HashMap 문서, TreeMap 문서, Map 문서를 참고하면 됩니다.