개발자로 후회없는 삶 살기
[spring] equals()와 hashCode()를 재정의 해야하는 이유 본문
서론
웹 어플리케이션을 개발하면 롬복을 사용하게 됩니다. 이번에는 롬복이 @equalsAndHashcode를 제공하는 이유와 자바의 동일성, 동등성을 알아보겠습니다.
본론
- 동일성과 동등성
package hello.jdbc.domain;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
public class Member {
private String memberId;
private int money;
public Member(String memberId, int money) {
this.memberId = memberId;
this.money = money;
}
public Member(String memberId) {
this.memberId = memberId;
}
public Member() {
}
}
우리가 사용할 클래스는 다음과 같습니다.
1. 동일성
동일성은 두 객체가 완전히 같은 경우를 의미합니다. 여기서 완전히 같다는 뜻은 두 객체가 사실상 하나의 객체로 봐도 무방하며, 주소 값이 같기 때문에 두 변수가 같은 객체를 가리키게 됩니다.
Member member = new Member("id", 10);
Member member2 = member;
System.out.println(member == member2);
// 결과
true
위 예제에서 member2가 member을 가리키고 있어서 동일한 객체를 가리키므로 두 변수는 동일하다고 할 수 있습니다. 해당 변수가 동일한지는 '==' 연산자를 통해 판별할 수 있으며 동일성은 메모리 내 주소값이 같은지 비교합니다.
2. 동등성
Member member = new Member("id", 10);
Member member2 = new Member("id", 10);
System.out.println(member == member2);
System.out.println(member.equals(member2));
// 결과
false
false
동등성은 두 객체가 같은 정보를 가지고 있는 경우를 의미합니다. 객체의 주소가 서로 다르더라도 내용만 같으면 두 변수는 동등하다고 이야기할 수 있습니다. 동등하다고 동일한 것은 아니며 동등한 것은 equals 연산자를 통해 판별할 수 있습니다.
✅ equals()와 hashCode() 재정의
public boolean equals(Object obj) {
return (this == obj);
}
근데 위 결과를 보면 객체의 정보가 같은데 false가 결과로 나옵니다. Object 객체의 equals 메서드 내부를 보면 이렇게 '==' 연산자를 사용합니다. 보면 동등성을 검사할 때 사용하는 메서드라고 했는데 동일성을 검사하게 작성이 되어있습니다.
여기서 우리가 원하는 목적을 다시 생각해 봐야합니다. 우리는 equals로 동일성이 아닌 동등성을 만족하고 싶습니다. 동일성은 hashcode() 결과인 주소가 같아야 만족할 수 있는 것인데 equals로 '주소가 아닌 정보가 같으면 같은' 동등성을 만족하고 싶은 것입니다.
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Car car = (Car)o;
return memberId.equals(member.memberId) && money.equals(member.money);
}
이렇게 equals가 동등성을 만족하도록 재정의하였습니다. 먼저 주소값이 같으면 동일성을 만족하여 true를 하고 마지막으로 id 필드의 값과 money 필드의 값이 같은지 비교합니다. 이 equals 메서드는 이제 객체의 동등성 검증에 사용됩니다. equals 메서드는 처음에 동일성 검증을 하는데 재정의하여 동등성 검증을 하게 됩니다.
-> hashcode()
해쉬코드 메서드는 객체의 주소값을 반환하는 메서드입니다. 동등성을 만족하려면 해쉬코드 메서드도 재정의를 해야합니다. 다시 우리가 원하는 목적을 생각해 보겠습니다. 우리는 객체의 정보가 같으면 동등성을 만족하게 하고 싶습니다. 위에서 equals 메서드를 재정의하여 동등성을 체크하게 하였습니다.
Member member = new Member("id", 10);
Member member2 = new Member("id", 10);
Set<Member> members = new HashSet<>();
members.add(member);
members.add(member2);
System.out.println(members.size() == 1);
// 결과
false
근데 여기에는 추가적으로 해줘야 하는 것이 있습니다. 코드를 실행해보면 size가 1이 나와야 하는데 실패합니다. 그 이유는 hashtable를 사용하는 자료형이기 때문인데 해시 테이블은 주소값이 같아야 같은 자료로 봅니다. 따라서 두 회원의 주소가 달라서 다른 객체로 보게 됩니다.
우리의 목적은 id와 money가 같으면 다른 주소라도 동등성을 만족하게 하고 싶은 것입니다. 그래서 hashcode를 우리가 원하는 모양으로 재정의 해야합니다.
@Override
public int hashCode() {
return Objects.hash(memberId) + Objects.hash(money);
}
기존 hashcode 메서드는 그냥 주소만 반환합니다. 이렇게 하면 주소값이 다르더라도 id와 money 필드의 값이 동일하면 hashcode의 결과값이 동일합니다. hashcode를 재정의하는 이유는 hashtable을 쓰는 자료구조에서도 동등성을 보장하기 위해서입니다. 원래 hashcode는 그냥 주소를 반환하는 메서드라서 hashtable을 쓰는 자료구조에서는 동등성을 보장할 수 없는데 이렇게 재정의하면 보장합니다.
- 롬복 equalsAndHashcode() 재정의
equalsAndHashcode() 재정의를 하는 이유는 필드값이 같은 객체는 hashtable을 쓰든 안 쓰든 동등성을 보장하기 위함입니다. 우리의 목적을 결론적으로 보면 equal와 hashcode를 둘 다 재정의 해야 하는 이유는 어떠한 상황에서도 동등성을 만족하게 하기 위함이었습니다. 롬복은 equals와 hashcode를 동일성이 아닌 동등성을 만족하도록 자동으로 재정의해줍니다.
해쉬코드 재정의 전에는 주소값이 나오는데 재정의 하면 주소값이 좀 다르게 나와서 필드 값이 같으면 동등성을 만족하고 해쉬 테이블을 쓰는 자료구조에서도 동등할 것입니다.
클래스 타입이 다르면 해쉬코드를 재정의해도 당연히 주소가 다르게 나옵니다.
hashcode를 재정의하면 여러 개의 필드가 모두 같은 값을 가져야 같다고 판단합니다.
- 롬복은 어캐 재정의 했나?
롬복은 어떠한 상황에서도 동등성을 만족하도록 equals와 hashcode를 재정의 했습니다. equals는 주소값이 같은지 확인하고 필드값이 같은지 순차적으로 확인하도록 재정의하고 hashcode는 objects.hash() 메서드를 return 하도록 재정의했습니다.
※ Objects.hash(Object... values) 메서드 : 매개 값으로 주어진 값들을 이용해서 해시 코드를 생성하는 역할로 필드에 초기화된 값이 같으면 같은 값을 리턴합니다. 그래서 같은 값을 초기화하고 해시코드를 재정의하면 같은 해시코드값을 가집니다.
결론
동등성과 동일성을 알아보는 시간을 가졌습니다. 개발할 때 같은 정보를 가지는 객체에 equals 메서드를 썼는데도 true 결과를 내지 않는 경우 hashcode와 hashtable를 생각해야 합니다.
'[백엔드] > [spring+JPA | 이슈해결]' 카테고리의 다른 글
spring PART.Value Object와 Custom Validator를 이용한 검증 개선 (0) | 2023.07.21 |
---|---|
[Java] Java의 immutable (0) | 2023.07.07 |
(작성중) spring PART.로컬 호스트에서 spring 서버와 flask 서버 통신하기 (0) | 2023.06.16 |
spring PART.JPA 사용하지 않고 enum 타입 DB에 저장하기 (0) | 2023.06.14 |
[spring] @NotNull, @NotEmpty, @NotBlank의 차이점 (1) | 2023.04.30 |