ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA 객체 사용 시 instanceof 는 주의해서 사용하자
    Computer - Java 2024. 6. 30. 00:40

    JPA가 굉장히 유용한 툴이라는 것은 사용해보면 모두 알지만, 그럼에도 불구하고 생각 외로 의도하지 않은 동작을 하는 경우가 많다.

    그 중 최근 내가 겪은 문제들 중 프록시 객체를 사용하기 때문에 주의해야 하는 부분들을 몇 가지 소개하고자 한다.

     

    1. Entity 객체 간 getClass 비교는 절대로 금물!

    다음 코드를 보고 직관적으로 특정 부분에서 잘못될 수 있다는 부분을 알 수 있겠는가?

    @Entity
    @Getter
    @AllArgsConstructor
    public class User {
        @GeneratedId
        private Long id;
    
        @NotNull
        private String name;
    
        @NotNull
        private String phoneNumber;
    
        @Override
        public boolean equals(Object other) {
            if (this.getClass() != other.getClass()) {
                return false;
            }
            return StringUtils.equals(this.getPhoneNumber(), ((MyObject)other).getPhoneNumber());
        }
    }

     

    기본적으로 equals 메서드가 getClass() 비교를 통해 이루어지기 때문에 저 메서드도 문제가 없다고 생각될 수 있으나,

    사실 JPA를 조금이나마 공부를 하면 저 equals 가 제대로 동작할 리 만무하다는 것을 잘 알 수 있다!

    기본적으로 JPA가 만들어내는 객체는 MyObject를 상속한 객체이기 때문에, getClass() 호출 시 User.class가 아닌 User.class를 상속한 클래스가 나오기 때문에 equals는 설령 같은 객체처럼 보여도 준영속 / 영속 상태의 객체의 equals()가 의도대로 동작하지 않을 수 있음다를 수 있음을 보여줍니다.

    (물론 기본적으로 제 실무에서는 Entity 객체 간 equals를 사용하는 경우가 생각보다 없었던 것 같기는 합니다)

    예를 들어, 아래와 같은 소스코드는 제대로 작동하지 않습니다.

     

    public class UserService {
    // ...
    	@Transactional
    	public boolean equals(Object other) {
        	// DB의 전화번호도 "010-1234-5678" 로 동일함
        	User userFromDB = userRepository.getById(1L);
            User newUser = new User(null, "larshew", "010-1234-5678");
            
            // 하지만 이 asserttion error가 된다! (getClass가 동일하지 않기 때문)
            assert newUser.equals(newUser, userFromDB);
        }
    // ...
    }

     

     

    2. instanceof 도 주의해서 사용할 것! (상속구조를 사용할 때)

    @Entity
    @Getter
    @AllArgsConstructor
    @DiscriminatorValue("KAKAO")
    public class KakaoUser extends User {
    
        private String kakaoEmail;
    }

     

    위 User를 상속한 KakaoUser 이라는 클래스가 있다고 하자. 

     

    그러면 아래 코드는 정상적으로 작동할까? 라고 한다면 아닐 수도 있다!

    public class UserService {
        ...
        @Transactional
        public boolean isKakaoUser(Long id) {
            User user = UserRepository.findById(id);
            // 여기서 user는 KakaoUser를 상속받은 프록시 객체가 아닐 수도 있다!
            if (user instanceof KakaoUser) {
                return true;
            } 
            return false;
        }
    }

     

    언뜻 보면 이 코드는 정상적으로 동작할 것 같으나, Spring Boot, Spring JPA 버전에 따라서 위 소스는 정상적으로 동작하지 않을 가능성도 충분하다. 왜냐 하면, user는 실제 kakaoEmail을 뜯어보기 전까지 User를 상속한 Proxy 객체일 가능성도 충분히 존재한다!

    따라서 위 부분을 적절히 수행해주기 위해서는 user를 unproxy 해 주거나, discriminator value(type)로 비교를 해 주는 것이 가장 안전한 전략일 것이다!

Designed by Tistory.