우리가 만든 객체는 눈으로 보기엔 논리적으로 동등할지 몰라도 메커니즘 상 처음부터 서로 같은 객체는 없다.
생성 시 고유한 메모리 주소 값을 참조하는 객체로 자바에선 해시 코드값으로 가볍게 확인할 수 있다.
실제로 Hashcode와 equals를 사용해 동등하다고 판단하는 HashMap을 볼 수 있다.
또한 여러 객체들이 논리적으로 동등한 지 판단하여 분기하는 경우도 많다.
최근 나는 엔티티와 별도의 객체를 비교하는 일이 필요했다. 하지만 엔티티에 equals를 재정의하는 일은 너무 위험부담이 커서 추가 로직을 통해 처리하였다.
아마 이는 equals를 잘 정의하는 법을 몰랐기 때문이 아닐까?
그래서 오늘은 이펙티브 자바의 equals 관련 내용을 공부하고 자려고 한다.
equals 메서드에는 일반 규약이 존재한다.
1. A.equals(A) == true
2. A.equals(B) == B.equals(A)
둘 다 false 이거나 true이어야 한다. 이를 위반하는 상황은 보통 다음과 같은 상황이다.
equals를 재정의할 수 없는 클래스와 비교하는 경우, 커스텀 클래스에서는 재정의 할 수 없는 클래스와 동등하다고 코드를 작성하는 경우. 반대의 경우에는 false가 반드시 나온다. 이는 대칭성을 위배한다.
3. A.equals(B) && B.equals(C), A.equals(C)
해당 규칙이 깨지는 경우는 보통 상속관계에서 일어난다. 부모 타입 입장에서 자식 타입이 들어오면 true를 반환하지만
자식 타입 입장에서 부모 타입이 들어오는 경우 false를 반환한다.
이 경우 리스 코프 치환 원칙에 따라 주의해서 구현해야 한다.
자식 클래스는 상위 타입의 equals를 그대로 사용하게 두고 부모 클래스에서만 equals를 재정의해야 한다.
그런데 자식과 부모의 필드가 다른 경우에는 문제가 있다. 이 경우에는 원칙을 위반하지 않을 수 없다.
이 경우에는 상속 대신 하나의 필드로 사용한 별도의 클래스를 생성하는 걸 권장한다.
그리고 부모 타입 필드를 반환하는 포인트 뷰를 반환하면 된다.
상속을 하지 않아서 부모 타입 캐스팅은 불가능하기 때문에 필드의 포인트를 반환하여 비교를 진행하면 된다.
4. 불변 객체 인경우 A.equals(B) == A.equals(B)
값이 변경되는 가변 객체는 같을 수 없다. 이 경우는 너무 복잡하게 구현하면 안 된다.
5. null 확인
equals에 null을 넘겼을 땐 당연히 false가 나와야 한다.
이제 구현 시 4가지 규칙을 지켜서 처리하자.
1. == 연산자를 이용해 자기 자신의 참조인지 확인한다.
2. instanceof 연산자로 올바른 타입인지 확인한다.
3. 입력된 값을 올바른 타입으로 형 변환한다.
4. 입력 객체와 자기 자신의 대응되는 핵심 필드가 일치하는지 확인한다.
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AUpload)) {
return false;
}
AUpload aUpload = (AUpload) obj;
return aUpload.a == a && aUpload.b == b;
}
부동 소수점 비교는 각 클래스가 제공해주는 compare()를 가지고 비교하자.
부동 소수점이 아니고 기본 타입은 ==으로 비교해도 된다.
만약 null을 허용하는 경우에는 Objects.equals(null, null)을 사용하자. 이 메서드의 인자는 nullable이다.
이런 규약을 지키면서 구현하기가 너무 복잡하고 실수할 여지가 많다.
그래서 보통 자동생성 기능을 사용한다.
1. auto-value-annotations, auto-value 라이브러리를 사용
2. lombok
lombok이 아무리 갑론을박이 많아도 제일 사용할만하다.
3. record 사용.
public record Person(String name, int age) {
}
레코드 클래스를 사용하면 동일한 불변 데이터 객체를 쉽게 만들 수 있다.
- 이름(Person), 헤더(String name, int age), 바디({})
ㆍ컴파일러는 헤더를 통해 내부 필드를 추론
- 생성자를 작성하지 않아도 되고 toString, equals, hashCode 메서드에 대한 구현을 자동으로 제공
주의 사항
- equals를 재정의 할 때 hashCode도 반드시 재정의하자.
HashCode 규약
equals 비교에 사용하는 정보가 변경되지 않았다면 hashCode는 매번 같은 값을 리턴해야 한다.
두 객체에 대한 equals가 같다면 hashCode의 값도 같아야 한다.
두 객체에 대한 equals가 다르더라도 hashCode의 값은 같을 수 있지만 해시 테이블 성능을 고려해 다른 값을 리턴하는 것이 좋다.
자바 컬렉션에서 재공 하는 해시 맵 같은 경우 객체를 꺼낼 때 hash코드를 실행하여 버켓을 찾고 값을 가져온다.
만약 해시 키값이 고르게 분포되지 않고 중복된다면 어떻게 처리하여야 할까?
자바는 일정 개수 미만인 경우는 링크드 리스트를 통해 중복 데이터를 저장했다.
이는 키를 통해 접근하는 시간 O(1) + 링크드 리스트를 순회하여 값을 찾는 시간 O(N)을 소비한다.
이를 개선하고자 자바에서는 일정 개수 이상일 경우 이진트리 구조를 적용하여 O(logN)으로 순회 시간을 줄였다.
사실 근본적으로 해시 충돌이 발생하지 않는 것이 예방방법이다. 해시 코드는 이에 영향을 준다.
대게 해시 코드를 직접 정의하는 일은 극히 드물다. 이 방법이 생각난다면 잠시 접어두고 다른 방향으로 문제를 해결할 수 있지 않을까 고민해보아야 할 것 같다.
스레드 안전
멀티 스레드 환경에서 안전한 코드를 말한다.
이를 위해 우리는 스레드 간 공유하는 변수가 있는 경우를 대비해 synchronized 키워드를 통해 멀티 스레드를 제어한다.
대신 자주 호출되는 메서드 단위의 synchronized를 걸면 성능의 하락으로 이어질 수 있다. 그래서 우리는 최소한의 블록단위로 설정한다.
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
synchronized (this){
if (!(obj instanceof AUpload)) {
return false;
}
}
AUpload aUpload = (AUpload) obj;
return aUpload.a == a && aUpload.b == b;
}
또한 volatile 키워드로 메인 메모리에 저장해 캐시 된 값을 피할 수 있다.
그리고 트랜잭션 동기화 매니저가 관리하는 커넥션을 보관하는 ThreadLocal을 사용해도 되며 불변 객체인 경우 thread-safe 하다.
불변 객체는 언제 어디서든 동시에 접근하여 사용해도 불변하기 때문에 매우 안전하다.
Concurrent 데이터를 사용해도 된다. Concurrent는 동시 스레드가 접근해도 사용하는 걸 허용하는 개념이다.
'JAVA > [JAVA] 바구니' 카테고리의 다른 글
Increasing Code Cache (0) | 2023.02.20 |
---|---|
Comparable (0) | 2022.11.14 |
finalizer와 cleaner 사용을 피하라 (0) | 2022.11.07 |
Exception (0) | 2022.09.25 |
SSL/TLS 서버 통신 (JSSE, TrustManager) (1) | 2021.11.06 |
댓글