사이드 프로젝트를 진행하면서 무심하게 Objects.equals() 메서드를 사용했었고 코드 리뷰를 받으면서 파라미터 두개의 타입이 Enum이면 == 비교 연산자를 사용하는게 어떤지 질문 받았다.
Objects.equals(newUser.getSocial(), existingUser.getSocial());
- 파라미터값 두개를 동시에 비교하려고 Objects.equals()를 무심하게 사용했었다.
NPE(NullPointerException)를 방지하는 목적으로 비교하기 위해서 사용했었는데 생각없이 사용한 자신을 반성하고 왜 == 비교 연산자에 대해서 이야기 하셨는지 Objects.equals(), enum의 equals(), == 비교 연산자에 대해서 Test 해보면서 알아보려고 한다.
테스트에서 사용하는 Enum
- static import해서 사용했습니다.
enum ObjsEquals {OBJECTS_EQUALS}
enum EnumEquals {ENUM_EQUALS}
👻 == 비교 연산자
주소에 접근해서 참조를 따라가는게 아니라 주소값 끼리 비교하기 때문에 NPE가 발생하지 않는다.
- Enum도 열거형 Class이기 때문에 주소값을 통해서 비교한다.
테스트
@Test
public void operatorComparisonTest() {
assertDoesNotThrow(() -> OBJECTS_EQUALS == OBJECTS_EQUALS); // true
assertDoesNotThrow(() -> OBJECTS_EQUALS == null); // false
assertDoesNotThrow(() -> null == OBJECTS_EQUALS); // false
assertDoesNotThrow(() -> null == null); // true
Object object = null;
assertDoesNotThrow(() -> ENUM_EQUALS == object); // false
assertDoesNotThrow(() -> object == OBJECTS_EQUALS); // false
assertDoesNotThrow(() -> ENUM_EQUALS == OBJECTS_EQUALS); // 컴파일 에러!!!
}
놓치고 있었던 부분은 NPE도 있었지만 추가로 컴파일 타임에 타입 미스 매치 잡아주는 것도 놓치고 있었다.
👻 Enum의 equals()
java.lang.Object
Object는 모든 클래스의 최고 조상 클래스로 모든 클래스들은 Object의 equals 메서드를 가지고 있고 재정의하면서 사용했었다.
java.lang.Enum
열거형 클래스는 기본적으로 추상 클래스이기는 하나 다른 클래스로부터 상속을 받지는 못한다.
- Object를 통해서 제공받았던 equals, hashCode등 final 키워드가 붙어있는 것을 볼 수 있다.
final 키워드가 붙어 있는 이유를 찾다가 마음에 와닿는 내용이 있어서 가져와 봤습니다.
return this == other는 직관적으로 알 수 있다.
- 두 열거형 상수는 equal을 통해서 동일한 객체인 경우에만 해당한다.
만약 final이 없어서 Override가 가능했다면 Enum의 장점들을 잃어버리게 됩니다.
- stackoverflow에 내용을 보면 Override가 가능했다면 "사용자나 동료가 예측 가능하게 하라" 라는 최소한의 놀라움 원칙(Principle of Least Astonishment)을 위반하게 된다고 하네요.
📌 결국 Enum의 equals()는 내부에서 == 연산자를 사용하고 있었습니다.
- 하지만 파라미터로 Object를 받고 있기 때문에 서로 다른 타입이라도 컴파일 에러가 발생하지 않습니다.
테스트
@Test
public void enumComparisonTest() {
EnumEquals enumEquals = null;
assertDoesNotThrow(() -> ENUM_EQUALS.equals(enumEquals)); // false
assertThrows(NullPointerException.class, () -> enumEquals.equals(ENUM_EQUALS));
//서로 다른 타입도 비교 가능
assertDoesNotThrow(() -> ENUM_EQUALS.equals(OBJECTS_EQUALS)); // false
}
주의점
Null 값을 통해서 equals를 사용하게 되면 쓰레기 값에서 참조로 들어가기 때문에 NPE가 발생하게 된다.
가끔 문자를 비교할 때도 Optional로 감싸서 보내기 애매하면 아래 코드처럼 NPE를 해결해도 좋을거 같다.
boolean isSameName(String name) {
return "현우".equals(name);
}
👻 Objects.equals()
java.util.Objects
내가 무심하게 사용하고 있었던 Objects.equals() 인데요 Enum에서 final 키워드가 붙은 장점을 느껴주세요.
- @Nullable을 통해서 null값을 허용하는 것을 볼 수 있었다.
- Object에 equals()처럼 서로 다른 타입이라도 컴파일 에러가 발생하지 않습니다.
파라미터 값 전부 null인 경우
- (a ==b)에서 null == null로 true 반환
두 개 중에서 한 개만 null인 경우
- a가 null ➡️ false 반환
- b가 null ➡️ a는 null 아니기 때문에 안전하게 a 객체의 equals()로 b를 검사하게 된다.
- a가 Enum이 아닌 경우 Object의 equals를 Override한 equals로 수행된다.
❗️ a가 Enum이 아니고 b가 null인 경우 주의할게 있다.
- 사용자가 equals 메서드 오버라이드를 재대로 해주지 않으면 NPE가 발생할 수 있다.
public static void main(String[] args) throws Exception {
EqualTest test1 = new EqualTest(1, 2);
System.out.println(Objects.equals(test1, null));
}
- b가 null로 들어가는 경우
❗️ Enum은 == 비교 연산자를 수행하기 때문에 NPE가 발생하지 않는다.
- abstract Enum에서 final 키워드 덕분에 equals()를 안전하게 사용할 수 있었습니다.
테스트
@Test
public void objectsComparisonTest() {
assertDoesNotThrow(() -> Objects.equals(OBJECTS_EQUALS, OBJECTS_EQUALS)); //true
//서로 다른 타입도 비교 가능
assertDoesNotThrow(() -> Objects.equals(OBJECTS_EQUALS, ENUM_EQUALS)); //false
assertDoesNotThrow(() -> Objects.equals(null, OBJECTS_EQUALS)); //false
assertDoesNotThrow(() -> Objects.equals(OBJECTS_EQUALS, null)); //false
assertDoesNotThrow(() -> Objects.equals(null, null)); //true
}
💬 결론
Null값이 들어올거 같은 것들은 Optional을 보통 사용했었는데 무심하게 놓친 내용들인거 같다.
결국 Enum도 equals를 사용하게 되면 결국 내부에서 == 비교 연산자를 통해서 주소 값을 비교하고 있었다.
그렇다면 컴파일 타임에 타입 미스 매치를 잡을 수 있고, NPE도 발생하지 않는 간단명료한 == 비교 연산자가 좋다고 생각한다.
물론 회사에 들어갔는데 코딩 컨벤션으로 Enum의 equals()를 쓰면 팀 내 표준을 따라가면 될거 같다.
코드리뷰 덕분에 돌아보는 시간을 가진거 같아서 감사합니다🤗
📚 Reference
'Java' 카테고리의 다른 글
default 메서드에서 왜 Obejct 메서드를 재정의하지 못할까? (0) | 2024.04.02 |
---|---|
JVM의 개념 및 구조(JDK, JRE, JIT 컴파일러) (0) | 2024.02.04 |
타입추론 var에 대해서 (0) | 2024.02.02 |
추상 클래스와 인터페이스 (0) | 2023.08.17 |