시작하기
해시(Hash)란 무엇인가요?
해시는 데이터를 빠르게 찾기 위한 기술입니다.
예시: 🏠아파트 우편함
1
2
3
4
5
6
| 아파트 우편함을 생각해보세요:
- 김철수(101호) → 1번 우편함
- 박영희(102호) → 2번 우편함
- 이민수(103호) → 3번 우편함
집 주소(키)를 보면 바로 우편함 번호(위치)를 알 수 있어요!
|
Java의 HashMap도 이와 똑같이 동작합니다.
왜 해시를 알아야 하나요?
실무에서 매일 사용하는 코드
1
2
3
| // 이런 코드를 자주 작성하지 않나요?
Map<String, User> userMap = new HashMap<>();
Set<String> uniqueIds = new HashSet<>();
|
🚀 성능의 차이
1
2
3
4
5
6
7
8
9
10
11
12
| // ArrayList로 사용자 찾기 (느림)
List<User> userList = new ArrayList<>();
for(
User user :userList){
if(user.getId().equals("user123")){
return user; // 최대 1000번 비교해야 함
}
}
// HashMap으로 사용자 찾기 (빠름)
Map<String, User> userMap = new HashMap<>();
User user = userMap.get("user123"); // 1번만에 찾음!
|
첫 번째 HashMap 사용하기
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class FirstHashMap {
public static void main(String[] args) {
// 1. HashMap 생성
Map<String, String> map = new HashMap<>();
// 2. 데이터 추가
map.put("apple", "사과");
map.put("banana", "바나나");
map.put("cherry", "체리");
// 3. 데이터 조회
String apple = map.get("apple");
System.out.println(apple); // "사과"
// 4. 데이터 존재 확인
if (map.containsKey("apple")) {
System.out.println("사과가 있어요!");
}
// 5. 모든 데이터 출력
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
}
|
핵심 개념
HashMap 기본 사용법
주요 메서드들
메서드 |
설명 |
예시 |
put(key, value) |
데이터 추가/수정 |
map.put("name", "김철수") |
get(key) |
데이터 조회 |
String name = map.get("name") |
remove(key) |
데이터 삭제 |
map.remove("name") |
containsKey(key) |
키 존재 확인 |
if (map.containsKey("name")) |
size() |
크기 확인 |
int size = map.size() |
isEmpty() |
비어있는지 확인 |
if (map.isEmpty()) |
실무 예시: 사용자 관리 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class UserManager {
private Map<String, User> users = new HashMap<>();
// 사용자 추가
public void addUser(User user) {
users.put(user.getId(), user);
}
// 사용자 조회
public User getUser(String userId) {
return users.get(userId);
}
// 사용자 존재 확인
public boolean hasUser(String userId) {
return users.containsKey(userId);
}
// 전체 사용자 수
public int getUserCount() {
return users.size();
}
}
|
hashCode()와 equals() - 가장 중요한 개념!
왜 중요한가요?
1
2
3
4
5
6
| // 이런 상황을 생각해보세요
Person person1 = new Person("김철수", 25);
Person person2 = new Person("김철수", 25);
// 같은 사람인데 다른 객체예요!
System.out.println(person1 == person2); // false
|
✅ 올바른 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
|
🧪 테스트해보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class Test {
public static void main(String[] args) {
Person person1 = new Person("김철수", 25);
Person person2 = new Person("김철수", 25);
// 이제 논리적으로 같은 객체로 인식됩니다
System.out.println(person1.equals(person2)); // true
System.out.println(person1.hashCode() == person2.hashCode()); // true
// HashMap에서도 같은 키로 인식됩니다
Map<Person, String> map = new HashMap<>();
map.put(person1, "첫 번째 사람");
System.out.println(map.get(person2)); // "첫 번째 사람"
}
}
|
자주 하는 실수들
🐛 실수 1: hashCode() 재정의 안 함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // ❌ 잘못된 예
public class BadPerson {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
// equals만 재정의하고 hashCode는 재정의 안 함
BadPerson other = (BadPerson) obj;
return name.equals(other.name) && age == other.age;
}
// hashCode() 재정의 안 함 - 큰 문제!
}
// 결과: HashMap에서 정상 동작 안 함
Set<BadPerson> set = new HashSet<>();
set.add(new BadPerson("김철수", 25));
set.add(new BadPerson("김철수", 25)); // 중복 저장됨!
System.out.println(set.size()); // 2 (잘못됨!)
|
🐛 실수 2: 가변 객체를 키로 사용
1
2
3
4
5
6
7
8
9
10
11
| // ❌ 위험한 예
List<String> mutableKey = new ArrayList<>();
mutableKey.add("key1");
Map<List<String>, String> map = new HashMap<>();
map.put(mutableKey, "value");
// 키를 변경하면 문제 발생!
mutableKey.add("key2"); // 키가 변경됨
String value = map.get(mutableKey); // null 반환! 찾을 수 없음
|
🐛 실수 3: null 처리 안 함
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ❌ 위험한 예
Map<String, User> userMap = new HashMap<>();
User user = userMap.get("unknown"); // null 반환 가능
user.getName(); // NullPointerException 발생!
// ✅ 안전한 예
User user = userMap.get("unknown");
if(user !=null){
System.out.println(user.getName());
}
// 또는
String name = userMap.getOrDefault("unknown", new User()).getName();
|
실무 활용
Map 인터페이스 vs 구현체
🎯 올바른 선언 방법
1
2
3
4
5
| // ✅ 권장: 인터페이스 타입으로 선언
Map<String, User> userMap = new HashMap<>();
// ❌ 비권장: 구현체 타입으로 선언
HashMap<String, User> userMap = new HashMap<>();
|
🤔 왜 인터페이스를 사용할까요?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class UserService {
// 인터페이스 사용으로 유연성 확보
private Map<String, User> users;
public UserService(boolean needsOrdering) {
if (needsOrdering) {
users = new LinkedHashMap<>(); // 순서 유지
} else {
users = new HashMap<>(); // 일반적인 경우
}
}
public void processUsers(Map<String, User> userMap) {
// HashMap, LinkedHashMap, TreeMap 모두 받을 수 있음
for (User user : userMap.values()) {
// 처리 로직
}
}
}
|
언제 어떤 Map을 사용할까요?
📊 Map 구현체 비교
구현체 |
특징 |
언제 사용? |
예시 |
HashMap |
가장 빠름 |
일반적인 경우 |
사용자 캐시 |
LinkedHashMap |
순서 유지 |
입력 순서 중요 |
설정 파일 |
TreeMap |
정렬됨 |
정렬 필요 |
학생 성적 순위 |
ConcurrentHashMap |
스레드 안전 |
멀티스레드 |
웹 애플리케이션 |
실무 사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 일반적인 캐시 - HashMap
Map<String, User> userCache = new HashMap<>();
// 입력 순서 유지 - LinkedHashMap
Map<String, String> configMap = new LinkedHashMap<>();
configMap.put("database.url","jdbc:mysql://localhost");
configMap.put("database.user","admin");
configMap.put("database.password","secret");
// 정렬 필요 - TreeMap
Map<String, Integer> scoreMap = new TreeMap<>();
scoreMap.put("김철수",95);
scoreMap.put("박영희",87);
scoreMap.put("이민수",92);
// 자동으로 이름 순으로 정렬됨
// 멀티스레드 환경 - ConcurrentHashMap
Map<String, User> threadSafeMap = new ConcurrentHashMap<>();
|
성능 최적화 기본
초기 용량 설정
1
2
3
4
5
6
7
8
| // ❌ 성능 문제 발생 가능
Map<String, User> map = new HashMap<>(); // 기본 용량 16
// 1000개 데이터 추가 시 여러 번 리사이징 발생
// ✅ 성능 최적화
int expectedSize = 1000;
Map<String, User> map = new HashMap<>(expectedSize * 4 / 3 + 1);
// 리사이징 없이 효율적으로 저장
|
📊 성능 측정 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class PerformanceTest {
public static void main(String[] args) {
// 작은 맵 vs 큰 맵 성능 비교
testMapPerformance(16); // 기본 크기
testMapPerformance(1000); // 최적화된 크기
}
private static void testMapPerformance(int initialCapacity) {
Map<String, Integer> map = new HashMap<>(initialCapacity);
long startTime = System.currentTimeMillis();
// 10,000개 데이터 추가
for (int i = 0; i < 10000; i++) {
map.put("key" + i, i);
}
long endTime = System.currentTimeMillis();
System.out.println("용량 " + initialCapacity + ": " + (endTime - startTime) + "ms");
}
}
|
심화 내용
내부 구조 이해하기
🏗️ HashMap은 어떻게 동작할까요?
1
2
3
4
5
6
7
8
| // 간단한 HashMap 동작 원리
Map<String, String> map = new HashMap<>();
map.put("apple","사과");
// 내부적으로 이런 과정을 거칩니다:
// 1. "apple".hashCode() 계산 → 예: 93029210
// 2. 배열 인덱스 계산 → 93029210 % 16 = 10
// 3. 10번 배열에 저장
|
🔄 해시 충돌이 발생하면?
1
2
3
4
5
6
| // 만약 두 키가 같은 위치에 저장되려 하면?
map.put("apple","사과"); // 10번 위치
map.put("grape","포도"); // 우연히 10번 위치
// Java 8 이전: 연결 리스트로 저장
// Java 8 이후: 8개 이상 쌓이면 트리로 변환 (더 빠름)
|
JVM 레벨 이해
JVM 전체 구조에서 HashMap의 위치
graph TB
subgraph "JVM 메모리 구조"
subgraph "힙 메모리 (Heap)"
A[HashMap 객체]
B[Node 배열]
C[실제 데이터들]
end
subgraph "스택 메모리 (Stack)"
D[지역변수들]
E[메서드 호출 정보]
end
subgraph "메서드 영역 (Method Area)"
F[HashMap 클래스 정보]
G[hashCode 메서드 바이트코드]
end
subgraph "PC 레지스터"
H[현재 실행중인 명령어]
end
end
D --> A
A --> B
B --> C
F --> A
H --> G
해시 계산 과정 (JVM 내부)
flowchart TD
A[자바 코드: map.get key] --> B[바이트코드로 변환]
B --> C[JVM이 바이트코드 실행]
C --> D[key.hashCode 호출]
D --> E{해시 코드 계산 방식}
E -->|String 등| F[자바 메서드 실행]
E -->|Object 기본| G[네이티브 메서드 호출]
F --> H[문자열 기반 계산]
G --> I[메모리 주소 기반 계산]
H --> J[해시 코드 반환]
I --> J
J --> K[배열 인덱스 계산]
K --> L[해당 위치에서 데이터 찾기]
L --> M[결과 반환]
HashMap 메모리 레이아웃
graph TB
subgraph "실제 힙 메모리 구조"
HM[HashMap 객체<br/>size: 3<br/>capacity: 16]
subgraph "Node 배열 (table)"
T0[0: null]
T1[1: Node-A]
T2[2: null]
T3[3: Node-B]
T4[4: null]
T5[5: Node-C]
T6[6: null]
T7[7: null]
end
subgraph "실제 Node 객체들"
NA[Node A<br/>key: apple<br/>value: 사과<br/>next: null]
NB[Node B<br/>key: banana<br/>value: 바나나<br/>next: null]
NC[Node C<br/>key: cherry<br/>value: 체리<br/>next: null]
end
end
HM --> T1
HM --> T3
HM --> T5
T1 --> NA
T3 --> NB
T5 --> NC
댓글남기기