어느 엔티티 안에 속해있는 프록시 객체의 멤버변수 값을 가져오고 싶을 때 어떻게 해야할까에 대한 문제가 생겨서 이 글을 작성하게 됐다.
예를 들어, Member 엔티티 안에 Team 엔티티를 지연 로딩으로 설정한다. 그리고 Member 엔티티에서 Team 엔티티의 이름을 가져올 때 1안과 2안 중 어느 것이 맞을지에 대해 논의하여 작성하게 됐다.
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
// 1안
public String getTeamName() {
return team.name;
}
// 2안
public String getTeamGetName() {
return team.getName();
}
}
이제 검증을 시작하겠다.
먼저 Team 클래스를 정의한다.
📌 Team.class
@Entity
@Getter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String name;
public Team(String name) {
this.name = name;
}
}
그리고 Member 클래스를 정의한다.
📌 Member.class
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
// 1안
public String getTeamName() {
return team.name;
}
// 2안
public String getTeamGetName() {
return team.getName();
}
}
Member 엔티티에서 Team 엔티티는 지연로딩 설정했고 영속성 전이설정까지 했다.
Member 엔티티에서 Team 엔티티의 이름을 가져올 때 '1안'에서는 필드값 접근인 team.name으로 가져온다.
'2안'에서는 team.getName()으로 값을 가져온다.
📌 MemberTests.class
@DataJpaTest
class MemberTests {
@Autowired
private MemberRepository memberRepository;
@Autowired
private TeamRepository teamRepository;
@PersistenceContext
private EntityManager em;
@Test
void 팀_이름_조회_테스트() {
Team teamA = new Team("teamA");
Member member1 = new Member("이름1", teamA);
Member savedMember = memberRepository.save(member1);
Long id = savedMember.getId();
em.flush();
em.clear();
System.out.println("=========================================");
Member findMember1 = memberRepository.findById(id).orElse(null);
System.out.println();
System.out.println("멤버가 속한 팀 조회 : " + findMember1.getTeam().getClass().getName());
System.out.println();
System.out.println("멤버가 속한 팀의 이름 조회 1안 : " + findMember1.getTeamName());
System.out.println();
System.out.println("멤버가 속한 팀의 이름 조회 2안 : " + findMember1.getTeamGetName());
System.out.println("=========================================");
}
}
=========================================
Hibernate: select member0_.id as id1_0_0_, member0_.name as name2_0_0_, member0_.team_id as team_id3_0_0_ from member member0_ where member0_.id=?
멤버가 속한 팀 조회 : hello.refactor.prac1.Team$HibernateProxy$Xww9CYjY
멤버가 속한 팀의 이름 조회 1안 : null
Hibernate: select team0_.id as id1_1_0_, team0_.name as name2_1_0_ from team team0_ where team0_.id=?
멤버가 속한 팀의 이름 조회 2안 : teamA
=========================================
결론부터 말하자면 2안인 team.getName()으로 가져와야 한다.
테스트 진행 순서
1. 먼저 teamA를 member1에 생성자 주입을 하고 영속성 전이로 member1이 영속화 될 때 teamA도 같이 영속화 시킨다.
2 .영속성 컨텍스트를 비워주기 위해서 em.flush()와 em.clear()를 날린다.
3. db에서 member1를 조회한다.
4. 조회한 member1에서 teamA을 조회해본다. 결과는 프록시 객체로 나온다.
5. member1에 속한 teamA의 팀 이름을 가져오기 위해서 어떤 방법을 사용할지 1안과 2안을 모두 테스트 해본다.
6. 1안을 사용할 때는 team.name 으로 값을 가져오지만 프록시 객체라서 null 값이 나온다.
7. 2안을 사용할 때는 team.getName()으로 값을 조회하므로 콘솔창에서 나오듯 teamA인 프록시 객체가 db에서 실제 엔티티에 접근하기 때문에 select 쿼리가 나간다.이 때 참고할 점이 프록시 객체가 실제 엔티티로 바뀌지 않는다. 영속성 컨텍스트 1차 캐시 때문에 프록시 객체가 실제 엔티티로 접근 하는 것이다. 고로 2안을 사용했을 때는 프록시가 실제 엔티티에 접근하여 DB에 있는 값을 제대로 가져올 수 있다.
이러한 관점으로 봤을 때 엔티티의 equals() 메서드와 hashCode() 메서드를 오버라이딩할 때 프록시를 대비하여 get 방식으로 사용해야 안전하다.
예시)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressEmd addressEmd = (AddressEmd) o;
return Objects.equals(getCity(), addressEmd.getCity())
&& Objects.equals(getStreet(), addressEmd.getStreet())
&& Objects.equals(getZipcode(), addressEmd.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
결론 : 프록시 객체를 대비하여 get 방식으로 가져올 것을 추천한다!
'[JPA] > JPA' 카테고리의 다른 글
[JPA] @DataJpaTest + 테스트 DB 변경 (0) | 2022.05.15 |
---|---|
[JPA] N+1 문제 (즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join) (0) | 2022.05.02 |
[JPA] 엔티티의 필드 컬렉션을 생성과 동시에 초기화 하는 이유 (0) | 2022.04.06 |