[자바 ORM 표준 JPA 프로그래밍 - 기본편] 8-1강


https://inf.run/2zDo 강의를 수강하고 작성하는 게시물입니다.


1. Member를 조회할때 Team도 조회해야할까?

아래와 같이 메서드를 만들어서 객체를 조회한다고 했을때 JPA는 Member를 조회할 때 Team도 함께 조회할까?

    private static void printMember(Member member) {
        System.out.println(member.getName());
    }

    private static void printMemberAndTeam(Member member) {
        System.out.println("username = "+member.getName());

        System.out.println("team = "+member.getTeam().getName());
    }

2. 프록시

JPA에는

em.find() 라는 메서드도 있지만

em.getReference() 라는 메서드가 존재한다.

find는 실제 엔티티 객체를 조회하는 메서드이다.

getReference는 데이터 베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회하는 메서드이다.

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();
        transaction.begin();
        try {

            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

            team.getMembers().add(member);

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

            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println("findMember.id = " +findMember.getId());
            System.out.println("findMember.username = "+findMember.getName());

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

위의 내용을 실행하면 아래와 같은 출력값을 얻을 수 있다.

id값을 print할때는 내가 넣은 값을 출력하는 것이므로 가져올 필요가 없었지만

name을 print할때는 실제로 객체의 데이터가 필요하므로 그때서야 쿼리를 실행하는 것을 알 수 있었다.

중략

findMember.id = 2
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_0_0_,
        member0_.USERNAME as username2_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        team1_.TEAM_ID as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.username = member1

중략

프록시 객체

만약 아래처럼 코드를 입력하면 어떻게 출력될까?

System.out.println("findMember = " +findMember.getClass());
findMember = class hellojpa.Member$HibernateProxy$AppaMBbR

위처럼 프록시 객체인 것을 알 수 있다.(가짜 엔티티 객체)

껍데기는 똑같은데 id값만 갖고있는 가짜 객체이다.

실제 클래스를 상속받아서 만들어지고 실제 클래스와 겉모양이 같다.

이론상으로는 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

프록시 객체의 초기화

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.username = "+findMember.getName());

위의 코드를 입력했을때 실제 내부 동작을 살펴보자.

  1. 클라이언트가 getName()을 호출했다.
  2. 프록시 객체가 영속성 컨텍스트에 초기화를 요청한다.
  3. DB를 조회한다.
  4. 실제 엔티티를 생성한다.
  5. 프록시 객체와 진짜 객체를 연결한다.

제일 중요한 부분은 영속성 컨텍스트에 초기화를 요청하는 것이다.

3. 프록시의 특징

  • 프록시객체는 처음 사용할때 한번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초 기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
    • 부연설명 : 실제 객체가 교체되는 것이 아닌 데이터만 채워지는 것이다.
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교가 아닌 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
    • 아래의 프록시 객체 비교시 주의할점 참고
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    • (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

프록시 객체 비교시 주의할점

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());

System.out.println("m1 == m2 : " +(m1.getClass() == m2.getClass()));

위는 당연히 true가 나온다.

그러나 아래처럼 비교하면 false가 나온다.


Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println("m1 == m2 : " +(m1.getClass() == m2.getClass()));

실무에서는 위처럼 눈에 보이는 것이 아닌 메서드로 받아올테니까 꼭 등호가 아닌 아래처럼 instanceof로 확인해야한다.

System.out.println("m1 == m2 : " +(m1 instanceof Member));
System.out.println("m1 == m2 : " +(m2 instanceof Member));

프록시와 객체 비교 예시 1

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

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

출력은 둘다 class hellojpa.Member 라고 나온다. 이미 원본을 갖고와서 프록시로 두는 것이 의미가 없기 때문이다.

실제 객체에서 같은 값이면 같은 객체이기 때문에 JPA는 같은 값이 나오도록 실제 엔티티를 반환한다.

프록시와 객체 비교 예시 2

Member ref1 = em.getReference(Member.class, member1.getId());
Member ref2 = em.getReference(Member.class, member1.getId());
System.out.println("ref1 == ref2 : " +(ref1.getClass() == ref2.getClass()));

만약 위와 같은 경우이면 어떨까?

결과는 true가 나온다. 둘 다 프록시 객체이다.

프록시와 객체 비교 예시 3

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

Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " +(m1.getClass()));
System.out.println("ref == m1 : " +(ref.getClass() == m1.getClass()));

위의 경우는 어떻게 될까? JPA는 무조건 두개가 참인 것을 보장해야한다.

위의경우는 둘다 프록시 객체로 표현된다. 심지어 두개가 같은 프록시이다. 그리고 값이 채워져있다.

영속성 컨텍스트의 도움을 받을 수 없다면

public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();
        transaction.begin();
        try {

            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

            team.getMembers().add(member);

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

            Member ref = em.getReference(Member.class, member.getId());
            System.out.println("findMember = " +ref.getClass());
            
            em.detach(ref);
            // 혹은 em.close();
            
            System.out.println(ref.getName());


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            System.out.println("e = "+e);
        } finally {
            em.close();
        }
        emf.close();
    }

System.out.println(ref.getName()); 라인을 실행할 때 아래와 같은 오류가 난다.

e = org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#2] - no Session

org.hibernate.LazyInitializationException 문구를 보면 프록시 문제라고 보면 된다. (JPA를 사용하면 무조건 만나게 되는 에러이다.)

4. 프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
    • 인스턴스가 초기화 되었으면 true / 아니면 false를 반환한다.
  • 프록시 클래스 확인 방법
    • entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
    • 무식하게 찍어보는 방법
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음
    • 강제 호출: member.getName()
// 프록시 인스턴스 초기화 여부 확인
System.out.println("isLoaded = "+emf.getPersistenceUnitUtil().isLoaded(ref))

// 프록시 강제 초기화
Hibernate.initialize(refMember);

5. 정리

실제 코딩할때 getReference를 굳이 사용하지 않는다.

즉시 로딩과 지연로딩의 초석이라고 보면된다!

댓글 남기기