1일1배움/JPA (김영한 님)

23/12/27 [em.find() 와 em.getReference() 의 차이]

kim chan jin 2023. 12. 27. 19:58
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) // 기본 전략이 auto
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    
    // getter, setter 생략
}

 

위와 같은 엔티티 클래스가 있다 가정하자

 

Member memberEntity = em.find(Member.class, member.getId()) 를 호출하면

호출한 바로 그 시점에 바로 영속성 컨텍스트 1차 캐시에 데이터가 저장되어 있는지 찾아보고

만약 없다면 DB에 SELECT 쿼리 보낸다 

 

반면, Member memberProxy = em.getReference(Member.class, member.getId()) 를 호출하면 

호출한 시점에 엔티티 객체를 상속받은 프록시 객체를 만들고 영속성 컨텍스트 1차 캐시에 저장한다. 

이후 memberProxy.getUsername() 으로 개발자가 프록시 객체를 사용하여 메서드를 호출하면 (1)

그때서야 영속성 컨텍스트 1차 캐시에 프록시 캐시의 target 필드 초기화를 요청한다. (2)

영속성 컨텍스트 1차 캐시에 엔티티 객체가 저장되어 있는지 찾아보고 존재한다면 엔티티 객체를 반환하고

(반대로 만약 1차 캐시에 프록시 객체가 있는 상태에서 엔티티 객체를 조회하면 프록시 객체가 반환된다!)

존재하지 않다면 DB에 SELECT 쿼리 보내고 반환받아 id : 엔티티 객체 형식으로 엔티티 객체를 1차 캐시에 저장한다 (3)

이후 엔티티 객체가 생성되고 프록시 객체의 target 필드가 초기화된다. (4)

이후 실제 엔티티 객체의 메서드가 호출된다 (5)

 

즉, 정리하자면

프록시 객체를 사용하는 이유는 실제 메서드를 사용하기 전까지 껍데기로써 존재하게 하여 쿼리를 미루기 위함이다

프록시를 사용하기 위해서는 

 

아래는 실습 코드이다.

김영한 님의 JPA 강의 1편의 예제코드를 조금 수정하였다

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); // persistence.xml
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Member member = new Member();
            member.setUsername("kim");

            em.persist(member);
            System.out.println("============== find() ==============");
            em.find(Member.class, member.getId());

            em.flush(); // INSERT 쿼리만 실행 -> 영속성 컨텍스트 1차 캐시에 데이터 저장 -> SELECT 쿼리 실행 X
            em.clear();

            System.out.println("============== getReference() ==============");
            Member referenceMember = em.getReference(Member.class, member.getId()); // 프록시 객체(엔티티를 상속받은 껍데기) 반환 (아직 쿼리 X)

            System.out.println("============== getReference() 이후 실제 첫번째 사용 ==============");
            System.out.println("member.getUsername() : " + referenceMember.getUsername()); // 영속성 컨텍스트에 초기화 요청 -> DB에 쿼리 O

            System.out.println("============== getReference() 이후 실제 두번째 사용 =============="); // 영두

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

아래는 출력 모습이다

... DDL 생략

Hibernate: 
    
    alter table Movie 
       add constraint FK5sq6d5agrc34ithpdfs0umo9g 
       foreign key (id) 
       references Item
Hibernate: 
    call next value for hibernate_sequence
============== find() ==============
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
============== getReference() ==============
============== getReference() 이후 실제 첫번째 사용 ==============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_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_.createdBy as createdB2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.lastModifiedBy as lastModi4_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_.MEMBER_ID=?
member.getUsername() : kim
============== getReference() 이후 실제 두번째 사용 ==============

 

 

주의할 점

1. 프록시 객체는 딱 한번만 초기화.

 

2. 프록시 객체를 초기화했다고 해서 프록시 객체가 엔티티 객체로 바뀌는 것은 아님. 

 

3.

프록시 객체는 엔티티 객체를 상속받아서 target 필드만 추가한 것임.

따라서 memberProxy.getClass() 와 memberEntity.getClass() 의 결과값은 다름.

따라서 타입 비교할 때 == 비교가 아니라 instanceof 를 써야 함

    - memberEntity instanceof Member

    - memberProxy instanceof Member

 

4.

만약 1차 캐시에 엔티티 객체가 존재한다면 em.getReference() 하더라도 프록시 객체가 아니라 엔티티 객체가 반환됨.

왜냐하면 JPA는 하나의 트랜잭션 단위 안에서 동일한 객체에 대해서 동일성을 보장하기 때문임.

Member member = new Member();
member.setUsername("kim");
em.persist(member);

em.flush(); // INSERT 쿼리
em.clear(); // 1차 캐시 비움

Member memberReference = em.getReference(Member.class, member.getId()); 
// getReference() : 프록시 객체 생성 후 1차 캐시 저장
Member memberEntity = em.find(Member.class, member.getId()); 
// find() : 엔티티 객체가 아니라 프록시 객체 반환

System.out.println(memberEntity.getClass());
System.out.println(memberReference.getClass());
            
// 출력
// class hellojpa.Member$HibernateProxy$vgKEjswj
// class hellojpa.Member$HibernateProxy$vgKEjswj

 

5.

만약 1차 캐시에 프록시 객체가 존재한다면 em.find()로 하더라도 엔티티 객체가 아니라 프록시 객체가 반환됨.

왜냐하면 JPA는 하나의 트랜잭션 단위 안에서 동일한 객체에 대해서 동일성을 보장하기 때문임.

Member member = new Member();
member.setUsername("kim");
em.persist(member); // 1차 캐시에 엔티티 객체 저장

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

Member memberReference = em.getReference(Member.class, member.getId()); 
// getReference() : 프록시 객체 생성 후 1차 캐시 저장. 하지만 1차 캐시에 이미 저장되어있는 엔티티 객체 반환
Member memberEntity = em.find(Member.class, member.getId()); 
// find() : 엔티티 객체 반환

System.out.println(memberEntity.getClass());
System.out.println(memberReference.getClass());
            
// 출력
// class hellojpa.Member
// class hellojpa.Member

 

6.

Member memberProxy = em.getReference() 호출하여 1차 캐시에 프록시 객체를 저장된 상태에서

em.detach(meberProxy) 또는 em.close() 호출하면 이후에 프록시 기능 사용 불가 

왜냐하면 프록시 객체가 1차 캐시에 저장되어 있는 상태여야 프록시를 초기화하여 프록시 기능을 사용할 수 있기 때문

 

 

프록시 관련 메서드

1. 프록시 객체 초기화 여부 확인 메서드

emf.PersistenceUnitUtil.isLoaded(memberProxy)

*emf = EntityManagerFactory 인스턴스

 

2. 프록시 클래스 확인 메서드

memberProxy.getClass()

 

3. 프록시 강제 초기화 메서드

Hibernate.initialize(memberProxy)