개발하다가 코드 리뷰를 받으면서 들었던 의문점
엔티티 필드에 값 객체가 존재하고, 외부에서 값 객체가 주어질 때, 해당 값 객체의 값과 엔티티가 갖고있는 값 객체의 값이 같은지 판별해야 하는 메서드를 작성할 일이 생겼다.
나는 단순하게 값 객체의 필드를 하나 하나 꺼내서 비교하는 메서드를 그냥 작성했는데, 리뷰로는 `equals()` 메서드를 사용하면 된다는 리뷰를 받았다.
코드를 작성할 땐 막연히 `equals()를 사용하면 참조비교하겠지?` 정도로만 생각해서 하나씩 세부 필드를 꺼내서 비교하는 코드를 작성했는데, 그 차이점을 직접 비교해보려고 한다.
GPT 가 알려준 것
GPT는 값 객체를 비교할 때 equals 메서드를 오버라이딩 하는 것을 추천하고 있다.
구글링 해봤을 때도 다른 블로그들이 모두 비슷하게 말하고 있었다.
기본적으로 equals 메서드는 Object 의 구현을 따라 동일성을 판별한다고 한다.
동일성은 정말 두 객체가 '동일한 객체인지' 판별하는 것으로, 객체의 교유 해시값을 비교한다.
따라서 만약 equals 메서드의 호출 결과가 true 라면 두 객체의 해시코드 값은 같다.
자바 공식문서를 보면 정말 그렇다고 한다.
코드로 확인하기
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public static class Semester {
private Integer year;
private SemesterType type;
public static Semester of(Integer year, SemesterType type) {
return new Semester(year, type);
}
}
public enum SemesterType {
FIRST, SECOND
}
위와 같은 값 객체를 만들었다.
혹시 jpa 의 @Embeddable 어노테이션이 비교에 영향을 줄까 싶어 함께 사용했는데, 영향은 따로 주지 않았다.
@Test
void testObject() {
Semester semester1 = new Semester(2024, SemesterType.FIRST);
Semester semester2 = new Semester(2024, SemesterType.FIRST);
Assertions.assertThat(semester1.equals(semester2)).isTrue();
}
그리고 테스트 코드를 작성했다.
내가 원하는 것은 값이 같다면 객체가 달라도 괜찮은 '동등성' 비교이다.
테스트는 실패한다.
따라서 기본적으로 equals() 메서드는 객체의 '동일성' 을 판별하는 메서드임을 알 수 있다.
그래서 GPT는 equals() 메서드를 오버라이딩 하라고 한 것이다.
@Test
void testObject2() {
Semester semester1 = new Semester(2024, SemesterType.FIRST);
Semester semester2 = new Semester(2024, SemesterType.FIRST);
Assertions.assertThat(semester1 == semester2).isTrue();
}
이번엔 == 연산자를 사용해서 비교해보았다.
객체에 대해 == 연산자를 사용한 비교를 적용하니 인텔리제이에서 경고문구와 함께 equals() 메서드를 사용하라고 했다.
실행결과는 False 가 나오면서 테스트가 역시 실패했다.
즉, equals() 와 == 연산자 모두 객체에 대해서는 해시코드를 비교하는 '동일성' 비교를 수행한다.
그렇다면 '동등성' 비교를 위해서 값을 하나하나 비교하는 메서드를 오버라이딩 해야 할까?
다행히 롬복의 @EqualsAndHashCode 어노테이션을 이용하면 간단하게 해결할 수 있었다.
그리고 equals() 메서드를 오버라이딩 할 때는 객체의 해시값을 계산하는 hashcode() 메서드 역시 함께 오버라이딩 하는 것을 권장하고 있는데, 이 역시 함께 해결된다.
equals() 메서드 API 문서를 보면 hashCode 메서드도 함께 오버라이딩할 것을 권고하고 있다.
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@AllArgsConstructor
public static class Semester {
private Integer year;
private SemesterType type;
public static Semester of(Integer year, SemesterType type) {
return new Semester(year, type);
}
}
이렇게 롬복 어노테이션을 통해 equals 메서드를 오버라이딩 하였다.
기존의 equals 메서드를 사용한 비교 테스트가 통과하는 것을 알 수 있다.
하지만 여전히 == 연산자를 사용한 비교 테스트는 실패한다.
== 연산자는 자바의 기본 연산자로서 그 동작 자체가 이미 객체의 해시값 비교로 이루어져 있어 오버라이딩에 영향을 받지 않기 때문이다.
그래서 누군가 equals() 메서드와 == 연산자의 차이를 묻는다면 '기본적으로 두 기능의 동작은 같지만, equals 메서드는 오버라이딩하여 그 동작을 재정의할 수 있는 반면, == 연산자는 재정의할 수 없다' 고 말하면 될 것 같다.
'연산자 오버로딩'과 '오버라이딩 vs 오버로딩'
그런데 여기까지 왔더니 다시 떠오른 의문점 하나
c++은 기본 연산자 역시 '연산자 재정의' 가 가능한데, 과연 자바도 가능할까?
그리고 여기에서 나온김에 항상 헷갈리는 개념을 다시 정리하자면.
연산자 재정의는 연산자 '오버로딩' 이라고 한다.
오버라이딩 = 부모 클래스의 동작을 자식 클래스에서 재정의 하는 것 (상속 개념과 연관)
오버로딩 = 하나의 클래스에서 같은 이름의 메서드를 다른 형태로 여러 개 정의하는 것 (상속 개념과 무관)
그렇다면 왜 연산자는 오버로딩인가?
내가 이해한 것은 c++ 에서 + 연산이 자바와 같이 기본 Object 클래스에 구현되어 있는데, 이를 재정의하고 하는 것이 아니라, 애초부터 객체에 대해서는 + 연산이 존재하지 않았는데, 연산자 정의를 통해서 신규로 생성하며 확장하는 느낌이기 때문에 오버로딩이라고 부른다고 이해했다.
결론
equals(), == 연산 모두 기본적으로는 객체의 해시값을 비교해서 '동일성' 을 체크한다.
다만 equals() 메서드를 오버라이딩 해서 '동등성' 을 체크하도록 변경할 수 있으며, 이 변경은 == 연산에는 영향을 주지 않는다.
'WEB(BE) > Spring & Spring Boot' 카테고리의 다른 글
[Swagger] Failed to load API definition (403, 500, NoSuchMethodError) (1) | 2025.01.22 |
---|---|
[Spring Boot] application.yml 데이터베이스 연결 정보 입력 (0) | 2025.01.06 |
[Spring Boot] profile 개념과 profile 분리 (0) | 2025.01.04 |