brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Mar 27. 2022

소소한 백엔드 개발 이야기 97

001. Java equals(), hashCode()

"소소한 백엔드 개발 이야기 97" 이라는 주제로, 백엔드 위주의 잡다하고 가벼운 글을 써보려고 합니다. 매주 주말 1편씩 발행 목표로 시작하였지만, 생업으로 인해서 몇주 늦어질 수 있습니다. 시니어 개발자에게는 너무 쉬운 내용이며, 주니어 개발자에게 적합할 것입니다. 


가벼운 마음으로 편하게 읽어주시면 됩니다. 잘못된 내용은 댓글로 제보해주세요!!


목차 


001. [이번글] Java equals(), hashCode()

002. 의존성 주입

003. 캐시

004. 미정

... 

100. 미정



첫번째 글의 제목은 "Java equals(), hashCode()" 이다. 제목을 보고 많은 개발자가 바로 예상했겠지만, "이펙티브 자바"의 11번째 아이템인 "equals 를 재정의하려거든 hashCode 도 재정의하라"라는 글과 매우 유사한 내용이다. 필자가 일요일 오후에 아주 급하게 두세시간만에 작성한 글이라서 내용이 좀 많이 허접할 것이다. 필자의 글이 이해가 되지 않는다면, 이펙티브 자바 를 반드시 읽어보길 바란다. 




Object class equals(), hashCode()


name 이라는 String 타입 필드를 갖는 Snow 클래스를 정의해보자. 생성자와 게터 메서드만 있는 아주 간단한 클래스이다. equals 를 재정의하지 않은 상황이다. 

"eddy"라는 이름의 Snow 인스턴스 객체를 두 개 생성해보자.

Snow 클래스에서 equals 를 재정의하지 않았다면, 최상위 클래스인 Object 의 equals 를 사용하게 된다.

equals 에서는 객체의 주소값을 비교하게 되는데, new Snow("이름")으로 생성된 객체들은 힙메모리내에서 서로 다른 주소값을 갖는다.  그래서, equals 메서드에서 false 를 반환하게 된다. 서로 다른 두 객체는, eddy 라는 같은 이름의 속성을 갖지만, 서로 다른 주소값을 갖고 있기 때문에, equals 메서드에서 false 를 반환하게 되는 것이다. 




Object 클래스의 hashCode() 메서드는 객체의 hashcode 를 반환하는데, 객체의 주소값을 변환해서 int 타입으로 반환한다. 객체의 동일성을 비교하기 위해 사용되며, 네이티브 메서드이다. 

위 샘플 코드에서는, a 와 b 는 힙메모리내에서 서로 다른 주소값을 가지므로, hashCode() 메서드 실행 시 서로 다른 해시값을 리턴한다.




String class equals(), hashCode()


이번에는 Java String 클래스의 equals, hashCode 메서드에 대해서 알아보자. String 객체를 만드는 방법은 두가지이다. 


- new String("문자열")  --> 예) String a = new String("eddy");

- 리터럴 방식                --> 예) String a = "eddy";


new Snow 클래스로 같은 이름을 갖는 두개의 인스턴스 객체는, 서로 다른 주소값을 갖고, equals 메서드에서 false 를 리턴하였다. 마찬가지로 new String 클래스로 동일한 문자열을 생성했을 때 equals 메서드는 false 를 리턴할까? 그렇지 않다. true 를 반환한다.  샘플 코드를 보면서 이해해보자. eddy 라는 문자열을 갖는 4개의 String 객체를 생성하였다. a 와 b 는 new String 으로 생성하였고, c 와 d 는 리터럴 방식으로 생성하였다. 

new String("eddy") 으로 생성한 모든 객체와, 리터럴 방식으로 생성한 모든 객체 즉 a, b, c, d 모두 equals 에서 true 를 반환한다. Object 클래스의 hashCode는 주소값을 리턴하였지만, String 클래스의 hashCode 는 주소값을 리턴하지 않는다. 실제 주소값은 identityHashCode 로 확인 가능한데, a 와 b 가 서로 다른 것을 확인할 있다. 즉, a 와 b 는 힙메모리안에서 서로 다른 영역에 저장이 되는 것이다. Object 클래스의 equals 메서드와는 다르게, String 클래스의 equals 메서드는 false 가 아닌, true 를 반환하는데, String 클래스의 equals 는 단순히 주소값을 비교하는게 아니라, 실제 문자열을 비교해서 true,false 를 판단해서 리턴하도록 구현이 되어있다. 


즉, String 클래스는 Object 클래스의 equals 메서드를 그대로 사용하지 않고, String 클래스의 특성에 맞게 재정의해서 사용한다는 것을 알 수 있다. 


참고로, jdk 11 소스를 확인하였다. jdk 8 또는 11 상위 버전에서는 소스가 다를 수 있다는 점 이해해주길 바란다.


String hashCode 메서드에 대해서도 알아보자. a 클래스와 b 클래스는 별도의 주소값을 갖지만, 실제로 hashCode 는 전부 같은 값을 반환한다. String 클래스의 hashCode() 메서드 역시 마찬가지로 Object 클래스의 hashCode() 메서드를 그대로사용하지 않고 재정의 되어있는데, 자바8 과 자바11 에서 조금 다르게 구현되었지만, 내용은 비슷하다.


어라? 근데 리터럴 방식으로 생성한 c 와 d 는 identityHashCode 메서드에서 확인해보니 같은 주소값을 갖는다. 즉, 힙메모리 안에서 같은 영역에 저장이 되는 것이다. 사실, String 생성 시 리터럴로 생성하는 경우에는, String Pool 이란 곳에 저장이 된다. 메모리 효율 측면에서는, new String 객체로 생성하는 것보다는, 리터럴 방식으로 생성하는게 효과적일수는 있다. 



override equals()


자... 이제 Snow 클래스에서 equals 메서드를 재정의해보자. 


Snow 클래스의 equals 메서드에서는 String 타입의 equals 를 사용하도록 재정의하였다. String 클래스의 equals 메서드는 주소값을 비교하지 않고, 문자열을 비교해서 같으면 true 를 반환하므로, Snow 클래스의 equals 메서드는 String 타입의 name필드의 실제 문자열을 비교하게 된다. 비록 new Snow("eddy") 로 생성한 이름은 갖지만, 서로 다른 두개의 객체는, 힙메모리에서 서로 다른 주소값을 갖지만... 위와 같이 재정의하면 Snow 클래스로 생성한 두객체는, name 이 같으면 논리적으로 같다고 판단하는 것이다. 물론, 두 객체는 물리적으로 서로 다른 힙메모리에 저장이 될 것이다. 

샘플 코드를 실행하니, equals 메서드는 true 를 반환하였다. 하지만, 무언가 좀 이상하다. Set 자료구조의 사이즈가 여전히 2 이다. Set 자료구조는 값이 중복될 수 없다. new Snow 는 논리적으로 서로 같은 객체로 판단하도록 equals 를 재정의하였지만, "eddy"라는 이름의 Snow 객체가 두개 저장되어있다. 

equals 메서드를 재정의해서 논리적으로 name 필드가 같으면 같은 객체라고 정의하였기 때문에, 1개의 객체가 저장되는 것을 기대했지만, 실제로는 HashSet 자료구조에서는 2개의 객체가 저장이 되었다. 


Hash 를 사용하는 자바 자료구조는 (HashMap, HashSet, HashTable 등) 객체가 논리적으로 같은지 비교할 때 다음과 같다. 


필자는 Snow 클래스에서 equals 메서드만 재정의하였고, hashCode() 메서드는 재정의하지 않았기 때문에, Object 클래스의 hashCode() 메서드를 사용하게 된다. 즉, Object 클래스는 hashCode 메서드에서 객체의 주소값을 반환하기 때문에, 같은 이름으로 생성한 new Snow 객체 2개는 서로 다른 주소에 저장되므로, hashCode 값 역시 서로 다르다. 



override hashCode()


Snow 클래스의 hashCode메서드를 재정의하자. name String 타입의 name 필드의 해시코드를 반환하도록 재정의하였다. 문자열이 같다면 hashCode 는 모두 동일하다.


테스트 해보자. 아래와 같다. 


자세한 설명은 생략한다.



String hashCode() 이상한 점...


new String("문자열") 로 생성한 객체는, 문자열이 다르면 항상 다른 hashCode 를 반환한다고 설명하였다. 하지만, 문자열이 다름에도, 같은 hashCode 를 반환하는 경우가 존재한다. 


확률적으로 매우 드문 케이스이다. 참 골때린다. 필자는 개발하면서 해당 이슈로 인해서 그동안 크게 문제될일은 없었지만, 그래도 자바 개발자는 알고는 있는게 좋겠다. 


그럼 혹시 Snow 클래스에 name 이름을 Aa, BB 로 지정한 두개의 객체는 HashSet 자료구조에 저장할 때 서로 다른 객체로 저장이 될까? 잘 된다. 비록, String 타입의 name 필드의 hashCode는 같지만, equals 는 false 라서 다행히도 서로 다른 객체로 저장이 잘 될 것이다. 



글을 마무리하면서


빠르게 작성한 글이라서, 설명이 좀 친절하지 못했다. 나머지 내용은 "이펙티브 자바 아이템11"을 읽어보길 바란다. 



"소소한 백엔드 개발 이야기 100" 의 두번째 글은 "의존성 주입" 이라는 주제로 얘기를 해보겠다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari