DEV ℧ Developer Diary

[JPA] 프록시

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

프록시

프록시에 대해 알아보기 전에 다음과 같은 상황에 대해 가정해보자.

앞서 만들었던 Member 예시에는 Team에 대한 연관관계가 설정되어 있었다.

프록시기초1

여기서 만약 Member Entity를 조회한다고 하자. 그런데 Team은 필요없고 Member의 정보만 필요할 경우에는 Team에 대한 정보도 조회해야 할 필요가 있을까?

  • 회원과 팀 출력
public void printUserAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 정보 : " + member.getUsername());
    System.out.println("팀정보 : " + team.getName());
}
  • 회원만 출력
public void printUser(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 정보 : " + member.getUsername());
}

JPA에서는 해당 상황을 프록시나, 지연로딩을 통해서 해결을 한다.

JPA에서 제공하는 해당 상황의 해결방안들을 알아보도록 하자.

프록시

프록시의 기초

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

프록시기초1

entityManager.find() vs entityManager.getReference()

  • entityManager.find()

아래와 같이 join 쿼리문과 Member에 해당하는 객체 참조정보를 가져온다.

select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?
member class : class com.dhaudgkr.jpa05.domain.Member
회원 정보 : member1
팀정보 : TeamA
  • entityManager.getReference()

Member Entity에 해당하는 객체정보가 아닌, JPA에서 자체적으로 만든 Proxy라는 객체를 가져온다.

member class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$oLU1f5H5
회원 정보 : member1
팀정보 : TeamA

여기서 프록시란? JPA에서 만든 해당 객체의 껍데기 객체라고 보면된다. 그러면 프록시에 대해 알아보자.

프록시의 특징

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

프록시특징1

  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

프록시특징2

프록시 객체의 초기화

Member member = entityManager.getReference(Member.class, "id1");
member.getName();

프록시특징3

  1. getReference 를 이용해 Proxy 객체를 가져올 경우 JPA는 해당 Entity의 Proxy객체를 가져옴
  2. member.getName();을 이용해 실제 Entity 조회를 요청할 경우 JPA가 영속성 컨텍스트에 초기화를 요청
  3. DB에서 Member에 대한 데이터 조회
  4. 실제 Entity를 생성해서 Proxy target에 실제 Entity를 연결해 준다.
  5. 이후 target.getName() 을 이용해 실제 Entity의 정보를 가져온다.

프록시의 특징 (중요)

  • 프록시 객체는 처음 사용할 때 한번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

위의 항목에 대해 부가설명을 위해 예시를 추가하면,

Member findMember = entityManager.getReference(Member.class, member.getId());
System.out.println("before member class : " + findMember.getClass());
System.out.println("회원 정보 : " + findMember.getUsername());
System.out.println("after member class : " + findMember.getClass());

해당 findMember는 Member Entity를 getReference를 이용해 Proxy 객체를 참조해왔다. 하지만, 중간에 findMember.getUsername()을 이용해 DB를 통해 실제 Entity와 연결을 해준다 하더라도, findMember객체는 member객체가 아닌, Proxy객체의 정보를 참조해 온다는 뜻이다.

위의 예제코드를 실행하면 아래와 같다.

before member class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$UwQjhJmS
15:53:13.180 [main] DEBUG org.hibernate.SQL -
    select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?
회원 정보 : member1
after member class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$UwQjhJmS
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)

예시를 들어보자. Member Entity 간에 == 을 이용해 비교할시 해당 객체가 Entity인지 Proxy인지 구분이 필요하다.

  • find 끼리 비교시
Member findMember1 = entityManager.find(Member.class, member1.getId());
Member findMember2 = entityManager.find(Member.class, member2.getId());

System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2));
findMember1 == findMember2 : true
  • find, getReference와 비교시
Member findMember = entityManager.find(Member.class, member1.getId());
Member referenceMember = entityManager.getReference(Member.class, member2.getId());

System.out.println("findMember == referenceMember : " + (findMember.getClass() == referenceMember.getClass()));
System.out.println("referenceMember instanceof Member : " + (referenceMember instanceof Member));
findMember == referenceMember : false
referenceMember instanceof Member : true

find로 가져온 실제 Entity와, Proxy객체가 다르기 때문에 ==으로 비교시 false의 결과를 반환한다. (거의 사용할 일 없다고 한다…)

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, entityManager.getReference()를 호출해도 실제 엔티티 반환

해당 항목은 헷갈릴 수 있는 항목이니 주의해주자. 먼저 예시를 들어주자.

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

Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember class : " + referenceMember.getClass());
findMember class : class com.dhaudgkr.jpa05.domain.Member
referenceMember class : class com.dhaudgkr.jpa05.domain.Member

getReference 를 통해 Member를 조회해와도 Proxy가 아닌 Member Entity를 반환한다.

이유는 무엇일까??

크게 두가지 이유가 있다. 먼저 Member 객체를 find해와서 영속성 컨텍스트와 1차 캐시 안에 있을경우 굳이 Proxy 객체를 가져올 필요가 없으므로, 1차 캐시에 있는 Member Entity를 반환한다. 두번째인 진짜 이유는, JPA는 한 트랜잭션 내에서는 같은 엔티티를 보장해준다. 그래서 같은 entity를 가져온다고 보면 된다.

따라서 findMember객체와 referenceMember 객체는 같아야 한다. 아래의 예제를 살펴보자.

System.out.println("findMember == referenceMember : " + (findMember == referenceMember));
findMember == referenceMember : true

같은 예시로 같은 Entity에 대한 proxy 객체를 비교해도 JPA는 한트랜잭션내에 데이터의 동일성을 보장하므로 같은 주소값을 가져온다.

Member referenceMember1 = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember1 class : " + referenceMember1.getClass());

Member referenceMember2 = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember2 class : " + referenceMember2.getClass());
# 주소값이 같다!
referenceMember1 class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$G6g8pNCg
referenceMember2 class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$G6g8pNCg

마지막 일례로, getReference를 통해 Proxy를 먼저 조회하고, find를 통해서 실제 Entity를 통해서 조회한다면 어떻게 될까?

Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("findMember class : " + referenceMember.getClass());

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

System.out.println("referenceMember == findMember : " + (findMember == referenceMember));
referenceMember class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$GwZvQt97
Hibernate:
    select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?
findMember class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$GwZvQt97
referenceMember == findMember : true

동일성 보장을 위해 find Member를 Proxy 객체로 가져왔다.

이렇게 정리하자면, JPA를 통해 데이터를 불러들여올때는, Proxy와 일반 Entity에 대해 구분하지 않고 사용하고자 하는게 JPA의 의도이다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.)
Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember class : " + referenceMember.getClass());

entityManager.detach(referenceMember);

System.out.println("referenceMember username : " + referenceMember.getUsername());

먼저 referenceMember를 통해 해당 객체의 Proxy를 조회해오자. 이후에 entityManager.detach(referenceMember); 를 통해 해당 Proxy객체를 영속성 컨텍스트 에서 제거한 후 username을 조회해오면,

org.hibernate.LazyInitializationException: could not initialize proxy [com.dhaudgkr.jpa05.domain.Member#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
	at com.dhaudgkr.jpa05.domain.Member$HibernateProxy$xaGaC3eW.getUsername(Unknown Source)
	at com.dhaudgkr.jpa05.JpaMain.main(JpaMain.java:34)

위와 같은 예외가 발생한다.

이는 entityManager.detach(referenceMember); 뿐만 아니라 entityManager.close();entityManager.clear();를 통해 영속성 컨텍스트를 닫거나, 초기화 할 경우도 같은 예외가 발생한다.

이와 같은 예외는 실무에서 영속성 컨텍스트는, 트랜잭션에 맞춰 트랜잭션이 끝날경우 영속성 컨텍스트도 끝나도록 맞추므로, 트랜잭션이 끝날때 위와 같은 예외가 발생할 수 있다. 위와 같은 상황이 생길 경우 프록시의 트랜잭션이나, 영속성 컨텍스트에 대한 특징을 다시한번 상기할 수 있도록 유의하자.

프록시 확인 Util

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("isLoaded : " + entityManagerFactory.getPersistenceUnitUtil().isLoaded(referenceMember));

referenceMember.getUsername();
System.out.println("isLoaded : " + entityManagerFactory.getPersistenceUnitUtil().isLoaded(referenceMember));
isLoaded : false
Hibernate:
    select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?
isLoaded : true
  • 프록시 클래스 확인 방법
    • entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember class : " + referenceMember.getClass());
referenceMember class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$G6g8pNCg
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);
Member referenceMember = entityManager.getReference(Member.class, member1.getId());
System.out.println("referenceMember class : " + referenceMember.getClass());

Hibernate.initialize(referenceMember);
referenceMember class : class com.dhaudgkr.jpa05.domain.Member$HibernateProxy$FydjtHJ3
16:47:56.580 [main] DEBUG org.hibernate.internal.SessionImpl - Initializing proxy: [com.dhaudgkr.jpa05.domain.Member#1]
Hibernate:
    select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?

Hibernate.initialize(entity); 를 사용할 경우 특별한 Entity 내부 호출 없이도 조회 쿼리가 날라가는 것을 확인 할 수 있다.

  • 참고: JPA 표준은 강제 초기화 없음
    • 강제 호출: member.getName()