[JPA] 프록시 (Proxy), getReference()

손효재·2022년 11월 10일
0

JPA

목록 보기
11/11
post-thumbnail

find와 getReference()의 차이를 학습하기위해 프록시에 대해 정리해봤습니다.

find() vs getReference()

결론부터 둘의 차이를 알아보면,

  • find : 데이터베이스를 통해 실제 엔티티를 조회
  • getReference() : 데이터베이스 조회를 미루는 프록시 엔티티 조회

먼저 테스트에 사용할 Member엔티티이다.
image

Member의 기본 정보가 있고, Team 엔티티를 매핑하고 있지만 사용하지 않을 예정

테스트 코드

	@Test
    @DisplayName("프록시 객체 테스트")
    public void proxyTest() {
        Member member = new Member("member1", 10);
        em.persist(member);

        em.flush();
        em.clear();

        Member refMember = em.getReference(Member.class, member.getId());
        System.out.println("refMember : " + refMember.getClass());
        System.out.println("refMember : " + refMember.getId());
        System.out.println("username : " + refMember.getUsername());
    }

image

테스트 결과 getReference로 조회하면 프록시 객체를 조회하고, 호출할때 이미 id를 가지고 있어서 쿼리가 나가지 않는다.
엔티티의 정보를 조회하면, 프록시 초기화를 하면서 데이터를 가져오기 때문에 select 쿼리가 나간다.

프록시 (Proxy)

이 프록시는 id값만 가지고 있고 target이 null로 된, 내부에는 비어있는 가짜 객체이다. 이때 target은 실제 객체를 가르키는 값이다.

실제 클래스를 상속받아 만들어져서, 실제 클래스와 겉모양이 같다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지않고 사용하면 된다.

프록시 객체의 초기화

image

초기 프록시 객체의 target은 null 이기때문에 프록시 객체에 값을 요청하면 값을 알 수 없다.
그래서 영속성 컨텍스트에 초기화를 요청하고, 영속성 컨텍스트는 DB를 조회해서 실제 엔티티 객체를 생성한이후 프록시객체의 target을 실제 객체에 연결시켜준다.
target으로 실제 객체와 연결되면, 실제 객체의 메서드를 호출한다.

프록시 특징

  • 프록시 객체는 처음 사용할때 한번만 초기화한다.
  • 프록시 객체를 초기화할때, 프록시 객체가 실제 엔티티로 바뀌는것이 아니다!!
    초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다.
  • 프록시 객체는 원본 엔티티를 상속받기 때문에, 타입체크시 주의해야한다!! ( == 대신 instance of를 사용하는게 좋다)
    * 프록시객체를 언제쓸지 모르니 JPA로 엔티티 타입비교시 가급적 instanceof 사용하는것도 방법
    -> 프록시 객체와 원본 객체는 == 비교시에 타입이 각각 프록시타입과 실제 타입으로 다르다!!
  • JPA는 하나의 트랜잭션 내에서 ==이 알맞게 동작하도록 상황에 따라 다르게 작동한다.
    ( JPA는 하나의 트랜잭션 내에서 같은 타입을 보장해준다. )

1. 실제 Entity 조회 후 프록시 객체를 조회하는 경우

	@Test
    @DisplayName("엔티티 조회 후 프록시 조회 테스트")
    public void proxyTest() {
        // given
        Member member = new Member("member1", 10);
        em.persist(member);

        em.flush();
        em.clear();

        // when
        Member findMember = em.find(Member.class, member.getId());
        System.out.println("findMember : " + findMember.getClass());

        Member refMember = em.getReference(Member.class, member.getId());
        System.out.println("refMember : " + refMember.getClass());

        // then
        assertThat(findMember.equals(refMember));
    }

findMember .getClass() → domain.Member (실제 객체)
reference.getClass() → domain.Member (실제객체)
findMember == reference → true

image

→ 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환된다.
이미 영속성 컨텍스트에 올라가있기때문에 굳이 프록시를 가져올 이유가없다.
성능최적화 입장에서도 영속성 컨텍스트에 올라간 실제 엔티티를 가져오면 된다.

2. 프록시 객체를 조회 후 실제 Entity를 조회하거나, 프록시객체를 초기화 하는 경우

	@Test
    @DisplayName("프록시 조회 후 엔티티를 조회")
    public void proxyTest2() {
        // given
        Member member = new Member("member1", 10);
        em.persist(member);

        em.flush();
        em.clear();

        // when
        Member refMember = em.getReference(Member.class, member.getId());
        System.out.println("refMember : " + refMember.getClass());

        Member findMember = em.find(Member.class, member.getId());
        System.out.println("findMember : " + findMember.getClass());

        // then
        assertThat(refMember.equals(findMember));
    }

refMember .getClass() → domain.MemberHibernateProxy(프록시객체)findMember.getClass()domain.MemberHibernateProxy (프록시 객체) findMember .getClass() → domain.MemberHibernateProxy (프록시 객체)
findMember .getClass() → domain.Member$HibernateProxy (프록시 객체)

image

1번과 반대의 상황으로, 프록시가 먼저 초기화됐으면, em.find()를 호출해도 프록시 객체가 반환된다

* 실제 Entity를 조회하면서 select 쿼리는 나가지만, 프록시 객체이다.

결론

이처럼 JPA는 한 트랜잭션 내에서 실제 엔티티 객체와 프록시 객체의 비교연산 동작의 완전성을 보장하기 위해 두 객체의 클래스 타입이 동일하다.

but, 동일한 트랜잭션이 아닌경우, ==을 사용한다면 상황에 따라 결과가 달라질 수 있기 때문에 instanceof 를 사용하는것이 좋다.

Proxy이든 실제 엔티티이든 상관없이 개발에 문제없게 개발하는것이 중요하다.

❗ 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생

프록시 초기화시 영속성 컨텍스트를 통해 초기화하게 되는데, 초기화 하기전에 영속성컨텍스트와 연결할 수 없는 경우

	@Test
    @DisplayName("프록시 초기화 전 영속성컨텍스트 에러")
    public void proxyTest3() {
        // given
        Member member = new Member("member1", 10);
        em.persist(member);

        em.flush();
        em.clear();

        // when
        Member refMember = em.getReference(Member.class, member.getId());
        System.out.println("refMember : " + refMember.getClass());

        em.detach(refMember); // 준영속상태로 변경
        // em.close(); // 영속성 컨텍스트 종료
        // em.clear(); // 영속성 컨텍스트 clear

        // then
        assertThatThrownBy(() -> refMember.getUsername())
        .isInstanceOf(LazyInitializationException.class);
    }

위 코드와 같이 em.detach()나 em.close(), em.clear()로 영속성 컨텍스트에서 detach시키거나 닫아서 준영속 상태에서 getUsername을 호출하면 LazyInitializationException 예외(could not initialize proxy)가 발생한다.

image

프록시 확인 메서드

  • EntityManagerFactory.getPersistenceUnitUtil.isLoaded(Object entity) - 프록시 인스턴스의 초기화 여부 확인
  • entity.getClass() 출력 - 프록시 클래스 확인 방법
  • Hibernate.initialize(entity) - 프록시 강제 초기화

참고로 JPA 표준은 강제 초기화가 없어서 강제로 호출해야한다. 강제 호출 ex) member.getName()


참고

0개의 댓글