8. 프록시와 연관관계 관리 (프록시)

HotFried·2023년 10월 2일
0

Member를 조회할 때 Team도 함께 조회해야 할까?

Case1) Member와 Team을 모두 조회하고 싶다.

public void printMemberAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();

    System.out.println("회원 이름: " + member.getUsername());
    System.out.println("소속팀: " + team.getName());
}

Case2) Member만 조회하고 싶다.

public void printUser(String memberId) {
        Member member = em.find(Member.class, memberId);
        Team team = member.getTeam();

        System.out.println("회원 이름: " + member.getUsername());
}

이 경우 Team의 정보를 매번 가져오면 리소스가 낭비된다.
-> 이를 해결하기 위해 프록시를 이해해야 한다.


프록시 기초

em.find()

Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

//
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());

tx.commit();

find()만 했음에도 select Query 가 나가는 것을 확인할 수 있다.


em.getReference()

em.find()대신 em.getReference()를 이용하면 어떤 결과가 나올까?

Member findMember = em.getReference(Member.class, member.getId());
Hibernate:
	call next value for hibernate_sequence
Hibernate:
	/* insert hellojpa.Member
    	*/ insert
    	into
        	Member
            (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID)
        values
        	(?, ?, ?, ?, ?, ?)

em.find()와 같이 insert Query만 나간다.
select Query는 나가지 않는다.
-> em.getReference() 이후

System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());

코드를 실행하면

findMember.id = 1
/*
/select 쿼리가 나간다.
/*

getReference 코드를 실행할 때 member.getId() 를 파라미터로 이용했기 때문에 Id는 바로 출력이 된다.

이후 findMember객체가 가지고있지 않은 정보인 Username을 호출할 때 select Query가 나가게 된다.

System.out.println("회원 이름: " + member.getClass());

Class를 조회해보면

findMember = class hellojpa.Member$HibernateProxy$ocdBHpyj

Proxy타입인 것을 확인할 수 있다.

  • 하이버네이트가 강제로 만든 가짜 클래스라는 뜻이다.

  • em.getReference는 프록시를 사용한다.
    - 진짜 객체가 아닌 텅텅 빈 가짜 객체인 프록시를 준다는 뜻이다.

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐
    -> 실제 클래스와 겉 모양이 같다.
  • 하이버네이트가 내부적으로 라이브러리를 사용해 상속한다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다 (이론상)

  • 프록시 객체는 실제 객체의 참조(target)을 보관한다.
  • 프록시 객체를 호출하면, 프록시 객체는 실제 객체의 메서드를 호출한다.

Question)
getId()를 호출하면 프록시는 target에 있는 getId()를 호출한다.
하지만 처음에는 DB 조회가 되지 않은 상태이므로 target이 비어있을 것이다. 이 때 무슨 일이 벌어질까?


프록시 객체의 초기화

public void getMemberName() {
        // 프록시 객체를 가져온다.
        Member member = em.getReference(Member.class, "id");
        
		// 1. 프록시에 getName()메서드를 요청한다.
        // 2. 프록시가 영속성 컨텍스트에 초기화를 요청한다.
        // 3. 영속성 컨텍스트가 DB를 조회한다.
        // 4. 영속성 컨텍스트가 실제 Entity를 생성한다.
        // 5. target의 getName()메서드가 실행된다.
        member.getName();
    }
  1. 프록시에 getName()메서드를 요청한다.
  2. 프록시가 영속성 컨텍스트에 초기화를 요청한다.
  3. 영속성 컨텍스트가 DB를 조회한다.
  4. 영속성 컨텍스트가 실제 Entity를 생성한다.
  5. target의 getName()메서드가 실행된다.

주의사항

1. 프록시 객체는 처음 사용할 때 한 번만 초기화

public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);
        
        System.out.println("회원 이름: " + member.getUsername());
        System.out.println("회원 이름: " + member.getUsername());
        System.out.println("회원 이름: " + member.getUsername());
    }

처음 member.getUsername()) 코드 실행 시 select Query가 나가고
그 이후는 Entity에 정보가 담겨있으므로 별도의 Query없이 바로 값이 출력된다.


2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능하다.

public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);

        System.out.println("before: " + member.getClass());
        System.out.println("회원 이름: " + member.getUsername());
        System.out.println("after: " + member.getClass());  // before와 같은 값 출력
    }

select Query가 나가면서 프록시가 실제 Entity로 바뀌는 것은 아니다.
초기화가 되면 프록시 객체를 통해 실제 Entity에 접근 할 수 있는 것 뿐이다.
Class의 값은 Proxy로 동일하다.


3. 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (==비교 실패, 대신 instance of 사용)

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

// find()로 가져왔고 타입을 정확하게 비교하는 것이므로 true를 출력한다.
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));

Member m3 = em.getReference(Member.class, member1.getId());
Member m4 = em.find(Member.class, member2.getId());

// m3는 getReference()이므로 false를 출력한다.
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));

tx.commit();

em.find는 Entity, em.getReference는 Proxy를 반환하므로 위 예제는 쉽게 이해할 수 있다.
하지만 비즈니스에서는 비교하는 로직을 Class를 만들어서 이용하기 때문에
logic(member1, member2)와 같은 코드를 보고 쉽게 판단할 수 없다.
따라서 == 대신 instance of를 사용하자.


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

public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

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

        Member m1 = em.find(Member.class, member1.getId());
        // find()로 진짜 객체를 가져오기 때문에 'Member'라고 출력된다.
        System.out.println("m1 = " + m1.getClass());

        // find()후 getReference()
        Member m2 = em.getReference(Member.class, member1.getId());
        // 'Proxy'가 아니라 'Member'로 출력된다.
        System.out.println("m2 = " + m2.getClass());

        // 프록시든 아니면 한 영속성 컨텍스트에서 가져오고 PK가 같다면 항상 true가 된다.
        System.out.println("m1 == reference = " + (m1 == reference));

        tx.commit();
    }

m1 : member1을 em.find()를 통해 호출
m2 : 호출되어 영속성 컨텍스트에 존재하즞 member1을 em.getReference()를 통해 호출

-> getReference()는 프록시를 반환하지만, 찾는 엔티티가 이미 영속성 컨텍스트에 존재하면 프록시가 아닌 실제 엔티티를 반환한다.
또한 원본과 레퍼런스를 ==비교 시 항상 true가 반환된다.


원본과 레퍼런스를 ==비교 시 항상 true가 반환되는 것이 매우 중요하다.

  1. em.find()em.find()
    -> Member, Member 반환

  2. em.find()em.getReference()
    -> Member, Member 반환 (영속성 컨텍스트에 찾는 엔티티가 이미 있으므로 실제 엔티티 반환)

  3. em.getReference()em.getReference()
    -> Proxy, Proxy 반환

  4. em.getReference()em.find()
    -> Proxy, Proxy 반환
    항상 == 비교 시 true를 보장해줘야한다.
    따라서, 레퍼런스를 얻고 em.getfind() 코드가 실행될 때 select Query가 작동하지만 결국 Proxy를 반환하는 것이다.


5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

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

        // 프록시 생성
        Member refMember = em.getReference(Member.class, member1.getId());

        // detach(), clear(), close()로 영속성 컨텍스트를 준영속으로 만든다.
        em.close();

        // 실제 데이터로 초기화 하면서 데이터를 가져와야 하지만
        // 영속성 컨텍스트로 관리하지 않게 되면서 exception이 떨어진다.
        refMember.getUsername();
        System.out.println("refMember = " + refMember.getClass());

        tx.commit();
    }

예외 발생 시 getstacktrace()를 통해 예외를 출력하는 코드 또한 작성했다고 가정한다.

위 코드에서 refMember.getUsername()이 실행될 때 프록시가 초기화 되어야한다.
초기화가 될 때는 영속성 컨텍스트를 통해 실제 Entity를 만들어낸다.
준영속 상태일 때 영속성 컨텍스트의 도움을 받을 수 없기 때문에 예외가 발생한다.

org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1]

프록시 유틸리티 메서드

1. 프록시 인스턴스의 초기화 여부 확인

public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory();
        ...
        // 앞에서 초기화 했다면 true, 아니라면 false
        System.out.println("isLoaded: " + emf.getPersistenceUnitUtil().isLoaded(refMember));
    }

2. 프록시 클래스 확인 방법

entity.getClass().getName()

3. 프록시 강제 초기화

public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

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

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

        // 이렇게 강제 호출하지말고
        refMember.getUsername();
        // initialize 메서드를 사용하자
        Hibernate.initialize(refMember);

        tx.commit();
    }

org.hibernate.Hibernate.initialize(entity)

  • Hibernate에서 제공하고, JPA 표준은 강제 초기화가 없다.
    JPA에서는 member.getName()처럼 강제로 호출 해야 한다.

참고 :

김영한. 『자바 ORM 표준 JPA 프로그래밍』. 에이콘, 2015.

자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
꾸준하게

0개의 댓글