본문 바로가기

Dev/JPA

RS) 프록시와 연관관계

프록시 기초

em.find() vs em.getReference()

  • em.find() : 데이터베이스를 통해 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    • 하이버네이트가 내부적으로 라이브러리를 사용해서 가짜 엔티티 객체를 준다

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐
    • 하이버네이트가 내부적으로 라이브러리를 사용해서 만든다.
  • 그러다보니 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 구분하지 않고 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(Target)을 보관한다.
    • 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

Target을 들고있는 프록시 객체

  • 프록시 객체는 처음 사용할 때 한번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 변경되는 것이 아니다.
    • 따라서 타입 체크할 때 주의해야한다.
    • 비교가 안되기 때문에 instance of 사용해야 한다.
  • 코드를 통해 살펴보자
  • Member member1 = new Member();
    member1.setUsername("member1");
    em.persist(member1);
    
    em.flush();
    em.clear();
    
    Member m1 = em.find(Member.class, member1.getId());
    System.out.println("m1 = " + m1.getClass());
    
    Member reference = em.getReference(Member.class, member1.getId());
    System.out.println("reference = " + reference.getClass());
    
    System.out.println("a == a: " + (m1 == reference));
    
    
    // 실행 결과
    
    Hibernate: 
        /* insert hellojpa.Member
            */ insert 
            into
                Member
                (createDateTime, INSERT_MEMBER, UPDATE_MEMBER, lastModifiedDate, TEAM_ID, USERNAME, MEBMER_ID) 
            values
                (?, ?, ?, ?, ?, ?, ?)
    Hibernate: 
        select
            member0_.MEBMER_ID as MEBMER_I1_3_0_,
            member0_.createDateTime as createDa2_3_0_,
            member0_.INSERT_MEMBER as INSERT_M3_3_0_,
            member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
            member0_.lastModifiedDate as lastModi5_3_0_,
            member0_.TEAM_ID as TEAM_ID7_3_0_,
            member0_.USERNAME as USERNAME6_3_0_,
            team1_.TEAM_ID as TEAM_ID1_7_1_,
            team1_.createDateTime as createDa2_7_1_,
            team1_.INSERT_MEMBER as INSERT_M3_7_1_,
            team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
            team1_.lastModifiedDate as lastModi5_7_1_,
            team1_.name as name6_7_1_ 
        from
            Member member0_ 
        left outer join
            Team team1_ 
                on member0_.TEAM_ID=team1_.TEAM_ID 
        where
            member0_.MEBMER_ID=?
            
    // 여기서 m1도 hellojpa.Member
    // reference도 hellojpa.Member
    // 이 두가지가 같은 경우는 우선 영속성 컨텍스트 1차캐시에 이미 실제 엔티티인 Member가 올라가있기 때문
    // 굳이 영속성컨텍스트가 디비에 들릴 필요가 없다는 뜻 (성능최적화를 위해)
    m1 = class hellojpa.Member
    reference = class hellojpa.Member
    
    // JPA에서는 같은 영속성 컨텍스트 안에서는 항상 같아야 한다.
    // JPA에서는 자바 컬렉션에서 가져오는 것처럼 항상 true를 만족해 줘야한다. (JPA 기본 매커니즘)
    a == a: true
    
    
    // 위와는 반대의 경우일 때
    Member refMember = em.getReference(Member.class, member1.getId());
    System.out.println("refMember = " + refMember.getClass());
    // 결과 : refMember = class hellojpa.Member$HibernateProxy$BVEj80y6
    
    Member findMember = em.find(Member.class, member1.getId());
    System.out.println("findMember = " + findMember.getClass());
    // 결과 : findMember = class hellojpa.Member$HibernateProxy$BVEj80y6
    // em.find 했을 때 실제 Member 엔티티를 가져올 거라고 생각되었지만..
    // 프록시가 만들어진다. 즉 em.find해도 프록시를 만들수도 있다는 뜻이다.
    // 위에서 프록시를 이미 만들어졌는데 JPA에서는 == 은 true를 만족시켜야하기때문에 프록시를 호출하는 것이다.
    
    System.out.println("a == a: " + (refMember == findMember));
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, em.getReference()를 호출해도 실제 엔티티 반환한다.
    • 영속성 컨텍스트에 찾는 엔티티가 없는 경우에 DB에 들려서 실제 엔티티를 만들고 target과 연결한다.

프록시의 동작과정

// 이 코드가 동작하는 과정을 살펴보자
// getReference를 통해 호출하고 아래에서 getName()을 호출했을 때..
Member member = em.getReference(Member.class, "id1");
member.getName();

 

프록시 동작과정

  1. getName()이라고 호출하게 되면
  2. 처음에 Member target에 값이 없기때문에 JPA가 영속성 컨텍스트에 요청을 한다.
  3. 영속성 컨텍스트가 DB를 조회해서 실제 Member 엔티티 객체를 만든다.
  4. 만들어진 진짜 엔티티를 target과 연결해준다.
  5. 연결된 target은 실제 엔티티와 연결되어 사용한다.
// 아래와 같이 코드를 작성하고 직접 호출하게 되면..
// 처음에 만들어지는 findMember는 프록시 객체이다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("findMember.username = " + findMember.getUsername());

실행 결과

Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createDateTime, INSERT_MEMBER, UPDATE_MEMBER, lastModifiedDate, TEAM_ID, USERNAME, MEBMER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)

// getUsername()을 호출하는 순간
// JPA가 영속성 컨텍스트에 요청해서 디비에 들려서 실제 객체를 만들라고 요청한다.
// 그리고 target과 연결시켜준다.
// 아래의 결과가 그 내용이다.
Hibernate: 
    select
        member0_.MEBMER_ID as MEBMER_I1_3_0_,
        member0_.createDateTime as createDa2_3_0_,
        member0_.INSERT_MEMBER as INSERT_M3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.createDateTime as createDa2_7_1_,
        team1_.INSERT_MEMBER as INSERT_M3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEBMER_ID=?

// hello가 두번찍히는 이유는
// 위에서 영속성 컨텍스트를 통해 실제 엔티티를 생성한 상태이고
// target과 연결되어있기 때문에 값을 가져올 수 있는 것이다.
findMember.username = hello
findMember.username = hello

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법 : entity.getClass().getName() 출력
  • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);

그림참조