JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.
프록시
나는 둘 중에 하나만 조회하고 싶어
Member와 Team이 연관관계로 매핑되어 있다 하자. 그럼 Member만 조회해야 할 때도 있고, Team만 조회해야 할 때도 있고, Member와 Team을 모두 조회해야 할 때도 있다.
Member와 Team 함께 출력
private static void printMemberAndTeam(Member member) {
String username = member.getUsername();
System.out.println("username = " + username);
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
Member만 출력
private static void printMember(Member member) {
System.out.println("member = " + member.getUsername());
}
Member와 Team을 함께 출력할 때야 문제가 없지만 Member만 조회하고 싶은데 Team까지 같이 딸려 들어와 버리면 최적화가 잘 되어있지 않은 것이다. 이런 경우엔 최적화할 필요가 있어 JPA에서는 지연 로딩과 프록시를 지원한다.
em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회. 즉 DB에는 SQL 쿼리가 나가지 않는데 객체는 조회하는 것이다.
getReference() 사용하기
- em.find() 사용
//Member.class
@Entity
public class Member extends BaseEntity{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...생략
// 실행 클래스
...생략
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
...생략
// 출력 로그
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_5_1_,
team1_.createdBy as createdB2_5_1_,
team1_.createdDate as createdD3_5_1_,
team1_.lastModifiedBy as lastModi4_5_1_,
team1_.lastModifiedDate as lastModi5_5_1_,
team1_.name as name6_5_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
em.find()를 사용하니 Member 객체에 관한 출력만 필요한데도 Team 테이블까지 조인해 SELECT 쿼리를 전송한다.
- em.getReference()
// Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId()); //**
// System.out.println("findMember.id = " + findMember.getId());
// System.out.println("findMember.username = " + findMember.getUsername());
위의 코드에서 em.find() 코드를 주석 처리하고 대신 getReference()를 사용해서 실행시키면 SELECT SQL이 전송되지 않는다. 그럼 이번엔 찾은 member의 값을 꺼내는 출력문의 주석을 풀어보자.
//Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
// 출력 로그
findMember.id = 1 // id 출력
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_5_1_,
team1_.createdBy as createdB2_5_1_,
team1_.createdDate as createdD3_5_1_,
team1_.lastModifiedBy as lastModi4_5_1_,
team1_.lastModifiedDate as lastModi5_5_1_,
team1_.name as name6_5_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
findMember.username = // username 출력
그러면 이제 다시 또 Member와 Team을 JOIN 한 SELECT SQL이 실행된다.
em.getReference() 코드를 실행할 때에는 쿼리가 발생하지 않는다. em.getReference()로 찾아온 객체를 사용할 때에 SQL 쿼리가 발생하는데, findMember.getId()의 경우엔 이미 em.getReference()에서 member.getId() 값을 넣어주었으므로 DB까지 다녀오지 않아도 알 수 있는 값이라 이때도 쿼리는 발생하지 않는다. 다만 find.getUsername()은 DB에서 조회를 해야지만 알 수 있는 값이기 때문에 이때 DB에 SQL문이 전달된다.
그럼 저 em.getReference()로 가져온 findMember는 도대체 뭘까? 출력해보자.
System.out.println("findMember = " + findMember.getClass());
// 출력 로그
findMember = class hellojpa.Member$HibernateProxy$nizRdaZG
findMember는 하이버네이트에서 만든 가짜 클래스이다.
프록시의 특징
- 실제 클래스를 상속받아서 만들어진다.
- 실제 클래스와 겉모양이 같다.
- 이론상 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, 1L);
member.getName();
- 클라이언트가 프록시 객체의 getName()을 호출한다.
- 프록시 target이 null이면 영속성 컨텍스트에 초기화를 요청한다.
- 영속성 컨텍스트에서 DB를 조회한다.
- DB에 조회한 데이터로 실제 Entity를 생성한다.
- 프록시의 target에 member 엔티티 정보가 저장되고, target.getName()을 했을 때 실제 객체에 존재하는 getName()을 호출한다.
초기화 이후 getName()을 호출하면 DB 조회는 이루어지지 않고 target에 값이 존재하니까 객체에 있는 값을 호출한다.
사실 프록시 객체에 대한 메커니즘은 JPA 표준 스펙에 존재하지 않는다. 하이버네이트 등의 라이브러리가 구현하기 나름이긴 하지만 기본적인 매커니즘은 위와 같다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때, 프록시가 실제 엔티티로 교체되는게 아니라, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다.
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다.
(== 비교 XXXXX, 대신 instance of를 사용해야 한다.)
// em.find() == em.find()
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
// 출력 로그
m1 == m2 : true
em.find()로 찾은 객체와 em.find()로 찾은 객체는 == 비교 시 true가 반환된다.
// em.find() == em.getReference()
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
// 출력 로그
m1 == m2 : false
em.find()로 찾은 객체와 em.getReference()로 찾은 객체는 == 비교 시 false가 반환된다.
System.out.println("m1 == Member : " + (m1 instanceof Member)); //true
System.out.println("m1 == Member : " + (m2 instanceof Member)); //true
타입 비교를 할 때는 instanceof를 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());
// 출력 로그
m1 = class hellojpa.Member
reference = class hellojpa.Member
getReference() 임에도 불구하고 프록시 객체가 아니라 Member 엔티티가 찍히는 이유는 두 가지가 있다.
- 영속성 컨텍스트에 있는 1차 캐시에 이미 값이 있는데 굳이 DB를 조회해 올 필요가 없다. 즉, 프록시 객체를 반환하는 이유가 없다.
- 같은 트랜잭션 안에 있고, 같은 id를 가진 객체는 무조건적으로 JPA에서 == 비교 시 true가 나오게 보장해주는 메커니즘이 존재한다.
2번과 같은 이유로 em.getReference() 후 em.find()를 하면 둘 다 프록시 객체를 반환하는 걸 볼 수 있다.
Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);
em.flush();
em.clear();
Member referenceM2 = em.getReference(Member.class, member2.getId());
System.out.println("referenceM2.getClass() = " + referenceM2.getClass());
Member findM2 = em.find(Member.class, member2.getId());
System.out.println("findM2.getClass() = " + findM2.getClass());
// 출력 로그
referenceM2.getClass() = class hellojpa.Member$HibernateProxy$Dl9hL0nE
findM2.getClass() = class hellojpa.Member$HibernateProxy$Dl9hL0nE
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다.
...생략
try {
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // Proxy
em.detach(reference); // reference 객체를 영속성 컨텍스트에서 제외
reference.getUsername(); // 프록시 초기화
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace(); // 에러 로그 출력
}
... 생략
위의 코드를 실행하면 에러가 터진다.
em.detach() 대신 em.close(), em.clear()를 해도 에러가 발생한다. 실무에서 많이 발생하는 문제이니 주의해서 코딩해야 한다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // Proxy
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // false
//=============================//
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // Proxy
reference.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // true
- 프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist...or HibernateProxy...)
System.out.println("reference = " + reference.getClass());
- 프록시 강제 초기화. JPA 표준에는 강제 초기화가 없고 하이버네이트에서 지원하는 기능이다.
org.hibernate.Hibernate.initialize(entity)
Hibernate.initialize(reference);