HJW's IT Blog

JPA 연관관계에서 프록시 객체의 역할과 한계 본문

JAVA

JPA 연관관계에서 프록시 객체의 역할과 한계

kiki1875 2025. 2. 26. 11:04

0. Hibernate 프록시 객체

본 글로 넘어가기전 간략하게 나마 Proxy 객체에 대해 짚고 넘어가겠다.

JPA 의 엔티티 간에 연관 관계가 있을때, fetch = FetchType.LAZY 가 설정되어 있다면, Hibernate 는 DB에 실제 쿼리를 날리는 대신 임시 프록시 객체를 반환한다. 이로 인해, Hibernate 는 데이터가 실제로 필요한 순간까지 SQL 을 실행하지 않을 수 있다.

Member member = em.getReference(Member.class, 1L);

 

이와 같이 getReference() 를 활용하면, 실제 클래스가 아닌 프록시 클래스를 조회할 수 있다.

 


 

1. 1:1 관계와 프록시 동작 분석

@Entity  
public class Member {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
    @Column(name = "name")  
    private String name;  

    @OneToOne(mappedBy = "member", fetch = FetchType.LAZY)  
    private MemberProfile profile;
}

@Entity  
public class MemberProfile {  
    @Id  
    @GeneratedValue  
    private Long id;  

    @OneToOne(fetch = FetchType.LAZY)  
    @JoinColumn(name = "MEMBER_ID")  
    private Member member;  

    private boolean status;
}

 

 

이 관계는, MemberProfile 이 주인이며, FK를 관리하는 양방향 관계이다. 이때, DB에 Member 와 이 Member 를 참조하는 MemberProfile 을 INSERT 하고 영속성 컨텍스트를 비운뒤 다시 불러와서 Member.getProfile().getClass() 와 , MemberProfile.getMember().getClass() 를 실행하게 되면 어떻게 될까?

 

Member member = new Member();  
MemberProfile memberProfile = new MemberProfile();  


member.setProfile(memberProfile);  
memberProfile.setMember(member);  

em.persist(member);  
em.persist(memberProfile);  

tx.commit();  
em.clear();

 

우선, 위와 같이 연관관계를 맺고, 확실하게 하기 위해 영속성 컨텍스트를 비우겠다.

MemberProfile findProfile = em.find(MemberProfile.class, 1L);  
System.out.println("[MEMBER_PROFILE_GET_MEMBER] : " + findProfile.getMember().getClass());

 

이 결과는 예상대로 Proxy 객체를 반환한다.

[MEMBER_PROFILE_GET_MEMBER] : class hellojpa.Member$HibernateProxy$mRh3MsWK

 

그렇다면 그 역은 어떨까?

Member findMember = em.find(Member.class, 1L);  
System.out.println("[MEMBER_GET_MEMBER_PROFILE] : " +findMember.getProfile().getClass());
[MEMBER_GET_MEMBER_PROFILE] : class hellojpa.MemberProfile

 

이 결과는 놀랍게도 Proxy 가 아닌, 실제 Member 객체이다. Hibernate 가 찍어주는 쿼리를 한번 살펴보자.

Hibernate: 
    select
        m1_0.id,
        m1_0.name 
    from
        Member m1_0 
    where
        m1_0.id=?
Hibernate: 
    select
        mp1_0.id,
        mp1_0.MEMBER_ID,
        mp1_0.status 
    from
        MemberProfile mp1_0 
    where
        mp1_0.MEMBER_ID=?

 

첫번째 쿼리는 em.find(Member.class, 1L) 을 통해 생성된 것을 알겠다. 그렇다면 이 두번째 쿼리의 정채는 뭘까? 분명 FetchType.LAZY 를 설정했는데, 어째서 MemberProfile 를 조회하는 쿼리가 생성된 것일까?

1.1 프록시 객체의 생성 조건

일반적으로 Hibernate 는 fetch = FetchType.LAZY 일 경우, 프록시 객체를 반환하는 것이 일반적이다. 하지만 특정 조건에선, 개발자가 이와 같이 설정하였을 지라도, Eager Loading 을 하는 상황이 발생한다.

Hibernate 가 프록시 객체를 생성하기 위한 조건은 다음과 같다.

  1. FetchType.LAZY 설정 : (@OneToMany, @ManyToMany 는 기본적으로 LAZY 이다)
  2. 영속성 컨텍스트에 해당 엔티티가 없어야 한다
  3. 불러오려는 객체의 식별자(PK) 만 알고 있을때 이다.

그렇다면 위의 findMember.getProfile().getClass() 로 돌아가 조건들을 하나 하나 살펴보자. 1번 조건인 FetchType.LAZY 는 만족 되었다. 2번 조건인 영속성 컨텍스트에 해당 엔티티가 없어야 한다는 조건 또한 만족되었다. 하지만, Hibernate 가 프록시를 생성할 수 없는 이유는 3번쨰 조건에 있다. Hibernate 는 연관관계에 있어 주인이 아닌 엔티티의 연관 엔티티의 식별자를 모른다.

여기서 가장 중요한 개념은 객체 그래프 탐색DB 관계와 Java 관계의 차이 이다.

 

DB 의 관계

  • DB 에서 양방향 관계는 단순히 Foreign Key 하나로 해결된다.
  • MemberProfile 테이블에는 MEMBER_ID FK 가 존재한다
  • 즉, MemberProfile -> Member 방향으로 쉽게 접근할 수 있다
  • 반대로, Member -> MemberProfile 또한 MemberID로 쉽게 접근할 수 있다.

JAVA 의 관계

  • JAVA 에선 객체 참조로 관계를 맺는다.
  • MemberProfile 이 연관 관계의 주인이므로, 실제 FK 를 관리한다 (MEMBER_ID)
  • 즉, MemberProfile 은 Member 의 식별자를 알고 있다
  • 하지만, Member 는 MemberProfile 의 식별자를 직접 알지 못한다.
  • 그렇기에, Member 에서 MemberProfile 을 찾으려면, Hibernate 는 추가 조회를 해야만 한다.

1.2 반대의 상황은 어떨까

자연스럽진 못하지만 만약, Member 가 관계의 주인이고 MemberProfile_ID 를 직접 관리하고, MemberProfile 이 Member_ID를 알지 못하는 상황이라면 어떨까?

  • Member 를 조회했을때, MemberProfile 프록시 객체를 만들 수 있다.
  • 반대로 MemberProfile 을 조회했을때, Hibernate 는 Member 프록시를 생성하지 못하고 추가 쿼리를 날려 실제 Member 를 조회한다.
@Entity  
public class Member {  
  @Id  
  @GeneratedValue(strategy = GenerationType.IDENTITY)  
  private Long id;  
  @Column(name = "name")  
  private String name;  

  @OneToOne(fetch = FetchType.LAZY)  
  @JoinColumn(name = "MEMBER_PROFILE_ID")  
  private MemberProfile profile;
}

@Entity  
public class MemberProfile {  

  @Id  
  @GeneratedValue  private Long id;  

  @OneToOne(mappedBy = "profile",fetch = FetchType.LAZY)  
  private Member member;  

  private boolean status;
}
[MEMBER_Profile] : class hellojpa.MemberProfile$HibernateProxy$pg6OTFcX // Member 에서 조회한 profile
Hibernate: 
    select
        mp1_0.id,
        mp1_0.status 
    from
        MemberProfile mp1_0 
    where
        mp1_0.id=?
Hibernate: 
    select
        m1_0.id,
        m1_0.name,
        m1_0.MEMBER_PROFILE_ID 
    from
        Member m1_0 
    where
        m1_0.MEMBER_PROFILE_ID=?
[Profile_MEMBERS] : class hellojpa.Member // profile 에서 조회한 member

 

추가 SELECT 문이 날라가는것을 볼 수 있다.

2. 1:N, N:1 에서의 동작

이제 1:N 에서의 동작을 살펴보자.

@Entity  
public class Member {  
  @Id  
  @GeneratedValue(strategy = GenerationType.IDENTITY)  
  private Long id;  
  @Column(name = "name")  
  private String name;  


  @OneToMany(mappedBy = "member")  
  private List<Orders> orders = new ArrayList<>();
}


@Entity  
public class Orders {  

  @Id @GeneratedValue  
  private Long id;  

  @ManyToOne(fetch = FetchType.LAZY)  
  @JoinColumn(name = "MEMBER_ID")  
  private Member member;
}

 

이 관계에서 주인은 Orders 이며, FK 또한 Orders 가 관리한다.

Member member = new Member();  
Orders order1 = new Orders();  
Orders order2 = new Orders();  

member.getOrders().add(order1);  
member.getOrders().add(order2);  
order1.setMember(member);  
order2.setMember(member);  


em.persist(member);  
em.persist(order1);  
em.persist(order2);

 

이와 같이 DB 에 삽입하고, clear 후 다시 불러와 보겠다. 이전 1:1 의 결과에 따르면,

Member findMember = em.find(Member.class, 1L);  
System.out.println("[MEMBER_ORDER] : " + findMember.getOrders().getClass());

 

호출시, 프록시가 아닌 실제 객체들이 들어 있어야 한다.

[MEMBER_ORDER] : class org.hibernate.collection.spi.PersistentBag

 

하지만 이상한게 들어있다. PersistentBag 는 또 뭘까?

  • PersistentBag 는 Hibernate 에서 Colleciton 타입의 필드를 관리하는 내부 구현체이다. 이때 이는 단순한 ArrayList 가 아닌 영속성 컨텍스트와 연결된 특별한 컬랙션이 된다.
  • PersistentBag 를 통해, Hibernate 는 실제 대상 엔티티들의 식별자를 모르더라도 프록시를 생성할 수 있다.
  • 이후 Collection 의 요소에 접근하는 순간, Hibernate 는 추가 쿼리를 날려 데이터를 조회한다.
Orders findOrder1 = em.find(Orders.class, 1L);  
System.out.println("[ORDER_MEMBER] : " + findOrder1.getMember().getClass());

 

이 경우는, 1:1 과 동일하다. Orders 객체는 Member 의 FK 를 관리하는 주인이므로 Member 의 식별자를 알고 있다. 즉, Proxy 객체를 만들 수 있다.

[ORDER_MEMBER] : class hellojpa.Member$HibernateProxy$mEh3Lhzc

3. N:M 에서의 동작

@Entity  
public class Member {  

  @Id  
  @GeneratedValue(strategy = GenerationType.IDENTITY)  
  private Long id;  
  @Column(name = "name")  
  private String name;  


  @ManyToMany(fetch = FetchType.LAZY)  
  @JoinTable(  
      name = "MEMBER_PRODUCT",  
      joinColumns = @JoinColumn(name = "MEMBER_ID"),  
      inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")  
  )  
  private List<Product> products = new ArrayList<>();
}


@Entity  
public class Product {  
  @Id @GeneratedValue  
  private Long id;  

  @ManyToMany(mappedBy = "products", fetch = FetchType.LAZY)  
  private List<Member> members = new ArrayList<>();
}

 

마지막으로 N:M 에서의 동작에 대해 짚고 넘어가겠다. 이전 내용들을 이해했다면, 이번 동작은 매우 간단하다.

Member member = new Member();  
member.setName("member");  


Product product1 = new Product();  
Product product2 = new Product();  

member.getProducts().add(product1);  
member.getProducts().add(product2);  

product1.getMembers().add(member);  
product2.getMembers().add(member);  


em.persist(member);  
em.persist(product1);  
em.persist(product2);

 

이와 같이 세팅을 해주고, 각각 member.getProducts.getClassproduct.getMembers.getClass 를 실행하면

Member findMember = em.find(Member.class, 1L);  
System.out.println("[MEMBER_PRODUCTS] : " + findMember.getProducts().getClass());  

em.clear();  

Product findProduct = em.find(Product.class, 1L);  
System.out.println("[PRODUCT_MEMBERS] : " + findProduct.getMembers().getClass());
[MEMBER_PRODUCTS] : class org.hibernate.collection.spi.PersistentBag
[PRODUCT_MEMBERS] : class org.hibernate.collection.spi.PersistentBag

 

둘 모두 Collection 을 다루기에 이전에 설명한 PersistentBag 를 사용한 프록시 객체를 생성한 것을 볼 수 있다.