8 min read

<JPA> Proxy Entity

만약 Member라는 엔티티와 Team이라는 엔티티가 존재하는 상황에서 Member를 조회할때 Team도 함께 조회할 수 있는 방법이 있을까요?

JPA에서는 위와 같은 과정을 Proxy와 연관관계의 옵션을 맺어서 해결해줍니다.

프록시가 무엇인지 부터 알아봅시다.

Proxy

우선 Proxy의 경우 Jpa에서 생성하는 진짜 Entity에 대한 가짜 Entity라고 이해하시면 됩니다.실제로 getReference()라는 메서드를 호출해서 프록시 객체를 만들어냅니다.코드를 보면서 만들어봅시다.

	Member member = new Member();
    member.setName("brido");
	em.persist(member);

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

	Member findProxyMember = em.getReference(Member.class, member.getId());

	System.out.println("findProxyMember = " + findProxyMember.getClass());
            System.out.println("findProxyMember.getId() = " + findProxyMember.getId());
            System.out.println("findProxyMember.getName() = " + findProxyMember.getName());

	tx.commit();

우선적으로 findProxyMember의 클래스 타입을 보시면 Member$HibernateProxy$unFM8ALS로 설정되어 있는데 예상하시다시피 하이버네이트가 생성한 Member 엔티티에 대한 가짜 엔티티입니다.

그 다음이 중요한데 findProxyMember의 getId는 select쿼리가 발생하기 전에 출력을 했고, getName의 경우 select 쿼리가 발생하고 난뒤에 출력되었습니다.이 코드에 프록시 객체가 진짜 객체와 맺는 관계가 담겨 있습니다.

프록시 객체의 경우 아래의 그림처럼 실제 클래스를 상속 받아서 만들어져 실제 클래스와 겉 껍데기가 동일합니다.그래서 사용자입장에서는 진짜 객체인지 가짜 객체인지 모르고 사용이 가능합니다.

그러면 프록시 객체를 호출할 경우 어떻게 진짜 객체에 접근할 수 있을까요?이는 프록시 객체가 실제 객체의 참조(Reference)를 보관하고 있기 때문에 가능해집니다.물론 프록시 객체가 생성될 당시에는 해당 참조는 null값으로 설정되어있습니다.하지만 해당 프록시를 통해 실제 객체를 참조하는 메서드를 호출하면 그때 null로 설정되어 있던 값이 실제 객체의 Reference로 바뀌게 됩니다.다만 여기서 호출하는 메서드가 영속성 컨텍스트에 없는 데이터에 접근해야 합니다.

예를 들어 위의 예제에서는 getId()가 아닌 getName()이 실제 객체의 Reference로 바뀌게 되는 시점입니다.왜냐하면 Id값의 경우 이미 getReference()메서드를 호출하면서 영속성 컨텍스트에 저장되었기 때문입니다.하지만 Name의 경우 영속성 컨텍스트에 존재하지 않기 실제 디비로 접근해 데이터를 꺼내야합니다.그래서 select 쿼리가 나간 후에 getName()에 대한 값이 출력되었던것입니다.아래의 그림은 지금까지 설명드린 proxy의 객체의 초기화 과정을 도식화한 그림입니다.

단계별로 정리해보겠습니다.

  1. Proxy 객체의 메서드들 중 디비에 접근해서 데이터를 가져와야하는 메서드를 호출합니다.
  2. 메서드를 호출받은 프록시 객체는 영속성 컨텍스트에 초기화 요청을 보냅니다.
  3. 영속성 컨텍스트에서 디비로 접근해 실제 값을 조회합니다.
  4. 영속성 컨텍스트 실제 Entity를 생성합니다.그리고 프록시 객체가 생성된 실제 Entity의 참조값을 null 대신 가지게 됩니다.
  5. Proxy 객체에서 참조를 통해 실제 엔티티의 메서드를 호출합니다.

사실 갑자기 Proxy 객체가 뭐니 어쩌니 너무 어렵다라고 느끼시는분들은 넘어가신뒤,추후에 Cascade나 Lazy 옵션 등을 사용은 하는데 정확히 뭐가 뭔지 모르겠다라고 생각이 드실때 프록시를 다시 공부하셔도 늦지 않으실것 같습니다.우선 JPA를 써서 서비스 만들어보시는게 훨씬 중요하다고 생각합니다!

이제 Proxy가 어떻게 JPA에서 작동되는지 알아보았습니다.지금부터는 유의점 몇가지를 짚어보겠습니다.

Proxy 객체 Class 타입 조회

아래의 코드를 실행 시켜봅시다.

	Member member = new Member();
    member.setName("brido");
	em.persist(member);

	Member member2 = new Member();
	member2.setName("brido2");
	em.persist(member2);

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

	Member findMember = em.find(Member.class, member.getId());
    Member findProxyMember = em.getReference(Member.class, member2.getId());
            System.out.println("findMember.getClass()==findProxyMember.getClass() = " + (findMember.getClass()==findProxyMember.getClass()));
    System.out.println("(findMember instanceof Member) = " + (findMember instanceof Member));
    System.out.println("(findProxyMember instanceof Member) = " + (findProxyMember instanceof Member));

너무나도 당연하게 Proxy객체와 실제 객체의 Class타입은 다르게 나옵니다.그래서 == 비교가 불가능한 상황이기에 instanceof 연산자를 사용해서 각각의 인스턴스의 타입을 확인해 볼 수 있습니다.

em.find <-> em.getReference 번걸아 가며 조회

이번에 실습할 코드는 이미 em.find나 em.getReference로 반환받은 멤버 엔티티에 대해 em.find면 em.getReference를 호출하고 그반대의 경우 반대로 호출해봅시다.

아래의 코드를 덧붙여 실행시켜봅니다.

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

    Member findMemberAfterProxy = em.find(Member.class, member2.getId());
            System.out.println("findMemberAfterProxy.getClass() = " + findMemberAfterProxy.getClass());

아래와 같은 결과가 발생합니다.

이미 member 인스턴스의 경우,em.find()를 통해 영속성 컨텍스트에 실제 엔티티로 저장을 해놓은 상황입니다.그래서 em.getReference()를 통해 Proxy 엔티티를 생성하려해도 이미 영속성 컨텍스트 실제 객체가 존재하기에 이의 참조값을 가져옵니다.실제로 동일한 주솟값을 가지는 객체임을 확인해 볼 수 있습니다.

  • findMember-> 가장 먼저 em.find()해서 얻은 멤버
  • findMemberAfterFind-> 위의 멤버와 동일한 id값이지만 em.getReference()

그러면 member2 인스턴스의 경우를 봅시다.em.getReference()를 통해 프록시 객체를 생성해놓은 상황에서 동일한 멤버를 em.find()를 조회하면 어떻게 될까요?이미 결과를 보셨겠지만 동일하게 Proxy 객체의 참조를 가집니다.

우선 영속성 컨텍스트에는 member2의 id값을 가져온 초기화 되지 않은 프록시 객체가 존재합니다.여기서 em.find()를 호출하면서 member2의 데이터들을 실제 디비에서 가져오면서 findProxyMember가 초기화 됩니다.이는 select 쿼리가 날라가는것으로부터 확인 가능합니다.실제로 클래스 타입을 찍어보면 아래와 같이 하이버네이트가 생성한 프록시임을 확인가능합니다.

  • findProxyMember->가장 먼저 em.getReference()해서 얻은 멤버
  • findMemberAfterProxy->위의 멤버와 동일 id지만 em.find()

Proxy 초기화 여부 확인하기

emf.getPersistenceUnitUtil().isLoaded()메서드를 사용하시면 현재 프록시 객체가 초기화 되었는지 확인 가능합니다.또한 Hibernate의 initialize() 메서드를 사용하시면 강제로 프록시 객체를 초기화 할 수도 있습니다.

	Member member2 = new Member();
            member2.setName("brido2");
            em.persist(member2);

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

    Member findProxyMember = em.getReference(Member.class, member2.getId());

   	System.out.println("findProxyMember is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(findProxyMember));
            Hibernate.initialize(findProxyMember);
    System.out.println("findProxyMember is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(findProxyMember));

    tx.commit();

이렇게 JPA가 Proxy를 다루는 법을 알아봤습니다.사실 Proxy는 이후에 나올 Cascade,Lazy 등의 옵션을 다루는 근간이 되는 개념입니다.그래서 이러한 개념들을 이해하기 위해서는 Proxy에 대한 이해가 필요합니다.하지만 말씀드린바와 같이 필요해지는 시점에 필요한 지식이니 먼저 JPA를 사용해보시며 무언갈 만들어보시는걸 추천드립니다.

Ref - 김영한님 - JPA 기본편