
SOFTMOA TECHNOLOGY
Java HashMap get이 null일 때 equals와 hashCode 키 확인 방법
소프트모아가 정리한 기술 기록입니다.
Contents
재고 맵에 상품 코드를 넣고 같은 코드로 다시 조회했는데 get()이 null이면, 먼저 키 클래스의 hashCode()를 확인하세요. 원인은 대부분 둘 중 하나입니다. equals()만 재정의하고 hashCode()는 그대로 두었거나, 맵에 넣은 뒤 키 객체의 필드를 바꿨습니다. 이 글은 두 경우를 JDK 25(Temurin 25.0.4.1)에서 직접 실행한 결과로 구분하고, 고치는 방법을 정리합니다.
증상: equals는 true인데 조회는 null
아래 키는 상품 코드가 같으면 equals()가 true를 돌려주지만 hashCode()는 재정의하지 않았습니다.
static class NoHashKey {
final String code;
NoHashKey(String code) { this.code = code; }
@Override public boolean equals(Object o) {
return o instanceof NoHashKey k && code.equals(k.code);
}
}
NoHashKey a1 = new NoHashKey("A-100");
NoHashKey a2 = new NoHashKey("A-100");
Map<NoHashKey, Integer> m1 = new HashMap<>();
m1.put(a1, 30);
System.out.println(a1.equals(a2)); // true
System.out.println(m1.get(a2)); // null
System.out.println(m1.containsKey(a2)); // false
실행 결과는 equals=true, get(new key)=null, containsKey=false였습니다. 두 객체의 해시 코드는 각각 498931366과 2060468723으로 달랐습니다. 이 숫자는 객체마다 달라지는 값이라 실행 환경에 따라 바뀔 수 있습니다. 그래도 원리는 같습니다. HashMap은 해시 코드로 먼저 보관 위치를 고르고, 그 위치 안에서만 equals()로 비교합니다. 해시 코드가 다르면 다른 위치를 찾으니 같은 코드의 키를 만나지 못합니다.
Java 공식 문서의 Object.hashCode() 규약도 같은 내용을 요구합니다. equals()로 같다고 판단하는 두 객체는 hashCode() 결과도 같아야 합니다. 반대로 서로 다른 객체가 같은 해시 코드를 갖는 것은 허용됩니다. 그래서 이 오류는 "가끔 우연히 맞는" 형태로 나타날 수도 있습니다. 한 번 조회에 성공했다고 구현이 맞다는 증거가 되지 않습니다.
더 헷갈리는 경우: 넣을 때는 정상이었는데 나중에 사라짐
두 메서드를 모두 올바르게 재정의해도 키의 필드가 바뀌면 같은 문제가 생깁니다. 아래 키는 code로 equals()와 hashCode()를 만들었지만 필드를 나중에 수정할 수 있습니다.
static class MutableKey {
String code;
MutableKey(String code) { this.code = code; }
@Override public boolean equals(Object o) {
return o instanceof MutableKey k && code.equals(k.code);
}
@Override public int hashCode() { return code.hashCode(); }
}
MutableKey k = new MutableKey("A-100");
Map<MutableKey, Integer> m2 = new HashMap<>();
m2.put(k, 30);
System.out.println(m2.get(k)); // 30
k.code = "A-101"; // 맵에 들어간 뒤 키 수정
System.out.println(m2.get(k)); // null
System.out.println(m2.get(new MutableKey("A-100"))); // null
System.out.println(m2.size()); // 1
System.out.println(m2.remove(k)); // null
실제 출력은 수정 전 30, 수정 후 get(k)=null, 옛 코드 A-100으로 조회해도 null, 새 코드 A-101로 조회해도 null이었습니다. size()는 계속 1이고, remove(k)도 null이라 항목을 지우지 못했습니다. 그런데 entrySet()을 순회하면 키 A-101과 값 [30]이 보였습니다. 항목은 맵 안에 있지만 어떤 키로도 꺼낼 수 없는 상태입니다. 오류 메시지가 없어서 로그에는 단지 "재고 없음"처럼 보일 수 있습니다.
Java의 Map 문서는 가변 객체를 키로 쓸 때 equals() 비교에 영향을 주는 값을 바꾸면 맵의 동작이 지정되지 않는다고 설명합니다. "null이 나온다"는 이 실행에서 관찰한 한 가지 결과일 뿐이고, 항상 그렇게 동작한다는 약속이 아닙니다.
원인을 가르는 확인 순서
- 같은 값의 두 객체를 만들어
a.equals(b)와a.hashCode() == b.hashCode()를 함께 출력합니다. true와 false가 나오면 첫 번째 경우입니다. - 두 결과가 모두 true인데 조회가 실패하면, 키를 넣은 뒤 수정하는 코드가 있는지 찾습니다.
setCode같은 setter 호출, public 필드 대입, 키로 쓰는 엔티티를 화면에서 편집하는 코드가 후보입니다. - 맵 순회에서는 보이는데
get(),containsKey(),remove()가 모두 실패하면 두 번째 경우일 가능성이 높습니다.size()만 보고 "값이 없다"고 판단하지 마세요.
고치는 방법
가장 안전한 방법은 키를 바꿀 수 없게 만드는 것입니다. 상품 코드처럼 값 몇 개로 정해지는 키는 record로 충분합니다. 컴파일러가 선언한 컴포넌트로 equals()와 hashCode()를 함께 만들고 필드는 final이라 나중에 바꿀 수 없습니다.
record SkuKey(String code) {}
Map<SkuKey, Integer> m3 = new HashMap<>();
m3.put(new SkuKey("A-100"), 30);
System.out.println(m3.get(new SkuKey("A-100"))); // 30
실행 결과 get(new SkuKey)=30이었고 같은 값 두 객체의 해시 코드도 같았습니다. 단, record 컴포넌트에 가변 배열이나 가변 리스트를 넣으면 그 안쪽 값이 바뀔 수 있으니 키 컴포넌트는 문자열, 숫자, 날짜처럼 불변 값으로 제한하세요.
기존 클래스를 당장 바꿀 수 없다면 수정 전에 맵에서 제거하고 수정 후 다시 넣는 순서를 지킵니다. 이미 꼬인 맵은 새 맵으로 옮겨 복구할 수 있습니다. 아래는 키 수정 뒤에 new HashMap<>(m)으로 다시 만든 결과입니다.
Map<K, Integer> rebuilt = new HashMap<>(m);
rebuilt.get(k); // 30
rebuilt.get(new K("A-101")); // 30
rebuilt.size(); // 1
복사 전 맵은 여전히 get(k)가 null이었고, 복사한 새 맵에서는 30이 나왔습니다. 이 방법은 현재 필드 값 기준으로 위치를 다시 계산하는 복구일 뿐입니다. 서로 다른 두 키가 수정 후 같은 값이 되면 복사 중 한 항목이 다른 항목을 덮어쓸 수 있으므로, 복사 전에 수정 후 키의 중복 여부를 확인해야 합니다. 그리고 근본 원인인 키 수정 코드를 그대로 두면 같은 문제가 다시 생깁니다.
점검할 때 한계
- 위 출력은 한 번의 JDK 25 실행 결과입니다. 해시 숫자와 우연한 위치 충돌은 환경과 객체에 따라 달라지므로, 규약 위반 코드가 테스트에서 한 번 통과해도 안전하다고 볼 수 없습니다.
HashSet은 내부에서HashMap을 쓰므로 같은 키 규칙이 적용됩니다.TreeMap은 해시 대신 비교 규칙을 쓰니Comparable또는Comparator일관성을 따로 확인해야 합니다.- JPA 엔티티나 화면 입력 객체를 맵 키로 쓰는 코드는 id 생성 시점이나 폼 수정으로 해시 값이 바뀔 수 있어 별도 설계가 필요합니다.
소프트모아는 해당 시스템을 구축합니다. 문의하기
소프트모아의 서비스와 포트폴리오는 softmoa.com에서 확인할 수 있습니다.
소프트모아는 해당 시스템을 구축합니다. 문의하기
