반응형

 

HashMap을 어느 정도 써봤다면 한 번쯤 이런 경험이 있을 겁니다.
“분명 put으로 값을 넣었는데, get을 하면 null이 나온다”, “같은 객체인데 key로 인식이 안 된다” 같은 상황 말이죠.

이 문제의 거의 대부분은 equals()hashCode()를 제대로 이해하지 못해서 발생합니다. 이 글에서는 HashMap에서 equals()와 hashCode()가 왜 중요한지, 그리고 실제로 어떤 문제가 주로 발생하는지 중심으로 정리해보겠습니다.

01. HashMap에서 Key는 어떻게 저장되고 찾을까?

HashMap은 단순히 key를 비교해서 값을 찾지 않습니다. 내부적으로는 다음 순서로 동작합니다.

  1. key의 hashCode()를 호출한다
  2. hashCode 값을 이용해 저장 위치(버킷)를 결정한다
  3. 같은 위치에 key가 여러개 있다면 equals()로 비교한다

따라서, HashMap에서 Key로 객체를 사용할 때는 hashCode()equals()가 반드시 함께 올바르게 구현되어야 합니다.

02. equals()만 구현하면 안 되는 이유

만약 equals()만 오버라이드하고 hashCode()는 그대로 두는 실수를 한다면 아래 예제와 같이 equals()는 true인데도 값이 나오지 않습니다. 왜냐하면 hashCode()가 다르기 때문입니다.

class Main {
    public static void main(String[] args) {
        HashMap<User, String> map = new HashMap<>();
        User u1 = new User("admin");
        User u2 = new User("admin");

        map.put(u1, "관리자");
        System.out.println(map.get(u2)); // 실행결과: null
    }
}

class User {
    String id;

    User(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }
}

03. equals()와 hashCode()의 계약(Contract)

자바에서는 다음 규칙을 반드시 지켜야 합니다.

  1. equals()가 true라면 hashCode()도 반드시 같아야 한다
  2. equals()가 false여도 hashCode()는 같을 수 있다

 

이제 위에서 언급한 HashMap의 내부동작을 좀 더 살펴보도록 하겠습니다.

HashMap은 두 객체가 서로 같은 객체인지 비교하기 위해 먼저 hashCode()를 실행해 해시코드값이 같은지를 비교합니다. 이때 해시코드값이 다르면 서로 다른 객체로 판단하고, 해시코드값이 같다면 equals()를 통해 다시 한번 두 객체가 같은 객체인지 비교합니다. 그리고 이 두 결과가 모두 맞을때만 서로 같은 객체로 판단합니다.

따라서, (hashCode로 먼저 위치를 찾기 때문에) hashCode가 다르면 equals를 비교할 기회조차 얻지 못하고 서로 다른 객체로 인식하게됩니다. 

💡 잠깐! 해시값이 같으면 무조건 같은 객체일까요? 아니요! 서로 다른 객체라 하더라도 같은 해시값을 갖게 될 수도 있습니다. 이를 해시충돌(Hash Collision)이라고 합니다. HashMap은 해시값이 같더라도 equals()를 통해 진짜 같은 객체인지 다시 확인합니다. 

04. 올바른 구현 방법

equals와 hashCode를 함께 구현한 예제입니다.

class Main {
    public static void main(String[] args) {
        HashMap<User, String> map = new HashMap<>();
        User u1 = new User("admin");
        User u2 = new User("admin");

        map.put(u1, "관리자");
        System.out.println(map.get(u2)); // 실행결과: 관리자
    }
}

class User {
    String id;

    User(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

💡equals에서 사용한 필드는 hashCode에서도 반드시 동일하게 사용해야 합니다. 그렇지 않으면 HashMap에서 정상적으로 동작하지 않습니다.

05. String은 왜 HashMap Key로 안전할까?

String이 HashMap의 key로 자주 쓰이는 이유는

  1. equals와 hashCode가 이미 올바르게 구현되어 있음
  2. 불변 객체(immutable)라 값이 변경되지 않음

반대로, 값이 변경되는 객체를 key로 사용하는 것은 매우 위험합니다. key의 상태가 바뀌면 hashCode도 바뀌어버리기 때문입니다.

User user = new User("admin");
map.put(user, "관리자");

user.id = "guest";

map.get(user); // null

06. 마무리: 주의사항!

  1. HashMap Key로 객체를 쓸 땐 equals()와 hashCode()는 세트로 구현해야합니다.
  2. equals()만 구현하면 100% 문제 발생합니다.
  3. 불변 객체가 가장 안전합니다.
  4. “put 했는데 get이 안되는 문제”의 대부분은 여기에서 시작됩니다.

HashMap 관련 버그의 상당수는 로직 문제가 아니라 equals()와 hashCode() 설계에서 시작됩니다.!

 

 

cf) HashMap 사용방법은 아래 링크의 글을 참고하세요

 

[Java] HashMap 사용방법과 꼭 기억해야 할 주의사항 (개념, 특징, 메소드 및 예제)

자바 개발에서 HashMap은 가장 기본이면서도, 실무에서는 의외로 많은 문제가 발생하는 컬렉션입니다.간단한 예제에서는 잘 동작하지만, 실제 서비스 코드에서는 “값이 왜 바뀌었는지 모르겠다

kadosholy.tistory.com

 

반응형

+ Recent posts