프록시
- find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
- getReference() : 데이터베이스 조회를 미루는 Proxy(가짜) 엔티티 객체를 조회한다.
위와같이 JPA에서는 em.find 뿐만 아니라 em.getReference()라는 메서드도 지원해준다.
아래 테스트 코드를 통해 find() 메소드로 User를 조회해 보자.
[find() user 테스트코드]
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
User user = new User();
user.setUsername("hello");
em.persist(user);
em.flush();
em.clear();
User findUser = em.find(User.class, 1L);
}
}
[콘솔 결과] : JPA가 연관관계 엔티티까지 JOIN을 통해 데이터를 한번에 가져온다.
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
team1_.team_id as team_id1_8_1_,
team1_.teamname as teamname2_8_1_
from
users user0_
left outer join
team team1_
on user0_.team_id=team1_.team_id
where
user0_.user_id=1
[getReference() 테스트 코드]
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
User user = new User();
user.setUsername("hello");
em.persist(user);
em.flush();
em.clear();
User getReferenceUser = em.getReference(User.class, 1L);
}
}
[콘솔 결과] : INSERT쿼리만 발생한다.
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
[객체 탐색 테스트]
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
User user = new User();
user.setUsername("hello");
em.persist(user);
em.flush();
em.clear();
User getReferenceUser = em.getReference(User.class, 1L);
System.out.println("getReferenceUser" + getReferenceUser.getUsername());
}
}
[콘솔 결과] : getReference 객체로부터 탐색이 진행되었을때 비로서 Select쿼리가 발생한다.
(getUserId()는 getReference의 매개변수로 1L를 지정했기 때문에 조회를 하지 않아도 값이 주입되어 있다.
따라서 getUserId()의 경우 Select쿼리가 발생하지 않는다.)
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
team1_.team_id as team_id1_8_1_,
team1_.teamname as teamname2_8_1_
from
users user0_
left outer join
team team1_
on user0_.team_id=team1_.team_id
where
user0_.user_id=1
[getReference 클래스 조회 테스트]
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
User user = new User();
user.setUsername("hello");
em.persist(user);
em.flush();
em.clear();
User refUser = em.getReference(User.class, 1L);
System.out.println("refUser" + refUser.getClass()); //클래스 조회
}
}
[테스트 결과]
class com.example.demo.domain.User$HibernateProxy$05mgluiR
User$HibernateProxy$05mgluiR : Hibernate가 강제로 만드는 Proxy(가짜)객체
진짜 엔티티와 껍데기는 같으나, ID값만 가지고 있고 내부는 텅텅 비어있다.
Proxy객체는 실제 Entity 클래스를 상속받아서 만들어지기 때문에 실제 엔티티 클래스와 겉 모양이 같다.
이론상으로 사용자 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
Proxy의 내부에는 target이라는 필드가 있다.
target은 실제 객체를 참조할수 있는 필드이다.
만약 Proxy에서 getUsername()을 호출한다면 실제 target(User)에 있는 getUsername을 호출하게 된다.
매커니즘
- 프록시 객체 조회
User findUser = em.getReference(User.class, 1L);
프록시 객체에는 target(null), id(1L), username("JPA") 와 같이 데이터가 들어있다. - 프록시 객체로 부터 객체탐색
findUser.getUsername();- Proxy객체에 target이 없으면 JPA에 영속성 컨텍스트를 요청한다.
- 영속성 컨텍스트가 Database를 조회한뒤 실제 Entity를 생성해서 반환한다.
- 프록시 내부에서 target.getUsername();으로 연결해준다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
초기화 되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는것으로 프록시가 실제로 바뀌는것이 아니다.
Proxy객체가 계속 유지되고 내부의 target으로 접근하는 매커니즘이다. - 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다.
( == (동등) 비교 대신 instance of 를 사용해야 한다.) - 영속성 컨텍스트에 찾으려는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환하게 된다.
em.find() 한 이후 바로 em.getReference() 하게되는 경우 : 2가지 이유가 있다.- 이미 영속성 컨텍스트에 있는데 굳이 Proxy객체를 가져오면 아무런 이점이 없기 때문
- JPA에서는 한 트랜잭션 안에서 영속성 컨텍스트로부터 반환받은 PK가 동일한 객체에 대해 동등성을 보장을 해준다.
(JPA가 기본적으로 제공해주는 매커니즘 중 하나이다.) - 반대로 첫번째로 em.getRererence()를 먼저 하고 두번째로 em.find()를 하게되면 프록시 객체로 주입된다.
(find에서 실제 Select쿼리가 발생하고 find객체는 프록시객체로 주입된다.)- 직전 2번에서 언급했던 JPA의 동등성을 보장해주는 매커니즘을 따라야 하기 때문이다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
(Hibernate는 org.hibernate.LazyInitializationException 예외를 터트린다.)
em.getReference() 이후 em.detach(refUser) 혹은 em.close(), em.clear() 를 하게되면
영속성 컨텍스트로부터 초기화 해야하는데, detach등을 하게되면 영속성 컨텍스트에서 분리가되기 때문에
결과적으로 Proxy객체를 초기화할 수 없게 되기 때문이다. -no Session
프록시 확인
- 프록시 인스턴스 초기화 여부 확인 (Boolean)
PersistenceUnitUtil.isLoaded(엔티티클래스 엔티티객체)- emf.getPersistenceUnitUtil().isLoaded(엔티티클래스 엔티티객체);
- em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(엔티티클래스 엔티티객체);
- 프록시 클래스 확인 방법
엔티티객체.getClass.getName() - 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
- 단, JPA표준은 강제 초기화가 없기 때문에 강제 호출 해야한다.
refUser.getUsername();
- 단, JPA표준은 강제 초기화가 없기 때문에 강제 호출 해야한다.
'Hibernate > JPA(EntityManager)' 카테고리의 다른 글
[JPA] 영속성 전이와 고아객체 (CASCADE, orphaRemoval) (0) | 2023.07.13 |
---|---|
[JPA] 즉시로딩/지연로딩(Eager/Lazy) 과 JPQL N+1 이슈 (0) | 2023.07.13 |
[JPA] 상속 관계 매핑 @Inheritance , @Discriminator____ (0) | 2023.06.30 |
[JPA] 값 타입 컬렉션 (@ElementCollection / @CollectionTable) (0) | 2023.06.29 |
[JPA] 값 타입 객체간 비교 (equlas & hashcode) (0) | 2023.06.29 |