[JPA] 프록시(Proxy)
연관관계가 존재하는 엔티티를 조회 시, 연관된 다른 엔티티도 함께 조회해야 할까?
실제 필요한 비지니스 로직에 따라 다를 것이며, 필요하지 않는 경우 다른 엔티티까지 가져오는 건 불필요할 것이다.
JPA는 위와 같은 문제를 대비하여, 지연로딩과 프록시라는 개념을 가지고 있다.
프록시 기초
먼저 지연 로딩을 이해하려면, 프록시의 개념에 대해 정확히 이해해야 한다.
JPA에서 em.find() 이외 em.getReference()라는 메서드도 제공된다.
- em.find(): DB를 통해 실제 엔티티 객체를 조회하는 메서드
- em.getReference(): DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회하는 메서드
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username;
private int age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Lob
private String description;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
위와 같은 Member 엔티티 객체가 있다고 가정하고 Member 객체를 생성하여 DB에 저장했다고 치자.
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
...
// 트랜잭션 커밋 및 종료
Member를 em.find()로 조회한다면 다음과 같이 select 쿼리가 발생한다.
Hibernate:
select
member0_.id as id1_4_0_,
member0_.age as age6_4_0_,
member0_.locker_id as locker_10_4_0_,
member0_.roleType as roleType8_4_0_,
member0_.team_id as team_id11_4_0_,
member0_.name as name9_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.id as id1_8_2_,
team2_.createdBy as createdB2_8_2_,
team2_.createdDate as createdD3_8_2_,
team2_.lastModifiedBy as lastModi4_8_2_,
team2_.lastModifiedDate as lastModi5_8_2_,
team2_.name as name6_8_2_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
left outer join
Team team2_
on member0_.team_id=team2_.id
where
member0_.id=?
findMember.id = 1
findMember.username = creator
Member 객체를 em.fetReference()로 조회하면 select 쿼리가 바로 발생하지 않고, 필요한 시점부터 select 쿼리가 발생한다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
...
// 트랜잭션 커밋 및 종료
findMember.getClass()를 실행한 시점에 select 쿼리는 발생되지 않고, Member 객체가 아닌 하이버네이트가 만든 가짜 프록시 객체가 있다.
findMember.getId()를 실행한 시점에는 프록시 객체가 id 값을 select 쿼리가 발생하지 않아도 알고 있다.
findMember.getUsername()를 실행한 시점부터 select 쿼리가 발생한다.
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
Hibernate:
select
member0_.id as id1_4_0_,
member0_.createdBy as createdB2_4_0_,
member0_.createdDate as createdD3_4_0_,
member0_.lastModifiedBy as lastModi4_4_0_,
member0_.lastModifiedDate as lastModi5_4_0_,
member0_.age as age6_4_0_,
member0_.description as descript7_4_0_,
member0_.locker_id as locker_10_4_0_,
member0_.roleType as roleType8_4_0_,
member0_.team_id as team_id11_4_0_,
member0_.name as name9_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.id as id1_8_2_,
team2_.createdBy as createdB2_8_2_,
team2_.createdDate as createdD3_8_2_,
team2_.lastModifiedBy as lastModi4_8_2_,
team2_.lastModifiedDate as lastModi5_8_2_,
team2_.name as name6_8_2_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
left outer join
Team team2_
on member0_.team_id=team2_.id
where
member0_.id=?
findMember.username = hello
프록시 특징
- 실제 클래스를 상속 받아 만들어진다.
- 실제 클래스와 겉 모양이 동일하며, 사용자 입장에서 진짜 객체인지 프록시 객체인지 구분할 필요 없이 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관하고 있다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, member.getId());
member.getName();
- em.getReference로 프록시 객체를 가져온 이후, getName() 메서드를 호출한다.
- MemberProxy 객체에 target 값을 JPA가 영속성 컨텍스트에 초기화 요청을 한다.
- 영속성 컨텍스트가 DB에 조회에서 실제 Entity를 생성시킨다.
- 프록시 객체가 가지고 있는 target 값을 이용하여 실제 Entity의 getName()을 호출해서 member.getName()을 호출한 결과를 받는다.
- 프록시 객체에 target 값이 할당되면, 프록시 객체의 초기화 동작은 일어나지 않는다.
프록시 정리
- 프록시 객체는 처음 사용할 때 한번 만 초기화 된다.
- 프록시 객체를 초기화한다고 해서, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.
- targer 값을 이용하여 실제 엔티티를 이어주는 역할을 한다.
- 프록시 객체는 원본 엔티티를 상속 받기 때문에 프록시 객체와 원본 객체의 타입이 다르기 때문에 주의해야한다.
- '==' 비교가 불가능하며, instanceOf를 사용한다.
- 영속성 컨텍스트가 내부에 이미 엔티티를 가지고 있다면, em.getReference()를 호출해도 실제 엔티티를 반환한다.
- JPA 는 하나의 영속성 컨텍스트는 같은 엔티티의 동일성을 보장해준다.
실무에서 만나는 문제
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일때, 초기화 문제가 발생한다.
트랜잭션 범위 밖에서 프록스 객체를 조회하려고 할때 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.
이를 방지하기 위해 OSIV(open-session-in-view) 설정으로 영속성 컨텍스트의 생존 범위를 늘려주는 방식인데 실무에서 사용하지 않는 편이다. 영속성 컨텍스트 생존 범위가 클수록 자원 낭비가 심해지기 때문이다.
reference
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com