🌱 Spring/JPA

[JPA] 프록시

an2z 2022. 10. 18. 00:41
본 포스팅은 인프런 - 자바 ORM 표준 JPA 프로그래밍 (기본편) 을 강의를 바탕으로 공부하고 정리한 글입니다.

 

프록시


JPA를 사용하면 객체 그래프를 통해 연관관계를 탐색할 수 있다는 장점이 있다.

그런데 엔티티들은 데이터베이스에 저장되어 있기 때문에 하나의 객체를 조회할 때 그와 연관되어 있는 엔티티들을 모두 조회하는 것보다는

필요한 연관관계만 조회해 오는 것이 효과적일 것이다.

 

예를 들어 Member와 Team이 있을 때, Member 조회 시 항상 Team의 정보도 함께 조회해야 할까?

실제 필요한 비즈니스 로직에 따라 다르다.

  • Member와 Team을 거의 항상 같이 사용하는 경우
  • Member와 Team을 거의 같이 사용하지 않고, Member만 출력하는 경우가 많으며 Team은 정말 어쩌다 호출하는 경우

비즈니스 로직 상 멤버의 username만 필요한데, 항상 팀에 대한 정보까지 같이 가져와서 사용하는 것은 낭비다.

 

JPA는 이러한 상황을 위해 프록시지연로딩이라는 개념을 사용한다.

 

 

프록시 기초

프록시는 "대신하다"라는 의미로, 동작을 대신해주는 가짜 객체이다.

  • 하이버네이트는 지연 로딩을 구현하기 위해 프록시를 사용한다.
  • 지연 로딩을 하려면 연관된 엔티티의 실제 데이터가 필요할 때 까지 조회를 미뤄야 하는데, 이때 연관관계로 가지고 있는 엔티티 필드에 null 값을 넣어둘 수는 없기 때문에 그 자리에 프록시 객체를 주입하여 실제 객체가 들어있는 것처럼 동작하도록 한다.
  • 따라서 개발자는 연관관계 자리에 프록시 객체가 들어있든 실제 객체가 들어있든 신경쓰지 않고 사용할 수 있는 것이다.

 

JPA에서는 프록시 객체를 호출할 수 있는 em.getReference()라는 메서드를 제공한다.

  • em.find() : 데이터 베이스를 통해서 실제 엔티티 객체를 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회

💻 em.find()

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

em.persist(member);

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

Member findMember = em.find(Member.class, member.getId()); // 실제 엔티티 객체 조회
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_.user_name as user_nam6_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=?
findMember.class() = class hellojpa.Member

em.find()로 멤버를 조회하면 데이터베이스에 조회 쿼리(select)가 나가고 실제 엔티티 객체가 조회된다.

 

💻 em.getReference()

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

em.persist(member);

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

Member findMember = em.getReference(Member.class, member.getId()); // 프록시 엔티티 객체 조회
System.out.println("findMember.class() = " + findMember.getClass());
findMember.class() = class hellojpa.Member$HibernateProxy$gwS4XRep

그러나 em.getReference()로 멤버를 조회하면 조회 쿼리(select)가 나가지 않고 하이버네이트가 강제로 만든 프록시(가짜) 객체가 조회된다.

이 경우 실제로 정보가 필요한 시점에 데이터베이스에 쿼리가 나간다.

 

   

프록시 특징

  • 프록시는 실제 클래스를 상속 받아 만들어진다.
  • 실제 클래스와 겉 모양이 같기 때문에 프록시가 실제 객체처럼 동작할 수 있다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체의 메서드를 호출하면 프록시 객체를 통해 실제 객체의 메소드를 호출한다.
💡 프록시 객체가 실제 객체의 상속본인 것은 JPA 엔티티 생성의 다음 규칙을 만들어 냈다.
• 기본 생성자는 최소 protected 접근 제한자를 가져야 한다.
   → 기본 생성자가 private이면 프록시 생성시 super를 호출할 수 없음
• 엔티티 클래스는 final로 정의할 수 없다.
  → final로 정의할 경우 상속이 불가능

 

프록시 객체의 초기화

Member member = em.getReference(Member.class, "id1"); // 프록시 조회
member.getName; // 프록시 객체 초기화 (실제 조회 쿼리 나감)

  • 최초 프록시 객체 조회 시점에는 프록시 내부 target에 참조값이 없다.
  • 실제 객체의 메서드를 호출하는 시점에 target이 없다면, JPA는 영속성 컨텍스트에 프록시 객체 초기화를 요청한다.
  • 영속성 컨텍스트는 초기화 요청을 받으면 데이터베이스에소 실제 엔티티 객체를 조회해오고, 이를 target에 연결시켜 실제 객체의 메소드를 호출한다.
  • 프록시 객체에 target이 한번 할당된 이후에는, 프록시 객체 초기화를 안해도 된다.

 

정리

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 
    • 초기화되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는 것이다.
    • 정확히 말하자면 프록시 내부 target 값이 채워지는 것 뿐이다.
  • 프록시 객체는 원본 엔티티를 상속 받는다. 프록시와 실제 객체의 타입이 다르기 때문에 타입 체크 시 주의가 필요하다.
    • == 비교 실패, instance of 비교 성공
    • 프록시 객체가 사용될 지 실제 객체가 사용될 지 모르기 때문에 JPA에서 타입 비교시 가능한 instance of를 사용하자
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티를 반환한다.
    반대로 처음에 프록시로 조회했다면, em.find()를 호출해도 프록시 엔티티를 반환한다.
    • JPA가 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티의 동일성을 보장해줌
    • 생각해보면, 이미 영속성 컨텍스트에 올려놓은 객체를 굳이 다시 프록시로 만들어 반환하는 것은 의미가 없다. 따라서 JPA는 그렇게 하지 않는다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화에 문제가 발생한다.
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터뜨린다.
    • 트랜잭션의 범위 밖에서 프록시 객체를 조회하려고 할 때 이 문제가 많이 발생한다.
    • 이를 해결하기 위해 Spring Boot에서는 open-in-view 설정을 true로 가져가는데 이와 관련해서 더 자세한 내용은 이곳을 참고하자

 

 

프록시 확인 방법

프록시 확인을 도와주는 Util성 메서드를 제공해준다.

 

👉🏻 프록시 인스턴스의 초기화 여부 확인

PersistenceUnitUtil.isLoaded(Object entity)

👉🏻 프록시 클래스 확인 방법

member.getClass();

// 출력
class hellojpa.Member$HibernateProxy$sMHLOeTj

👉🏻 프록시 강제 초기화

org.hibernate.Hibernate.initialize(entity);

* 참고로 JPA 표준은 강제 초기화가 없다.
강제 호출 : member.getName()

 


Reference

JPA Hibernate 프록시 제대로 알고 쓰기