JPA - 지연로딩과 프록시 객체 이해하기

KoK·2025년 7월 12일

JPA

목록 보기
6/8
post-thumbnail

JPA의 주요 장점 중 하나는 객체 그래프 탐색을 통한 연관관계 조회가 가능하다는 점이다. 하지만 모든 연관 엔티티를 한 번에 조회하면 비효율적일 수 있기 때문에, JPA는 필요한 시점에 데이터를 조회하는 지연 로딩(Lazy Loading)을 지원한다.

이 지연 로딩 기능은 대부분의 구현체(Hibernate 등)에서 프록시 객체를 통해 구현된다.

1. 프록시란?

프록시(proxy) 란 어떤 동작을 대신 수행해주는 객체를 의미한다.
JPA에서는 실제 엔티티 대신 프록시 객체를 먼저 반환하고, 해당 엔티티의 데이터에 접근하는 시점에 DB 조회를 수행한다.

이 기술은 Hibernate뿐만 아니라 Spring의 트랜잭션 처리, AOP 등 다양한 곳에서 사용된다.


2. find() vs getReference()

JPA에서는 엔티티를 조회할 때 두 가지 메서드를 사용할 수 있다.

메서드설명
em.find()즉시 실제 객체를 조회하며 바로 SELECT 쿼리 실행
em.getReference()프록시 객체 반환, 실제 필드 접근 시점에 SELECT 쿼리 실행

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

package hellojpa;

import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.List;

public class JpaMain {

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

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

            em.persist(member);

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

			// 실제 SELECT 안 나감
            Member findMember = em.getReference(Member.class, member.getId()); 
            System.out.println("findMember = " + findMember.getClass());
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.username = " + findMember.getUsername());

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

  • em.find()는 바로 SELECT쿼리가 나간다.
  • em.getReference()는 프록시를 반환한다.

3. 프록시객체 초기화

프록시는 처음에는 단순한 껍데기 객체일 뿐이다.
실제 데이터 접근이 일어나면 영속성컨텍스트가 개입하여 프록시 내부를 초기화하고, 그 이후 실제 엔티티처럼 동작한다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

// INSERT 쿼리 실행
em.flush();    
// 영속성 컨텍스트 초기화
em.clear();       

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


refMember.getUsername(); 

// true
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); 

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

초기화 순서는 다음과 같다.

  • getReference() 호출
    → 이 시점에는 DB 조회 없이 프록시 객체만 생성된다.

  • 프록시 객체 접근 (getName() 등 호출)
    → 내부적으로 JPA가 영속성 컨텍스트에 실제 엔티티 조회 요청

  • 영속성 컨텍스트 처리
    → 먼저 1차 캐시에서 엔티티를 찾고, 없으면 DB에서 조회

  • 실제 엔티티 로딩 및 저장
    → DB에서 조회한 데이터를 바탕으로 엔티티를 만들고, 영속성 컨텍스트에 저장

  • 프록시 → 실제 엔티티 위임
    → 프록시는 내부적으로 target.getName() 같은 방식으로 실 데이터에 접근


4. 프록시 객체의 특징

항목설명
가짜 객체프록시는 실제 엔티티가 아닌, Hibernate가 만든 클래스
초기화 시점실제 필드에 접근할 때(DB 쿼리 실행)
타입클래스는 다르지만 원본 엔티티를 상속하므로 instanceof로 체크 가능
1차 캐시 확인이미 엔티티가 로딩되어 있으면 프록시 대신 실제 객체 반환
초기화 후에도 프록시 유지프록시 객체가 실제 엔티티로 바뀌지 않음, 단지 위임만 함
주의사항초기화 전에 em.close()되면 LazyInitializationException 발생

프록시 객체가 초기화되지 않은 상태에서 영속성 컨텍스트가 종료되면, 실제 데이터를 로딩할 수 없어 LazyInitializationException 이 발생한다.

Member proxy = em.getReference(Member.class, id);
em.close();
proxy.getUsername(); // 예외 발생!

지연 로딩을 사용하는 경우, 반드시 영속성 컨텍스트 범위 안에서 프록시가 초기화되어야 한다.


5. 프록시 객체 초기화 여부 확인

System.out.println(emf.getPersistenceUnitUtil().isLoaded(refMember));

프록시 객체는 실제 데이터에 접근하는 시점에 영속성 컨텍스트를 통해 초기화된다.
예를 들어, 아래 코드처럼 refMember.getUsername() 처럼 프록시 객체의 실제 데이터를 조회하려고 하면, 내부적으로 초기화가 발생하고 PersistenceUnitUtil.isLoaded() 는 true를 반환한다.

  • 반면, 실제 데이터에 접근하지 않은 상태에서는 프록시 객체는 초기화되지 않았기 때문에 isLoaded() 결과는 false 로 출력된다.
  • 이처럼 프록시는 지연 로딩(Lazy Loading) 을 통해 성능을 최적화하지만, 예상치 못한 시점에 초기화될 수 있으므로 주의가 필요하다.

6. 마무리

  • 프록시는 지연 로딩을 가능하게 해주는 핵심 기술이다.
  • Hibernate는 이를 통해 성능을 최적화하지만, 초기화 시점, 영속성 컨텍스트 종료 등에 주의해야 한다.
  • 초기화 시점을 놓치면 LazyInitializationException이 발생할 수 있으니 주의가 필요하다.

지금까지 JPA에서 프록시 객체가 어떻게 동작하는지 알아보았다.
프록시는 JPA의 지연 로딩(Lazy Loading) 을 가능하게 하는 핵심 요소이다.
다음 글에서는 프록시를 기반으로 동작하는 지연 로딩의 실제 활용 방식, 즉시 로딩과의 비교 등을 함께 살펴보겠습니다.

profile
개발 이것저것

0개의 댓글