Collection Framework를 공부하면서 hashCode가 정확히 어떤식으로 사용되는지에 대해서도 알 수 있었다.
보통 Java에서 equals를 오버라이딩하면 hashCode 또한 오버라이딩 해야 한다고 알고 있는데, 왜 그래야 하는지 그리고 정말 꼭 필요한 것인지에 대해서 알아 보도록 하자.
Java 개발자라면 equals를 한 번쯤은 접해 봤을 것이다. 객체의 동등성을 비교할 때 많이 사용되곤 한다.
Java에서는 모든 원시타입을 제외한 데이터들은 객체이다. 각 객체들은 메모리에 할당되면 서로 다른 메모리 주소를 가지기 때문에 "==" 비교 연산자로는 같다는 결과가 나올 수가 없다. 하지만 개발 특성상, 물리적으로 같지는 않지만 논리적으로 같은 객체로 동작해야 하는 경우가 있을 수 있다. 예를 들면, 객체가 가진 특정 값이 같다면 또는 객체의 모든 프로퍼티 값이 같다면 같은 객체라고 판단하고 싶을때가 있다. 그런 경우의 수를 대비해서 equals라는 개념이 나온 것이다. equals 연산을 통해 true가 반환되면 서로 같은 객체로서 동작하도록 동등성 비교에 대한 여지를 만들어 준 것이다.
( equals 연산으로 비교되고 동작하는 코드내에서는 사실상 동일한 객체로 동작한다고 볼 수 있다. )
Java의 hashCode메소드는 Hash Table (Java의 Hash Table 구현체가 아닌 자료구조로서의)에서의 성능 보조를 위해 존재하는 개념이다. 예를 들어 HashMap이나 HashSet과 같은 자료 구조들이 이러한 HashTable구조를 사용한다.
HashTable은 배열을 사용하여 각각의 Key값에 해시 함수를 적용하고 배열의 index로서 사용한다. index를 활용하여 값을 저장하거나 조회하기 때문에 평균 시간복잡도는 O(1)이다.
실제로 개발자들의 의도와도 관련이 있다. equals를 오버라이딩 한다는 것은 해당 객체가 동일하진 않지만 마치 동일한 것처럼 동작하길 바라고 사용하는 것이다. 물론 실제로 동일해질 수는 없기 때문에 한계는 존재하지만, 객체 간에 "==" 연산을 사용할 일은 크게 많지 않다.
만약 개발자가 equals를 오버라이드하여 동일한 것처럼 동작하기를 바랬는데, 동일한 객체처럼 동작하지 않는다면 어떨까??
이를 이해하기 위해서는 hashCode가 어디서 쓰이고 어떻게 동작하는지 알 필요가 있다. 필자가 작성한 Collection Fremework 2 내용을 본다면 알 수 있겠지만, 간단하게나마 설명하겠다.
예를 들어 HashSet에 데이터를 저장한다고 가정해 보자. hashCode메소드는 내부적으로 정수형 index를 만드는 데에 사용된다. 서로 다른 객체의 hashCode메소드에서 반환되는 값이 같다면 만들어지는 정수형 인덱스도 같아지게 되고, 같은 인덱스 위치에 데이터가 저장이 된다.
( hashCode값이 다르더라도 같은 정수값이 반환되는 경우도 있는데 이를 해시 충돌이라고 한다. 자세한 내용은 Collection Framework 내용 참조 )
같은 인덱스 내에서는 equals 연산이 이루어지고 해당 인덱스에 같은 객체가 저장되어 있다는 결론이 나오면 해당 객체는 저장되지 않게 된다.
위와 같은 원리로 Set에는 중복 데이터가 저장되지 않도록 구현되어 있다. 동일한 값을 반환하는 hashCode메소드를 가져야, 동일한 인덱스에 접근하여 equals 연산을 수행할 것이다. 그래야만 데이터가 중복으로 저장되지 않을 수가 있다.
하지만, equals메소드만 오버라이드 하고 hashCode메소드를 오버라이드 하지 않는 경우, hashCode를 통해 만들어지는 정수형 index값이 서로 달라질 수 있고, 이런 경우 equals 메소드를 통해 같은 객체임을 판단하기 이전에 서로 다른 인덱스에 접근했기 때문에, 데이터가 저장되는 문제가 발생할 수 있다.
만약 개발자가 위와 같은 상황에서 equals를 오버라이드한 객체들은 중복으로 처리되어 HashSet에 저장되지 않았을 거라고 예상했다면, 이는 치명적인 문제일 수 있다.
hashCode를 오버라이드 하는 경우에는 equals를 함께 오버라이드할 필요가 없다. hashCode를 오버라이드 해서 같은 정수값을 반환하도록 만들더라도 결국은 내부적으로 equals 연산을 통해서 같은 객체인지 판단을 하는 과정을 거치기 때문에, equals에서만 동등 객체로 판단이 되지 않으면 중복이 허용될 일이 없기 때문이다.
다만 위에서 HashTable의 동작 원리를 보면 알겠지만, 같은 인덱스에 접근하는 경우의 수가 많아질 수록 데이터의 중복 확인 과정 등으로 인한 오버헤드가 더더욱 커진다. 그렇기 때문에 hashCode를 단독으로 오버라이드 하는 것은 문제가 되진 않지만 성능상 유리하진 않다.
우리가 Java 언어를 개발하는 개발자라고 가정해 보자. 결국 객체가 존재하는 Java라는 프로그래밍 언어 안에서, 객체들이 마치 동일한 것처럼 동작을 해야 하는 상황이 존재할 수밖에 없을 것이다.
우리는 그러한 상황을 위해 equals 메소드를 만들어서 동등성 비교가 가능하도록 여지를 주었다. 그러나 실제로 동일한 객체가 되는 것이 아니기 때문에, 그로 인한 사이드 이펙트가 발생할 수 있고, 실제로 HashMap이나 HashSet처럼 특정 고유 해시 인덱스를 사용하여 데이터에 접근하는 형태의 자료구조에서 그러한 상황이 발생했다. 이러한 문제점을 해결하기 위한 대안이 필요했고, hashCode메소드를 만들어 개발자가 직접 인덱스 생성에 관여할 수 있도록 만들었다.
( 실제 Java 개발자들의 의도는 모르겠지만, 이러한 의도가 아닐까 하는 개인적인 생각이다. )
실제로도 우리가 hashCode라는 메소드를 단독으로 오버라이드해서 사용하는 경우는 거의 없을 것이다. 사실 잘 생각해 보면 equals가 없었다면 hashCode라는 메소드도 굳이 필요하지 않았을 것이다. 즉, hashCode는 개발자가 동등성을 부여할 수 있는 자유도로 인한 문제점을 예방하기 위해서 나올 수밖에 없던 개념이라는 생각이 든다.
그러면 HashMap이나 HashSet도 안 쓰고, hashCode가 사용되지 않는 로직이라면 문제가 없는가??
개인적인 의견으로는 당장은 문제될 것이 없다고 생각한다. hashCode가 equals라는 개념으로 인한 사이드 이펙트를 막기 위한 보안장치와 같은 개념이라면 그러한 사이드 이펙트가 발생할 여지가 없는 경우 굳이 오버라이드가 필수라고 생각하지는 않는다. 하지만 나중에라도 로직은 언제든지 바뀔 수 있고, 오버라이드를 해 놓는 것이 추후에 발생할 사이드 이펙트를 미리 방지할 수 있다. 또한 Java진영에서는 equals를 오버라이드 하는 경우에는 항상 hashCode를 함께 오버라이드 하라고 강력하게 얘기하고 있다. 사실상 필수라고 얘기하고 있다.
결론은 equals를 오버라이드 하는 경우 사이드 이펙트 방지를 위해 hashCode도 항상 함께 오버라이드 하는 것이 가장 바람직하다고 생각한다.