본 포스트는 김영한 님의 자바 ORM 표준 JPA 프로그래밍 강의를 토대로 작성하였습니다.
프록시 객체란 쉽게 말해 가짜 객체를 말한다. JPA에서는 성능 최적화를 위해 프록시라는 기술을 사용하여 필요할 때 sql 쿼리를 날리도록 구현하고 있다. 메커니즘에 대해 알아보자.
지금까지는 EntityManager의 find 함수를 통해 찾고자 하는 객체를 가져왔다. 이 때 가져온 객체는 영속 컨텍스트에 있으면 그 객체를 가져오고 없으면 DB에서 쿼리해와서 반환하는 진짜 객체이다.
그러나 em.getReference()를 사용하면 JPA에서는 Proxy 객체를 반환한다. 프록시 객체는 가짜 객체이며, 나중에 해당 객체가 진짜 사용될 때 DB에 쿼리를 날려 진짜 객체를 프록시 객체에 연결해준다.
좀 더 구체적으로 설명하면, 위 그림에 있는 Entity target이 진짜 객체의 주소를 저장하는 필드가 된다. 따라서 처음 프록시 객체를 만들면 해당 필드의 값이 null 이지만 실제로 사용될 때(ex member.geName() 호출 등) DB에 쿼리를 날려 객체를 찾아오고 그 객체를 target에 집어넣는다.
프록시는 내부적으로 실제 클래스를 상속 받아서 만들어지기 때문에 프록시 객체의 클래스와 진짜 클래스는 클래스 타입이 다르다.
Member member1 = em.find(Member.class, member.getId());
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println(member1.getClass().getName());
System.out.println(refMember.getClass().getName());
실행 결과
다음 결과를 보면 find로 찾은 객체의 클래스와 getReference로 찾은 객체는 클래스가 다른 것을 볼 수 있다.
프록시 객체를 초기화 하는 과정을 알아보자.
다음은 나름대로 이해한 흐름을 정리한 것이다. 그림과는 조금 다를 수도 있다. 추후 변경될 수 도 있다.
여기서 프록시 객체도 영속 컨텍스트의 Map에 저장되는지는 확실하지 않다. 인프런 질문 글들을 보면 영속성 컨텍스트에서 관리된다고는 하지만, 명확히 Map에 저장된다고는 나오지 않는다.
이 부분은 추후에 확실해지면 다시 수정하도록 하겠다.
특징에 대해 정리해보자.
프록시 객체를 초기화하면 target에 실제 객체가 매핑되므로 두 번 초기화가 되지 않는다.
프록시 객체는 내부적으로 실제 엔티티를 상속해서 만들어지기 때문에 하나의 다른 클래스이다. 따라서 target 필드에 실제 객체가 매핑될 뿐 클래스가 바뀌진 않는다.
그렇다.
JPA에서는 한 트랜잭션 안에서 같은 객체를 사용할 때 완벽한 동일성을 보장한다. 따라서 먼저 찾은 객체가 프록시 객체면 find로 찾아도 프록시 객체. find로 실제 객체를 찾으면 getReference로 프록시 객체를 찾아도 실제 객체가 반환된다.
준영속 상태일 경우 더 이상 영속성 컨텍스트의 관리를 받지 않는 상황이다. 따라서 프록시 초기화를 할 수 없기 때문에 에러가 발생한다.
tx.begin();
try {
Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
em.clear();
System.out.println(refMember.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
실행 결과
다음과 같이 org.hibernate.LazyInitializationException 이 발생하게 된다.