일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- Oracle 테이블 띄어쓰기
- 오라클 캐릭터셋 변경
- 윈도우 Oracle
- 무료 오라클 설치
- Oracle 18c 설치
- ORA-12899
- 서평단
- Oracle 초기 사용자
- oracle 18c
- Oracle 18c HR schema
- 비전공자를 위한 데이터베이스 입문
- 오라클 캐릭터셋 조회
- 무료 오라클 데이터베이스
- Oracle 사용자명 입력
- Oracle 윈도우 설치
- Oracle 18c HR
- Oracle Express Edition
- 오라클 캐릭터셋 확인
- ORA-00922
- Orace 18c
- Oracle 테이블 대소문자
- ora-01722
- Oracle 사용자명
- oracle
- Today
- Total
The Nirsa Way
[Effective Java 3/E - 모든 객체의 공통 메서드] Item 10-3. equals는 일반 규약을 지켜 재정의하라 - equals의 일반 규약 알아보기 본문
[Effective Java 3/E - 모든 객체의 공통 메서드] Item 10-3. equals는 일반 규약을 지켜 재정의하라 - equals의 일반 규약 알아보기
KoreaNirsa 2025. 7. 16. 12:46
equals는 일반 규약을 지켜 재정의하라 - equals의 일반 규약 알아보기
Object 명세에 작성된 내용을 확인해보면 equals 메서드는 널이아닌 객체들에 대해 동치 관계(equivalence relation)을 만족해야 하며 아래의 조건들을 지켜야 한다는 내용이 있습니다. 아래와 같이 크게 5가지로 이루어집니다.
- 반사성(Reflexivity)
- 대칭성(Symmetry)
- 추이성(Transitivity)
- 일관성(Consistency)
- non-null
5가지 규약들에 대하여 설명을 보면 은근히 헷갈리게 써있지만 예시 코드를 확인해 보시면 당연하다면 당연한 이야기들이라 이해하기 어렵지 않을 것 입니다. 이제 각 항목들에 대한 예시 코드를 작성해가며 확인해볼텐데, 예시 코드는 이전 포스팅인 [Effective Java 3/E - 모든 객체의 공통 메서드] Item 10-2. equals는 일반 규약을 지켜 재정의하라 - equals를 재정의해야 하는 상황과 인스터스 통제 클래스의 Money 클래스를 가지고 진행합니다.
1. 반사성(Reflexive) : 어떤 객체 x에 대해 x.equals(x)는 항상 true를 반환해야 한다.
public class Test {
public static void main(String[] args) {
Money x = new Money(1000, "KRW");
System.out.println("x.equals(x): " + x.equals(x)); // true
}
}
2. 대칭성(Symmetry) : 어떤 객체 x, y에 대해 x.equals(y)가 true라면, y.equals(x)도 반드시 true여야 한다.
public class Test {
public static void main(String[] args) {
Money x = new Money(1000, "KRW");
Money y = new Money(1000, "KRW");
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("y.equals(x): " + y.equals(x)); // true
}
}
단, 해당 기능을 구현할 때 주의해야할 점이 있습니다. 우선 아래의 예시 코드로 진행하도록 할텐데, equals()를 재정의 했으며 타입을 확인하고 equalsIgnoreCase 비교를 합니다. (문자열 일치 여부 확인 메서드) 정상적으로 보이지만 해당 클래스는 대치성을 만족하지 못하는 코드입니다.
이러한 이유는 타입 비교에 의해 결과가 달라집니다. CaseInsensitiveString → String 비교는 허용하지만, String → CaseInsensitiveString 비교는 허용하지 않습니다. CaseInsensitiveString 객체의 경우 우리가 재정의한 equals()에 의해 각 타입을 확인하고 equalsIgnoreCase를 호출하지만, String 객체의 경우 방금 CaseInsensitiveString 에서 재정의한 equals()가 아니라 String에 이미 구현되어있는 equals를 사용하므로 서로의 결과가 완전히 상반될 수 있습니다.
class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = s;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
if (obj instanceof String) {
return s.equalsIgnoreCase((String) obj);
}
return false;
}
}
이러한 경우 equals를 재정의할 때 같은 타입일 때(자기 타입끼리만) 비교할 수 있도록 아래와 같은 로직을 추가해주는 것이 좋습니다.
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof CaseInsensitiveString)) return false;
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
3. 추이성(Transitive) : 어떤 객체 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)가 true이면 x.equals(z)도 반드시 true여야 한다.
추이성을 만족시키는 코드는 아래와 같습니다. x=y이고 y=z이면 x=z이어야 한다는 내용입니다.
public class Test {
public static void main(String[] args) {
Money x = new Money(1000, "KRW");
Money y = new Money(1000, "KRW");
Money z = new Money(1000, "KRW");
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("y.equals(z): " + y.equals(z)); // true
System.out.println("x.equals(z): " + x.equals(z)); // true
}
}
하지만, 대칭성은 만족시키지만 추이성을 만족하지 못하는 아래와 같은 케이스가 있습니다. 아래와 같이 기본값(0)을 지정하여 기본값이 아닐 경우에만 반(classNumber)을 무시하고 이름으로만 비교합니다. 그 외에 classNumber가 기본값이 아닐 경우에는 반과 이름이 모두 일치하는지를 검사합니다.
public class Student {
private final String name;
private final int classNumber; // 0이면 미지정 상태
public Student(String name, int classNumber) {
this.name = name;
this.classNumber = classNumber;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Student)) return false;
Student other = (Student) obj;
// 이름이 같고, classNumber가 0이 아닌 경우만 비교
if (this.classNumber == 0 || other.classNumber == 0) {
// 반은 무시하고 이름만 비교
return this.name.equals(other.name);
}
// 반과 이름 모두 같아야 함
return this.name.equals(other.name) &&
this.classNumber == other.classNumber;
}
}
b는 반을 미지정하여 classNumber가 0이므로 이름이 같은지만을 판단하게 되는데, 이러한 이유로 a.equals(b)와 b.equals(c)는 true가 나와 대칭성을 만족하게 됩니다.
하지만 a.equals(c)를 하게 될 경우 반과 이름이 모두 일치해야 하므로 서로 이름은 일치하나, 반이 다르기 때문에 false가 나옵니다. 즉 현재 a=b 이고, b=c이지만 a=c가 아니게 되기 때문에 추이성을 만족하지 못하게 됩니다.
이렇게 equals()를 재정의하면서 개발자의 실수로 인해 특정 케이스에서 추이성이 위반될 수 있습니다.
public class Test {
public static void main(String[] args) {
Student a = new Student("Jisoo", 1);
Student b = new Student("Jisoo", 0); // 기본값: 반 미정
Student c = new Student("Jisoo", 2);
System.out.println("a.equals(b): " + a.equals(b)); // true
System.out.println("b.equals(c): " + b.equals(c)); // true
System.out.println("a.equals(c): " + a.equals(c)); // false
}
}
4. 일관성(Consistent) : 객체의 상태가 변하지 않는 한 x.equals(y)는 여러 번 호출해도 항상 같은 결과를 반환해야 한다.
객체의 상태 자체가 변하지 않는 한 for문이든 뭐든 여러번 호출해도 true였다면 항상 true, false였다면 항상 false가 나와야 한다는 규칙입니다.
public class Test {
public static void main(String[] args) {
Money x = new Money(1000, "KRW");\
Money different = new Money(500, "USD");
for (int i = 0; i < 5; i++) {
System.out.println("x.equals(x): " + x.equals(x)); // 항상 true
}
for (int i = 0; i < 5; i++) {
System.out.println("x.equals(different): " + x.equals(different)); // 항상 false
}
}
}
마찬가지로 이번에는 일관성이 위반되는 상황의 예시 코드를 확인해보겠습니다. 현재 코드는 시작이 짝수일때만 같고, 홀수일때는 같지 않은 비결정적 요소를 가지고 있습니다.
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
// 외부 조건: 현재 시각이 짝수 초일 때만 같다고 본다 → 비결정적 요소
long currentSecond = System.currentTimeMillis() / 1000;
if (currentSecond % 2 == 0) {
return this.name.equals(other.name);
} else {
return false;
}
}
@Override
public int hashCode() {
return name.hashCode();
}
}
그렇기에 p1.equals(p2)를 여러 번 호출하게 되면 시간에 따라 항상 같은 결과가 나오지 않기 때문에 일관성을 위반했다. 라고 볼 수 있습니다.
public class Test {
public static void main(String[] args) throws InterruptedException {
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
for (int i = 0; i < 5; i++) {
System.out.println("equals(): " + p1.equals(p2));
Thread.sleep(1000);
}
}
}
5. non-null : null이 아닌 객체 x에 대해 x.equals(null)은 항상 false를 반환해야 한다.
우선 아래의 코드를 살펴보면 null을 비교하므로 당연히 false가 출력됩니다.
public class Test {
public static void main(String[] args) {
Money x = new Money(1000, "KRW");
System.out.println("x.equals(null): " + x.equals(null)); // false
}
}
코드를 살펴보면 재정의된 equals()는 매개변수로 obj를 받으며 비교를 진행하게 됩니다. 하지만 null을 받았으므로 호출하게 되면 NullPointerException이 발생합니다. 즉 false가 반환되지 않았으므로 non-null을 위반하였습니다.
public class Test {
public static void main(String[] args) {
WeirdPerson p = new WeirdPerson("Alice");
System.out.println(p.equals(null)); // NPE 발생
}
}
public class WeirdPerson {
private final String name;
public WeirdPerson(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
return obj.toString().equals(name);
}
}
이러한 경우 아래와 같이 null 체크를 통해 NPE를 방지하여 non-null 규약을 어느정도 지킬 수 있습니다. (단, 해당 코드 자체는 규약 자체를 위반할 가능성이 있지만 예시 코드로써 작성하였습니다)
@Override
public boolean equals(Object object) {
if (object == null) return false;
return name.equals(object.toString());
}
마무리
살펴본것처럼 equals()를 재정의할 땐 규약을 지켜야 하며, 해당 규약들은 어렵지 않습니다. 하지만 개발자의 실수로부터 발생할 여지가 있으므로 주의하여 사용해야 합니다.