본문 바로가기

전체보기/Spring

상속과 @Data의 warning, 그리고 @EqualsAndHashCode

이번 글에서는 상속 구조에서 @Data를 사용할 때,

발생되는 warning과 그 원인, 그리고 해당 내용과 @EqualsAndHashCode가 어떤 연관이 있는지 정리해보겠습니다.

 

우리는 상속 구조에서 @Data를 사용하면 IDE(적어도 Intellij는) 아래와 같은 warning을 보내주는 것을 알 수 있습니다.

 

메세지를 읽어보면, equals/hashCode 메소드가 부모 클래스의 equals/hashCode 메소드를 호출하지 않는다고 합니다.

 

실제로 그런지, 빌드된 코드와 간단한 테스트 코드를 살펴보겠습니다.

 

우선 예제에서는 아래와 같은 간단한 상속 클래스를 사용하였습니다.

 

@Data
public class Parent {
    private String familyName;
}

 

@Data
public class Child extends Parent {
    private String name;
}

 

그리고 아래는 Child.class를 decompile 한 결과 중 일부입니다.

 

public boolean equals(final Object o) {
    if (o == this) {
        return true;
    } else if (!(o instanceof Child)) {
        return false;
    } else {
        Child other = (Child)o;
        if (!other.canEqual(this)) {
            return false;
        } else {
            Object this$name = this.getName();
            Object other$name = other.getName();
            if (this$name == null) {
                if (other$name != null) {
                    return false;
                }
            } else if (!this$name.equals(other$name)) {
                return false;
            }
            
            return true;
        }
    }
}

public int hashCode() {
    int PRIME = true;
    int result = 1;
    Object $name = this.getName();
    result = result * 59 + ($name == null ? 43 : $name.hashCode());
    return result;
}

 

Child 클래스는 Parent를 상속받고 있고,

Parent 클래스 내부에는 familyName이란 필드가 정의되어 있지만,

Child 클래스의 equals/hashCode에는 해당 필드가 전혀 사용되고 있지 않습니다.

 

위와 같은 경우에는 어떤 문제가 발생할 수 있을까요? 간단한 테스트 코드를 작성해 보았습니다.

 

class ChildTest {
    @Test
    void equalsAndHashCodeTest() {
        HashMap<Child, String> hashMap = new HashMap<>();

        Child firstChild = new Child();
        firstChild.setFamilyName("lee");
        firstChild.setName("chol soo");

        hashMap.put(firstChild, "is he really lee chol soo?");

        Child secondChild = new Child();
        secondChild.setFamilyName("kim");
        secondChild.setName("chol soo");

        String message = hashMap.get(secondChild);

        assertNull(message);
    }
}

 

이름은 같지만(철수) 성은 다른(이/김) 두 Child 객체를 생성하고

이철수 Child 객체를 key로 메세지를 hashMap에 저장하였습니다.

그리고 김철수 Child 객체를 이용해서 메세지를 조회하고 해당 값이 null인지를 확인하는 테스트 코드입니다.

 

해당 테스트는 message가 null값이 아니기 때문에 실패하게 되어있습니다.

(message에는 "is he really lee chol soo"라는 값이 할당되어 있습니다.)

 

성 또한 사람을 구분하는 핵심 정보라고 생각하면

두 객체는 논리적으로 다른 객체이지만,

결과를 보면 hashMap은 김철수와 이철수 Child를 같은 객체로 인식하였습니다.

(아니라면 message는 null이 리턴되어야 합니다.)

 

원인은 많은 분들이 예상하시겠지만 해당 문제의 원인은,

Parent의 familyName이 두 객체의 비교에 사용되지 않았기 때문입니다.

 

이를 해결하기 위해서는 다양한 방법이 있겠지만 

이번 글에서는 @EqualsAndHashCode를 사용하여 해결해보겠습니다.

 

@EqualsAndHashCode는 이름에서 알 수 있듯이,

lombok이 equals 메서드와 hashCode 메서드를 생성하도록 하는 어노테이션입니다.

 

@Data에는 사실 위 어노테이션이 포함되어 있지만,

예제에서 살펴봤듯이 부모 클래스의 필드를 사용하지 않도록 구현이 되어있습니다.

 

그럼 @EqualsAndHashCode를 사용해서 부모 클래스의 필드를 사용하도록 수정해보겠습니다.

 

@Data
@EqualsAndHashCode(callSuper = true)
public class Child extends Parent {
    private String name;
}

 

코드는 매우 간단합니다.

@EqualsAndHashCode 어노테이션을 붙여주고 callSuper 속성을 true로 설정해주었습니다.

 

해당 코드를 빌드했을 때 결과는 어떻게 달라졌는지 살펴보면

 

 public boolean equals(final Object o) {
     if (o == this) {
         return true;
     } else if (!(o instanceof Child)) {
         return false;
     } else {
         Child other = (Child)o;
         if (!other.canEqual(this)) {
             return false;
         } else if (!super.equals(o)) {
             return false;
         } else {
             Object this$name = this.getName();
             Object other$name = other.getName();
             if (this$name == null) {
                 if (other$name != null) {
                     return false;
                 }
             } else if (!this$name.equals(other$name)) {
                 return false;
             }

             return true;
         }
     }
 }
 
 public int hashCode() {
     int PRIME = true;
     int result = super.hashCode();
     Object $name = this.getName();
     result = result * 59 + ($name == null ? 43 : $name.hashCode());
     return result;
 }

 

처음 예제와는 달리,

equals 안(10번째 줄)에서는 부모 클래스의 equals 메소드를 추가적으로 호출하고

hashCode안에서는 result값을 초기화할때 부모 클래스의 hashCode 메소드를 호출하는 것을 알 수 있습니다.

 

그럼 위에서 수행했던 테스트 코드는 어떻게 동작할까요?

 

class ChildTest {
    @Test
    void equalsAndHashCodeTest() {
        HashMap<Child, String> hashMap = new HashMap<>();

        Child firstChild = new Child();
        firstChild.setFamilyName("lee");
        firstChild.setName("chol soo");

        hashMap.put(firstChild, "is he really lee chol soo?");

        Child secondChild = new Child();
        secondChild.setFamilyName("kim");
        secondChild.setName("chol soo");

        String message = hashMap.get(secondChild);

        assertNull(message);
    }
}

 

이번에는 우리가 의도한대로 message는 null값이되고 테스트는 성공하게 됩니다.

 

위의 예제에서는 부모 클래스의 필드가

객체를 구분하는 핵심 필드이기 때문에 @EqualsAndHashCode의 callSuper를 true로 설정하였지만

부모 클래스의 필드를 equals/hashCode 내부에서 비교하지 않기를 원한다면

callSuper 속성을 false로 설정해주면 됩니다.

 

callSuper=false는 default 값이지만,

명시적으로 표기하지 않으면 IDE(적어도 Intellij는)에서 warning을 보내기 때문에

해당 warning이 꼴 보기 싫으신 분들은

@EqualsAndHashCode(callSuper=false)를 붙여주세요.

 

그럼 여기까지 상속 구조에서 @Data 사용시 발생하는 warning의 원인과 그 해결법이었습니다.

반응형