인프런 커뮤니티 질문&답변

edusolution0723님의 프로필 이미지
edusolution0723

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

동일성 비교 실패

해결된 질문

작성

·

232

0

안녕하세요.
먼저 좋은 강의 만들어주셔서 감사하다는 말씀 드립니다.
동일성 비교를 제가 잘못 이해하고 있었던 것인지 모르겠지만 아래 코드가 동작하지 않습니다.
뭐가 문제일까요?
제가 이해했던 동일성 비교는
"같은 트랜젝션 내부에서는 보통 같은 영속성 컨텍스트를 가지고, 따라서 같은 컨텍스트에서 가져온 같은 id의 entity는 동일성 비교를 하여도 true를 반환한다" 라고 알고 있습니다.
그리고 이 내용을 이용해서 서비스에 있던 비즈니스 로직을 도메인으로 옮기는 작업을 하는 중이었습니다.
다음은 기존에 작성했던 코드 입니다.
이 유저가 팀의 생성자가 맞으면 팀을 삭제할 수 있다
Service.java
// 유저 정보를 가져옵니다.
User user = userRepository.findByEmail(SuccessAuthentication.getPrincipal(String.class));

// 삭제하려는 팀 정보를 가져옵니다.                
Team team = teamRepository.findById(teamDTO.getId());

// 이 유저가 팀 생성자가 아니면 Exception
if (user != team.getUser())
    throw new UnauthorizedException(ErrorCode.UNAUTHORIZED_VALUE);

이 코드는 정상동작하는 코드입니다.

그런데 여기서 팀에 대한 권한이 있는지 여부를 검사하는 것은 Team의 역할이라고 생각이 되서 다음과 같이 변경하였습니다.

Service.java

// .. 위 코드는 동일합니다.

// team 이 user 에 의해서 삭제 됩니다.
team.deletedByUser(this);

Team.java

public Team deletedByUser(User modifier) {
    // 팀의 생성자가 매개변수로 넘겨 받은 User Entity와 같은지 비교 후 삭제
    if (this.user != modifier)
        throw new UnauthorizedException(ErrorCode.UNAUTHORIZED_VALUE);
    this.state = State.DELETED;
    return this;
}

변경 전 코드에서는 Team 생성자가 맞을 경우 user != team.getUser() 이 코드 부분을 정상 pass 하였지만,

코드를 변경하고 나니 이 부분에서 걸립니다. => 유저가 팀 생성자라서 같은 id를 가지는 entity 객체임에도 같지 않다는 결과를 반환합니다.

Service 에서 동작하던 코드를 Domain 으로 옮기고 this 키워드로 team의 user(생성자)를 참조하는 코드로 변경했을 뿐인데 동작하지 않습니다.

뭐가 문제일까요..?

답변 4

2

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. edusolution0723님

이 문제는 프록시가 가지는 한계점 때문에 발생합니다.

현재 team -> user의 관계가 LAZY로 되어 있습니다.

이렇게 되면 team을 조회할 때 user는 지연로딩을 위해서 프록시user로 조회됩니다.

JPA는 동일성을 보장하기 위해서 이후에 user를 조회하면 해당 user도 프록시로 조회됩니다.

다음 코드를 실행해보면 둘다 프록시 인것을 확인할 수 있습니다.

Team team = teamRepository.findByTitle("teamA"); //User.team을 프록시로 조회

User user = userRepository.findByName("user1");  //반환된 user도 프록시(단 초기화는 되어 있음)

System.out.println("team.user=" + team.getCreator().getClass());

System.out.println("user=" + user.getClass());

[실행 결과]

team.user=class User$HibernateProxy$teNsLrVM

user=class User$HibernateProxy$teNsLrVM

당연히 team == user도 결과가 true가 나오게 됩니다.

JPA가 동일성을 보장하기 위해 처음에 프록시로 조회된 엔티티가 있으면 끝까지 프록시로 반환해주는 것이지요.

여기서는 처음에 team.user가 프록시로 조회되었기 때문에 이후에 user를 직접 조회해도 프록시로 반환해줍니다.

그런데 말씀하신 user.delete()를 호출하는 로직에서는 왜 문제가 발생했을까요?

public Team delete(Team team) {

    System.out.println("this user.getClass()=" + this.getClass());

    return team.deletedByUser(this);

}

[실행 결과]

this user=class User

여기서 문제는 바로 this입니다. delete() 코드 안에서 this는 이미 프록시 객체를 넘어서 실제 객체를 뜻합니다.

클라이언트 -> proxy -> 실제 객체

여기서 this라고 하면 실제 객체가 되는 것이지요. 실행 결과를 보면 this user가 프록시가 아닌 실제 클래스 타입이 출력되는 것을 확인할 수 있습니다.

이렇게 되면 tea.deleteByUser(this)에서 프록시가 넘어가는 것이 아니라 실제 객체가 넘어가기 때문에 deleteByUser() 메서드 호출에서 비교가 실패하게 됩니다.

public Team deletedByUser(User user) {

    if (this.creator == user) {

    //this.creator = 프록시

    //user = 실제 객체

    }

    return this;

}

반대로 처음에 조회시 순서를 뒤집으면 프록시가 아니라 모두 실제 객체로 조회되므로 문제가 없습니다.

User user = userRepository.findByName("user1");  //user는 실제 객체

Team team = teamRepository.findByTitle("teamA"); //User.team도 실제 객체

현재 해결방안은 지금처럼 서비스 코드에서 user를 호출하고 user에서 또 team의 코드를 본인(this)를 넘기면서 실행하는 것이 아니라

서비스 코드에서 단순하게 다음처럼 바로 호출하는 것을 추천합니다.

team.deletedByUser(user); //이렇게 호출해서 처리

도움이 되셨길 바래요.

1

와... 이렇게 정성 스럽게 답변해주셔서 감사합니다..!!

완전히 이해 되었습니다 ㅎㅎ

정말 정말 감사합니다!

0

안녕하세요!

문제 상황과 동일하게 테스트 코드를 만들었습니다.

https://github.com/donghyeon0725/spring_equals_test1

위 링크입니다.

언제 문제가 발생하는지는 찾았는데, JPA 가 어떻게 동작하기에 이런 결과가 나온건지 궁금합니다. 

문제 상황이나, 실행 방법에 관해서는 README.md 파일에 정리 해두었습니다!

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. edusolution0723님

전체 프로젝트를 압축해서 구글 드라이브로 공유해주세요.

여기에서 어떻게해야 해당 상황을 재현할 수 있는지 실행 방법도 자세히 남겨주세요.

감사합니다.

edusolution0723님의 프로필 이미지
edusolution0723

작성한 질문수

질문하기