Home [백엔드|스프링부트] 프록시 객체때문에 발생할 수 있는 equals 예외
포스트
취소

[백엔드|스프링부트] 프록시 객체때문에 발생할 수 있는 equals 예외

개요

JPA를 통해 객체를 불러오면서도 모든 연관된 엔티티를 전부 불러오고 싶지는 않다면, JPA가 지원하는 지연로딩 방식을 사용하면 된다. 그러면 JPA는 하이버네이트 구현체가 만든 프록시 객체로 불러와 데이터의 자리를 메워준다. 이 프록시 객체는 실제 데이터를 DB에서 불러오지 않고도 데이터가 존재하는 것처럼 해줘서, 해당 데이터를 실제로 접근하기 전까지 불러오는 걸 미뤄둘 수 있도록 해주는 유용한 객체이다. 하지만 그런 차이점 때문에 프록시 객체를 실제 객체처럼 대하다가는 오류가 발생할 수 있다. 이번 글에서는 그렇게 발생할 수 있는 오류에 대해 알아보고자 한다.

프록시의 equals

자바에서 객체 비교를 하기 위해 보통 인텔리제이의 기능을 빌려 equals 메서드를 오버라이딩 한다. 그렇게 작성된 엔티티는 다음과 같다.

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
32
33
34
35
36
37
38
@Getter
@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private UserAccount userAccount;

    // 기타 필드 생략...
}

@Getter
@Entity
public class UserAccount {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    //... 생략

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        UserAccount userAccount = (UserAccount) o;
        return Objects.equals(id, userAccount.id);
    }
}

이 때, 다음과 같이 하나의 Post에서 동일한 UserAccount를 호출해 비교하는 테스트 코드를 실행시켜보자.

1
2
3
4
5
6
7
@Test
void name() {
    Post post = postRepository.findById(1L).orElseThrow();
    UserAccount userAccount1 = post.getUserAccount();
    UserAccount userAccount2 = post.getUserAccount();
    assertThat(userAccount1).isEqualTo(userAccount2);
}

하나의 엔티티에서 가져온 동일한 엔티티여야 하지만, 테스트 코드는 실패를 반환한다.

1
2
3
org.opentest4j.AssertionFailedError: 
expected: "UserAccount(userId=1) (UserAccount$HibernateProxy$luj7HgwZ@5f8d4b51)"
 but was: "UserAccount(userId=1) (UserAccount$HibernateProxy$luj7HgwZ@5f8d4b51)"

메시지를 보면 필드도 주솟값도 같은 객체인데도, 테스트에 실패한다는 메시지를 보여준다. 어디에서 문제가 있는 걸까?

원인은 getClass() 에 있다. equals()를 호출하는 객체를 A라 하고, 그 인자로 받은 객체는 B라 하자. A는 확실히 UserAccount 클래스가 맞다. 하지만 B의 클래스는 UserAccount가 아닌, 하이버네이트가 만든 프록시 객체인 것. 고치려면 조건문을 if (!(o instanceof UserAccount)) 로 바꾸면 된다.

그렇다면 클래스 비교만이 문제일까? 코드를 고치고 다시 테스트 코드를 실행해보면 똑같은 오류가 또 다시 나온다. Objects.equals(id, userAccount.id) 에도 문제가 있다는 뜻이다.

하이버네이트가 만들어주는 프록시 객체는 실제 객체의 상속본이며, 실제 객체와 다르게 필드가 존재하지 않고 메서드로만 값을 조회할 수 있다. 따라서 인자로 받은 객체 B의 id를 userAccount.id 로 접근하면 id 필드가 존재하지 않아 null을 반환하게 되는 것이다. 고치려면 getter 메서드를 이용해 필드를 조회하도록 바꾸면 된다.

위 두 오류를 고쳐 다음과 같이 작성하면 드디어 테스트 코드를 통과하게 된다.

1
2
3
4
5
6
7
8
9
10
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof UserAccount that)) { // that을 패턴 변수로 변환해 한 줄을 줄였다.
            return false;
        }
        return this.getUserId() != null && this.getUserId().equals(that.getUserId());
    }

참고 자료


이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[백엔드|스프링부트] 서버로 들어오는 값에 대한 유효성 간단하게 검사하는 법

[백엔드|스프링부트] 운영중인 웹 서비스를 Actuator로 모니터링 해보자