우아한테크코스 1주차 과정 중,
객체의 동일성과 동등성이라는 키워드를 접하게 되어 관련하여 학습한 뒤 포스팅하게 되었습니다.
📱📱 동일성과 동등성
자바에서 객체를 비교할 땐, 동일성과 동등성의 개념이 필요합니다.
동일성은 Identity 입니다. 메모리 내 주소값이 같은지 비교합니다.
동등성은 Equality 입니다. 논리적 지위가 동등한지 비교합니다.
모든 상세 스펙이 동일한 핸드폰 두 개가 있다고 가정해볼게요.
위 두 개의 핸드폰은 같은 핸드폰일까요?
상세 스펙이 똑같으니까 같은 핸드폰이라고 할 수도 있겠습니다.
그러나 한 편으로는 분명 두 개의 핸드폰이니
왼쪽 핸드폰과 오른쪽 핸드폰은 같은 핸드폰이 아니라고 할 수도 있을 것 같아요.
두 핸드폰은 동일하진 않습니다. 분명 두 개의 핸드폰이지요.
전자기기를 고유하게 식별하는 값인 Mac 주소를 확인하면 두 핸드폰이 분명 다른 걸 알 수 있을 겁니다.
그러나 두 핸드폰은 동등하다고 할 수 있습니다.
같은 스펙을 지녔고, 같은 가격에 거래되고, 같은 모델명을 지녔습니다.
일반적으로 같은 모델명, 같은 상세 스펙, 같은 가격에 거래된다면 같다 라는 개념을 사용할 수 있습니다.
Identity, 동일성은 == 비교를 통해 객체의 메모리 내 주소값이 같은지 식별합니다.
Id는 신분증이라는 의미로도 사용되고, 웹에서 사용자를 구분하는 계정명을 의미하기도 합니다.
한 사람 당 하나의 값을 갖게 되는, 식별값으로 비교해서 완전히 동일한 것인지 비교하는 것입니다.
Equality, 동등성은 equals 메소드를 통해 논리적으로 같은 지위를 지녔는지를 확인합니다.
이는 기준이 필요함을 의미합니다. 어떤 기준을 만족하면 동등하다고 말할지 말이죠.
이제 코드로 살펴볼까요?
🚗 equals() 메소드로 같은 객체인지 확인하기
같은 객체인지 확인하기 위한 과정에서 동일성, 동등성이라는 개념이 필요했어요.
메모리 내 주소값이 다른, 별개의 객체라 할지라도,
특정 조건을 만족하면 논리적으로 같은 객체로 다뤄야하는 경우가 있거든요.
그리고 그 특정 조건은, 매번 달라질 수 있어야 합니다.
public class Car {
private final String name;
private int position;
public Car(String name) {
this.name = name;
}
}
자동차 클래스를 하나 정의했습니다.
속성값으로는 이름과 위치를 갖고, 생성시에는 이름만 전달하면 됩니다.
요구사항으로 같은 이름의 자동차는 존재할 수 없다는 내용이 주어졌다고 한다면,
우리는 반대로 같은 이름을 지닌 자동차 두 객체가 있다면, 같은 자동차로 취급되어야 한다고 기대할 것입니다.
자동차는 primitive type이 아닌 reference type 이므로, equals() 메소드로 비교해보겠습니다.
@Test
void carEqualityTest() {
Car genesis1 = new Car("제네시스");
Car genesis2 = new Car("제네시스");
assertThat(genesis1).isEqualTo(genesis2);
}
//org.opentest4j.AssertionFailedError:
//expected: "Car{name='제네시스', position=0} (Car@10163d6)"
//but was: "Car{name='제네시스', position=0} (Car@49912c99)"
//Expected :Car{name='제네시스', position=0}
//Actual :Car{name='제네시스', position=0}
테스트가 실패했습니다.
Object.equals() 메소드는 == 비교를 통해 주소값 비교, 즉 동일성 검사를 수행한 결과를 반환하기 때문입니다.
new 로 두 번 생성했으니 당연히 메모리 내 서로 다른 주소값을 갖게 되고, 다르다고 식별되었습니다.
그럼 이 테스트 케이스를 통과하게 만들어볼까요?
public boolean equals(Object obj) {
return (this == obj);
}
위 코드는 Object 클래스의 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 name.equals(car.name);
}
Car 클래스에 equals 메소드를 오버라이드했습니다.
이제 Car 클래스의 인스턴스로 equals 메소드를 호출할 경우, 우리가 재정의한 코드가 수행될 것입니다.
먼저, 주소값이 같으면 동일성을 만족하니 바로 true를 early return합니다.
그 뒤, null 또는 같은 자료형이 아니면 false 를 early return합니다.
마지막으로 인자로 전달된 객체를 Car 클래스로 형변환 한 뒤, name 필드의 값이 동일한지 비교합니다.
이 equals 메소드는 객체의 동등성 검증에 사용됩니다.
논리적 동등성의 기준을 어떻게 정하느냐에 따라 개발자가 재정의해야 합니다.
이제는 테스트가 정상적으로 통과합니다.
🤔 hashCode() 도 함께 오버라이드 해야 하는 이유는?
equals 와 항상 같이 거론되는 hashCode 는 왜 함께 오버라이드 해야 할까요?
객체의 동등성 기준을 재정의해줌으로써,
같은 name을 가진 car 인스턴스는 equals 메소드를 통해 같은 객체로 인식되도록 구현하였습니다.
그렇다면, 이 car 인스턴스들을 Collection 에서 사용할 땐 어떨까요?
List, Map, Set 등의 Collection 에서 사용할 때에도 contains 등으로 동등성 비교가 가능할까요?
@Test
void carContainsTest() {
Car genesis1 = new Car("제네시스");
Car genesis2 = new Car("제네시스");
Set<Car> cars = new HashSet<>();
cars.add(genesis1);
cars.add(genesis2);
assertThat(cars.size()).isEqualTo(1);
}
//org.opentest4j.AssertionFailedError:
//expected: 1
//but was: 2
//Expected :1
//Actual :2
같은 이름을 가진 두 개의 Car 인스턴스를 생성하고,
Set에 담은 뒤 길이가 1인지 테스트해봤습니다.
결과는 실패입니다.
이렇게 되면, 중복을 허용하지 않는 Set의 자료구조를 이용할 수 없습니다.
즉, Car 인스턴스의 동등성이 보장되지 않는 상황인데요, 이는 Set이 Hash Table을 사용하는 자료형이기 때문입니다.
Hash Table을 사용하는 자료형에서는 어떤 데이터가 존재하는지 확인하기 위해 해싱 알고리즘을 사용합니다.
해싱된 결과를 주소값으로 찾아가서 그곳에 같은 자료가 있는지 확인하는 겁니다.
그 해싱 알고리즘에 사용되는 데이터가 바로 hashCode입니다.
Object 클래스의 hashCode는 native 메소드이고, 메모리 주소값을 정수로 변환한 값을 반환합니다.
지금은 두 Car 인스턴스의 주소값이 다르므로, 각 hashCode 메소드의 결과값이 다르게 반환될 거고,
그 결과 다른 공간에 저장되어 길이가 1이 아닌 2가 되는 것입니다.
따라서, 객체의 동등성을 위해 equals메소드를 오버라이드하는 경우,
hashCode메소드도 오버라이드 해야 합니다.
우리가 정하고자 하는 동등성의 기준값으로 hashCode를 생성하게 해야 합니다.
그래야 Hash Table을 사용하는 자료구조에서도 동등성을 보장할 수 있습니다.
@Override
public int hashCode() {
return Objects.hash(name);
}
equals에 이어서, hashCode를 추가로 오버라이드했습니다.
이제는 주소값이 다르더라도, name필드의 값이 동일하면 hashCode의 결과값이 동일할 수 있도록,
name필드 값을 해싱에 사용하도록 전달하는 모습입니다.
그리고, 테스트가 통과하네요 :)
요약
동일성, 동등성, equals, hashCode 에 대해 알아봤어요!
1. 동일성(Identity) 비교는 == 를 통해 메모리 내 주소값이 같은지 비교하는 것입니다.
2. 동등성(Equality) 비교는 equals() & hashCode() 를 통해 논리적 지위가 같은지 비교하는 것입니다.
3. 논리적 지위가 같다의 기준은 개발자가 요구사항에 맞게 오버라이드하여 재정의해야 합니다.
4. equals 메소드에서는 "두 객체가 같다" 의 기준이 될 필드들을 비교하도록 재정의합니다.
5. hashCode 메소드에서는 "두 객체가 같다" 의 기준이 될 필드들의 값으로 hashCode를 만들도록 재정의합니다.
여기까지 공부하고 나니, VO 클래스에 대해서는 equals, hashCode 오버라이드를 권장한다는 수업이 이해되었습니다.
또한, 김영한님의 JPA 수업 중, JPA가 == 비교, 동일성을 보장한다는 말씀이 어떤 의미인지 제대로 이해하게 되었습니다.
'우아한테크코스 4기' 카테고리의 다른 글
우아한테크코스 - 첫 팀 프로젝트 회고, 첫 화음 🎶 (4) | 2022.02.18 |
---|---|
Comparable vs Comparator (2) | 2022.02.17 |
우아한테크코스 1주차 - TDD 자동차 경주 게임, 페어 프로그래밍, 그리고... (2) | 2022.02.12 |
우아한테크코스 4기 오리엔테이션 (3) | 2022.02.09 |
우아한테크코스 웹 백엔드 4기, 화음을 좋아하는 리차드 (0) | 2022.01.20 |