본문 바로가기
프로젝트/텀블벅 클론 코딩

[Java] 상속 관계의 동등성 비교 구현

by zangsu_ 2023. 5. 20.

DTO 분리

 

현재 프로젝트에서 사용하는 UserDTO는 총 두개로, 다음과 같다.

package com.example.tumblbugclone.dto;

@NoArgsConstructor
@Data
public class UserSendingDTO {

    private Long userIdx;
    private String userName;
    private String userId;
    private String userEmail;
    private String greeting;
    private String userImg;
    private Date lastLogin;
    private boolean isActive;
}

@NoArgsConstructor
@Data
public class UserReceivingDTO extends UserSendingDTO {

    private String userPassword;
}

 

위와 같이 두개의 DTO로 분리 한 이유는, 서버에서 UserDTO를 반환 해 줄 때, 민감한 정보인 userPassword는 숨기고 싶었기 때문이다.

로그인 기능, 또는 회원 가입 기능 등을 위해 하나의 DTO를 사용하게 된다면, userPassword를 꼭 포함 시켜야 한다. 이 경우, userPassword를 숨기기 위해 null 등의 값을 삽입 해 주어야 하며 여러 상황에서 DTO의 형태가 동일하다는 것을 보장해 주지 못한다.

이를 해결하기 위해 서버가 받게 되는 DTO에는 필요한 데이터인 userPassword를 포함시키고, 서버가 반환해 주는 UserDTO에는 userPassword 정보를 제외시켜 보안을 신경 쓰고자 했다.

 

동등성 비교

아래는 UserService의 동작을 확인하기 위한 테스트 코드의 일부이다.

@Test
    @Transactional
    public void 회원_정보_수정성공() throws Exception{
        //given
        UserReceivingDTO user = make_Nth_User(1);
        long userIdx = userService.join(user);
        user.setUserIdx(userIdx);

        //when
        UserReceivingDTO modifyUser = make_Nth_User(1);
        modifyUser.setUserIdx(userIdx);
        modifyUser.setUserImg("newImageURL");
        modifyUser.setUserPassword("newPassword");
        modifyUser.setGreeting("Hi");
        userService.modify(modifyUser);

        //then
        Assertions.assertThat(userService.findUserByIndex(user.getUserIdx())).isEqualTo(modifyUser);
    		//userService.findUserByIndex()는 UserSendingDTO를 반환
    }

 

위 코드의 마지막 줄인 Assertion 부분에서 UserSendingDTO와 UserReceivingDTO, 다른 두 클래스의 동등성 비교가 이루어진다. 때문에 이 코드 자체로는 테스트를 통과하지 못한다.

org.opentest4j.AssertionFailedError: 
expected: UserReceivingDTO(userPassword=newPassword)
 but was: UserSendingDTO(userIdx=1, userName=user1Name, userId=user1Id, userEmail=user1Email, greeting=Hi, userImg=newImageURL, lastLogin=null, isActive=false)

 

해결 방법

UpCasting을 사용

가장 먼저 든 생각은 equals() 메서드를 오버라이딩 하고, UserReceiverDTO를 UserSendingDTO로 형 변환 한 후 두 객체를 비교하는 것이었다.

public class UserReceivingDTO extends UserSendingDTO {
	...
    
    @Override
    public boolean equals(Object o){
        if(!(o instanceof UserSendingDTO))
            return false;
        if(!(o instanceof UserReceivingDTO))
            return super.equals(o);
        return super.equals(o) && ((UserReceivingDTO) o).userPassword.equals(this.userPassword);
    }
}


public class UserSendingDTO {
	...
    
    @Override
    public boolean equals(Object o){
        if(!(o instanceof UserSendingDTO))
            return false;
        UserSendingDTO u = ((UserSendingDTO)o);
        return super.equals(u);
    }
}

결과적으로 이 방법은 실패했다.

 

업캐스팅을 사용하여 자식 클래스를 부모 클래스로 형 변환을 해 주는 것은 일시적인 변화이다. 즉, UserSendingDTO와 UserReceiverDTO를 비교해 주는 경우, UserSendingDTO.equals()에서 새로 생성해 준 UserSendingDTO u의 실질적인 클래스는 UserReceiverDTO인 것이다.

https://zangsu.tistory.com/16

 

[오개념 정리] Upcasting

다음과 같은 클래스 Parent, Child가 존재한다고 가정 해 봅시다. public class Parent { int a; } public class Child extends Parent{ int b; } 즉, Child 클래스는 Parent 클래스를 부모 클래스로 상속 받습니다. 이후 다음

zangsu.tistory.com

 

결과적으로 다른 두 클래스에 대한 equals() 동등성 비교 값은 항상 false 이다.

 

hashCode() 오버라이딩

두 번째 방법은 hashCode()를 사용하는 동등성 비교이다.

자식 클래스의 hashCode()를 부모의 hashCode()를 그대로 오버라이딩 해 사용하는 것이다.

public class UserReceivingDTO extends UserSendingDTO {
	...	

    @Override
    public int hashCode(){
        return super.hashCode();
    }
}

 

아주 잠깐, hashCode()의 값이 같으면 같은 객체가 아닐까 라는 생각을 했던 것 같다.

미리 말하자면, 이 방법은 아주 잘못되었다. 동등성이 같지 않은 두 객체가 같은 hashCode()의 값을 가질 수 있기 때문이다.

 

그런데,,, 테스트는 통과가 되었다....

(이게 왜 되지)

 

equals() 오버라이딩

결국 마지막에 선택한 방법은, equals()를 오버라이딩 하여 필요한 모든 필드 값을 직접 비교하는 것이다.

현재 내가 비교하고자 하는 객체는 DTO 이지만, 완전히 생성되고 값이 주입 된 이후 DTO의 사용이 끝날 때 까지 도중에 값이 변경되야 할 일은 크게 없다. 즉 VO의 특성을 어느 정도 가지고 있다. 때문에 공통된 모든 필드 값을 비교 하여 동등성 비교를 진행할 수 있다.

public class UserReceivingDTO extends UserSendingDTO {
	...
    
    @Override
    public boolean equals(Object o){
        if(!(o instanceof UserSendingDTO))
            return false;
        if(!(o instanceof UserReceivingDTO)){
            return super.equals(o);
        }
        return super.equals(o) && this.userPassword.equals(((UserReceivingDTO) o).userPassword);
    }

}

public class UserSendingDTO {
	...

    @Override
    public boolean equals(Object o){
        if(!(o instanceof UserSendingDTO)) {
            return false;
        }

        if(!this.userIdx.equals(((UserSendingDTO) o).userIdx))
             return false;
        if(!this.userName.equals(((UserSendingDTO) o).getUserName()))
             return false;
        if(!this.userId.equals(((UserSendingDTO) o).getUserId()))
            return false;
        if(!this.userIdx.equals(((UserSendingDTO) o).getUserIdx()))
            return false;
        if(!this.userEmail.equals(((UserSendingDTO) o).getUserEmail()))
            return false;
        if (!this.greeting.equals(((UserSendingDTO) o).getGreeting()))
            return false;
        if(!this.userImg.equals(((UserSendingDTO) o).getUserImg()))
            return false;
        if(!this.lastLogin.equals(((UserSendingDTO) o).getLastLogin())) 
            return false;
        if(this.isActive != ((UserSendingDTO)o).isActive())
            return false;

        return true;
    }
}

 

댓글