ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

Map

도입

키(Key)와 값(Value)을 한 쌍으로 저장하는 자바의 대표적인 자료구조다

자바에서 데이터를 다룰 때 가장 자주 만나는 자료구조는 보통 List, Set, Map입니다.

이 중 Map은 “순서대로 나열된 값의 모음”이 아니라, 특정 키를 이용해 값을 빠르게 찾기 위한 구조라는 점에서 매우 중요합니다.

예를 들어 회원 ID로 회원 정보를 찾거나, 상품 코드로 가격을 조회하거나, 설정 이름으로 설정값을 꺼내는 상황은 대부분 Map으로 해결할 수 있습니다. 그래서 Map은 알고리즘 문제뿐 아니라 실무 백엔드, 웹 개발, 데이터 처리, 캐싱, 설정 관리 등 거의 모든 자바 개발 영역에서 자주 사용됩니다.

정의

하나의 키에 하나의 값을 대응시키는 키-값 기반 자료구조다

자바의 Mapjava.util.Map 인터페이스로 제공되며, 데이터를 key-value 형태로 저장합니다. 여기서 핵심은 키는 중복될 수 없고, 값은 중복될 수 있다는 점입니다.

즉, 같은 키로 값을 다시 넣으면 새로운 엔트리가 하나 더 생기는 것이 아니라, 기존 값이 덮어써집니다. 그래서 Map은 “중복 없는 식별자”를 기준으로 데이터를 관리할 때 특히 강력합니다.

핵심 요약
  • Map은 키와 값의 쌍으로 데이터를 저장한다.
  • 키는 중복 불가, 값은 중복 가능하다.
  • 같은 키로 put하면 새 데이터가 추가되는 것이 아니라 기존 값이 변경된다.
한 줄 정리
Map = 키로 값을 찾는 구조라고 이해하면 가장 쉽습니다.

필요성

데이터를 “순회해서 찾는 방식”이 아니라 “키로 즉시 접근하는 방식”으로 바꿔준다

만약 사용자 10만 명의 정보를 List에 넣고 특정 ID를 가진 사용자를 찾는다면, 최악의 경우 끝까지 순회해야 합니다. 하지만 Map을 사용하면 보통 키를 통해 훨씬 빠르게 원하는 값을 찾을 수 있습니다.

그래서 Map은 단순 저장소가 아니라, 조회 성능을 개선하고 데이터 접근 방식을 바꾸는 핵심 도구입니다. 특히 “이름으로 찾기”, “번호로 찾기”, “코드로 찾기”, “설정 키로 찾기” 같은 패턴은 거의 전부 Map과 잘 맞습니다.

Map이 중요한 이유
  • 빠른 조회: 키를 이용해 값을 빠르게 찾을 수 있다.
  • 명확한 구조: “무엇을 기준으로 찾는가”가 분명하다.
  • 실무 활용성: 캐시, 설정, 집계, 카운팅, 인덱싱 등에 광범위하게 쓰인다.
  • 알고리즘 활용성: 빈도 수 계산, 중복 제거 보조, 해시 기반 탐색 등에 자주 등장한다.

List, Set과 차이

List는 순서 중심, Set은 중복 제거 중심, Map은 키 기반 조회 중심 구조다
구조 저장 방식 중복 주요 목적
List 값의 순차 저장 허용 순서 있는 데이터 관리
Set 값 집합 저장 불가 중복 없는 데이터 관리
Map 키-값 쌍 저장 키 중복 불가 키 기반 조회

특히 Map은 Collection 인터페이스를 직접 상속하지 않는다는 점도 중요합니다. List와 Set은 “값들의 모음”이지만, Map은 “키와 값의 관계”를 다루는 별도의 계열입니다.

기본 사용법

put, get, remove, containsKey 같은 기본 연산만 알아도 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() : 전체 삭제

주요 구현체

Map이라도 성격이 완전히 다른 다양한 구현체가 있습니다.
구현체 특징 순서 보장 정렬 스레드 안전
HashMap 가장 많이 쓰는 기본 구현체 보장 안 함 안 함 아님
LinkedHashMap 삽입 순서 또는 접근 순서 유지 보장 안 함 아님
TreeMap 키 기준 정렬 유지 정렬 순서 아님
ConcurrentHashMap 동시성 환경용 보장 안 함 안 함 지원
Hashtable 구식 동기화 구현체 보장 안 함 안 함 지원
 

HashMap

도입자바에서 가장 널리 쓰이는 키-값 저장소이자, 빠른 조회를 가능하게 하는 대표 자료구조다자바에서 Map을 사용할 때 가장 먼저 떠올리는 구현체는 보통 HashMap입니다. 이유는 단순합니다. 대

develop-enchantment.tistory.com

 

2. LinkedHashMap

HashMap의 성격을 유지하면서도 삽입 순서를 보장합니다. LRU 캐시 같은 구조를 만들 때 접근 순서(access order) 옵션과 함께 자주 활용됩니다.

3. TreeMap

키를 자동 정렬된 상태로 유지합니다. 내부적으로 보통 레드-블랙 트리 기반으로 동작하며, 정렬된 순서가 중요한 경우에 유용합니다.

4. ConcurrentHashMap

여러 스레드가 동시에 Map을 다루는 환경에서 사용합니다. 단순히 HashMap을 공유하는 것보다 훨씬 안전하며, Hashtable보다 일반적으로 더 현대적인 선택입니다.

5. Hashtable

과거부터 존재하던 동기화 Map이지만, 지금은 보통 ConcurrentHashMap이 더 많이 권장됩니다.

내부 동작 원리

HashMap 계열은 해시값을 이용해 저장 위치를 빠르게 찾고, 충돌은 별도 구조로 처리한다

자바에서 가장 자주 쓰는 Map은 보통 HashMap입니다.

키 객체의 hashCode() 결과를 바탕으로 저장 위치를 계산하고, 그 위치에 값을 저장하거나 찾습니다.

  1. 키의 hashCode 계산
  2. 배열 인덱스 결정
  3. 해당 버킷(bucket) 확인
  4. 같은 키가 있으면 값 변경
  5. 없으면 새 엔트리 추가

만약 서로 다른 키가 같은 해시 위치로 들어오면 해시 충돌이 발생합니다. 이때 버킷 내부에서 추가 구조를 이용해 데이터를 관리합니다. 충돌이 많아지면 성능이 나빠질 수 있기 때문에, 키 객체의 equals()hashCode()를 올바르게 구현하는 것이 매우 중요합니다.

equals와 hashCode

Map에서 사용자 정의 객체를 키로 쓸 때는 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);
    }
}
실무 함정
Map 키로 mutable(가변) 객체를 쓰는 것은 매우 위험합니다. 저장 후 키 상태가 바뀌면 hashCode 결과가 달라져 값을 찾지 못할 수 있습니다.

순회 방법

keySet, values, entrySet을 통해 다양한 방식으로 순회할 수 있다
1. keySet 사용
for (String key : scores.keySet()) {
    System.out.println(key + " : " + scores.get(key));
}
2. values 사용
for (Integer value : scores.values()) {
    System.out.println(value);
}
3. entrySet 사용
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}

보통 키와 값을 함께 써야 한다면 entrySet() 순회가 가장 효율적이고 자연스럽습니다. keySet으로 순회하면서 get을 반복하는 방식은 불필요한 조회가 추가될 수 있습니다.

null 처리

Map 구현체마다 null 키와 null 값 허용 여부가 다르기 때문에 반드시 구분해서 써야 한다
구현체 null 키 null 값
HashMap 허용 허용
LinkedHashMap 허용 허용
TreeMap 일반적으로 허용 안 함 허용 가능
ConcurrentHashMap 허용 안 함 허용 안 함
Hashtable 허용 안 함 허용 안 함

특히 get(key)null을 반환했다고 해서 “키가 없음”인지 “값이 null임”인지 바로 단정하면 안 됩니다. 이런 경우에는 containsKey(key)로 함께 확인하는 습관이 좋습니다.

시간 복잡도

Map의 성능은 구현체에 따라 다르며, HashMap은 평균적으로 매우 빠르고 TreeMap은 정렬 대가로 log n을 사용한다
구현체 조회 삽입 삭제
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이 자연스러운 선택입니다.

실전 패턴

카운팅, 그룹핑, 캐싱, 인덱싱처럼 “기준으로 빠르게 찾는 작업”에서 압도적으로 자주 쓰인다
1. 빈도 수 계산
Map<String, Integer> countMap = new HashMap<>();

for (String word : words) {
    countMap.put(word, countMap.getOrDefault(word, 0) + 1);
}
2. 그룹핑
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);
}
3. 캐시 또는 인덱스

ID → 객체, URL → 응답, 코드 → 설정값처럼 “무언가를 기준으로 바로 찾아야 하는 경우”에는 거의 항상 Map이 등장합니다.

주의할 점

Map은 편하지만 키 설계와 구현체 선택을 잘못하면 성능과 안정성 문제가 생길 수 있다
  • 키 중복 덮어쓰기: 같은 키를 put하면 이전 값이 사라진다.
  • 키 객체의 불변성: mutable 객체를 키로 쓰면 위험하다.
  • equals/hashCode 불일치: 조회 실패나 중복 문제를 부를 수 있다.
  • 순서 오해: HashMap은 순서를 보장하지 않는다.
  • 멀티스레드 환경: 일반 HashMap을 공유하면 안전하지 않을 수 있다.
  • null 처리 혼동: get 결과가 null일 때 의미를 정확히 구분해야 한다.
GOOD ✅
대부분의 일반적인 경우는 Map 인터페이스로 선언하고 HashMap으로 시작하면 좋습니다. 순서, 정렬, 동시성이 필요할 때만 구현체를 바꾸는 식이 가장 실용적입니다.

디버깅

Map 관련 버그는 보통 키 설계, null 처리, 순서 오해, 동시성 문제에서 발생한다
1
같은 키로 put해서 값이 덮어써진 것은 아닌지 확인한다.
2
사용자 정의 키 객체라면 equals()hashCode()가 올바른지 확인한다.
3
get() 결과가 null일 때는 키 없음null 값을 구분한다.
4
순서를 기대하고 있다면 HashMap이 아니라 LinkedHashMap 또는 TreeMap이 필요한지 점검한다.
5
멀티스레드 환경이라면 ConcurrentHashMap 같은 동시성 대응 구현체가 필요한지 확인한다.

요약

Java의 Map은 키 기반 조회를 가능하게 하는 핵심 자료구조이며, 구현체 선택이 성능과 동작 방식을 결정한다
  • ✅ Map은 키와 값의 쌍을 저장하는 자료구조다.
  • ✅ 키는 중복될 수 없고, 같은 키로 put하면 값이 덮어써진다.
  • ✅ 일반적인 기본 선택은 HashMap이다.
  • ✅ 순서가 필요하면 LinkedHashMap, 정렬이 필요하면 TreeMap, 동시성이 필요하면 ConcurrentHashMap을 고려한다.
  • ✅ 사용자 정의 키를 쓸 때는 equals와 hashCode를 반드시 함께 설계해야 한다.
728x90