Spring/JPA

프록시와 연관관계 관리 - 프록시

Ynghan 2023. 2. 9. 19:47

목차

  • 프록시
  • 즉시 로딩과 지연 로딩
  • 지연 로딩 활용
  • 영속성 전이 : CASCADE
  • 고아 객체
  • 영속성 전이 + 고아 객체, 생명주기
  • 실전 예제 - 5. 연관관계 관리

프록시

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

1) Member와 Member가 속한 Team을 출력하는 로직에서는 Member를 persist할 경우 Team과 함께 db query를 보내는 것이 좋다.

2) Member만 출력하는 로직은 Member를 persist할 경우 Team과 함께 db query를 보내는 것은 손해다. 

 

JPA는 지연 로딩프록시를 통해 이 문제점을 해결한다.
이 해결 방식을 이해하기 위해서는 프록시를 명확하게 알고 있어야 한다.

프록시 기초

JPA에서는 em.getReference()라는 흥미로운 메소드를 제공한다.

  • em.find() vs em.getReference()
  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    • db에 query가 나가지 않았는데 데이터가 조회된다.

JpaMain.java

        try{
            Member member = new Member();
            member.setUsername("hello");
            em.persist(member);

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

위 코드의 경우 em.find를 하는 시점에 select 쿼리가 나간다. 

 

 try{
            Member member = new Member();
            member.setName("hello");
            em.persist(member);

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

em.getReference(Member.class, member.getId()); 실행 시 오류.

 try{
            Member member = new Member();
            member.setUsername("hello");
            em.persist(member);

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

하지만 이 경우에는 findMember.getUsername() 시점에 select 쿼리 생성.

getId() 시점에는 쿼리를 생성하지 않는다. getReference() 시점에 이미 Id 값이 있기 때문이다.

 

System.out.println("findMember = " + findMember.getClass());

 

findMember의 정체는 가짜 클래스이다.

 

 

 

 

프록시 특징

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

 

프록시 객체의 초기화

 

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

 

 

 

 

getName의 값이 없다면 영속성 컨텍스트에 요청(초기화)한다.

영속성 컨텍스트는 DB에서 값을 가져온 후 실제 엔티티를 생성하고 프록시 객체의 참조 변수(target)에 실제 엔티티를 매핑을 시켜준다. 그래서 getName을 호출했을 경우 target의 getName을 사용하여 반환해준다.

 

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함( == 비교 실패(동일성 비교 실패), 대신 instance of 사용(동등성 비교 사용) )
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
    • 이미 영속성 컨텍스트에 있으면 실제 엔티티를 반환해준다.
    • 영속성 컨텍스트에 들어가 있는데 굳이 프록시를 반환해봐야 이점이 없다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
JPA에서는 하나의 영속성 컨텍스트에서 pk가 같은 객체를 가져오면 항상 true를 반환해줘야 한다.

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력(..javasist.. or HibernateProxy...)
  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);
  • 참고 : JPA 표준은 강제 초기화 없음
    강제 호출 : member.getName()