JPA - JPA 에서의 Proxy 활용 (지연로딩)

이유석·2023년 1월 16일
1

JPA - Entity

목록 보기
10/14
post-thumbnail

즉시로딩, 지연로딩

이해를 돕기 위해, 회원(Member)과 팀(Team)의 다대일 단방향 관계를 예시로 들어보겠습니다.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 다수의 회원은 하나의 팀에 소속될 수 있습니다.
  • 즉, 회원관 팀은 다대일(N:1)의 관계입니다.

위 조건에 다대일 단방향 관계를 위한 추가 조건은 아래와 같습니다.

  • 회원 객체와 팀 객체는 단방향 관계입니다.
  • 회원 객체(Member)는 Member.team 필드를 통해서 회원이 속한 팀 객체(Team)에 접근할 수 있습니다.
  • 팀 객체(Team)는 팀에 속한 회원 객체(Member)에 접근할 수 없습니다.

위 관계를 통해 작성된 엔티티 코드는 아래와 같습니다.

Member 클래스 (다대일에서 에 해당합니다.)

@Entity
public class Member {
   @Id
   @Column(name = "MEMBER_ID)
   private Long id;
 
   @Column(name = "USERNAME")
   private String username;
 
   @ManyToOne // @ManyToOne 의 속성 fetch 의 기본값 은 FetchType.EAGER
   @JoinColumn(name = "TEAM_ID")
   private Team team;
  
  // Getter, Setter, Constructor...
}

Team 클래스 (다대일에서 에 해당합니다.)

@Entity
public class Team {
	@Id
    @Column(name = "TEAM_ID)
    private Long id;
    
    @Column(name = "NAME")
    private String name;
    
    // Getter, Setter, Constructor
}

회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만, 그렇지 않을 때도 있습니다.

즉시로딩이 필요할 때

회원 엔티티를 조회할 때 연관된 팀 엔티티가 사용될 때를 살펴보겠습니다.

public void printUserAndTeam() {
    Member member = entityManager.find(Member.class, 0L);
    Team team = member.getTeam();

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

위 코드는 회원 엔티티를 찾음과 동시에 회원과 연관된 팀 엔티티의 이름을 찾아서 출력합니다.
JPA 는 이와 같이 자주 함께 사용하는 객체들의 경우에는 조인을 사용하여 즉시 함께 조회하는 즉시 로딩을 지원합니다.

지연로딩이 필요할 때

회원 엔티티를 조회할 때 연관된 팀 엔티티가 사용되지 않을 때를 살펴보겠습니다.

public void printUser() {
	Member member = entityManager.find(Member.class, 0L);

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

위 코드는 회원 엔티티만 출력합니다.
즉, entityManager.find() 로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회해 두는 것 은 효율적이지 않습니다.

실제 위 코드를 실행하였을 때, 실행되는 SQL 문은 아래와 같습니다.

SELECT m.MEMBER_ID, m.TEAM_ID, m.USERNAME, t.TEAM_ID, t.NAME
FROM 
	Member m
	LEFT OUTER JOIN
	Team t
	ON m.TEAM_ID = t.TEAM_ID
WHERE m.MEMBER_ID = 0;
  • JPA 구현체들은 객체를 실제 사용하는 시점에 데이터베이스에서 조회할 수 있는 Proxy 기술을 활용한 지연 로딩을 지원하여 위 문제를 해결합니다.

Proxy

Proxy 패턴이란?
어떤 다른 객체로 접근하는 것을 통제하기 위해서, 그 객체의 대리자 또는 자리표시자의 역할을 하는 객체를 제공하는 패턴입니다.

JPA 의 지연로딩을 이해하기 위해서, JPA 에서 사용하는 Proxy 에 대해서 학습해보겠습니다.

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager.getReference() 메서드를 사용하면 됩니다.

public void printUserWithProxy() {
    Member member = entityManager.getReference(Member.class, 0L);

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

위 코드를 실행했을 때, 출력되는 로그는 아래와 같습니다.

before use member entity

SELECT m.MEMBER_ID, m.TEAM_ID, m.USERNAME, t.TEAM_ID, t.NAME
FROM 
	Member m
	LEFT OUTER JOIN
	Team t
	ON m.TEAM_ID = t.TEAM_ID
WHERE m.MEMBER_ID = 0;

회원 이름 : 회원1

즉, getReference()를 호출할 때 JPA 는 데이터베이스를 조회(SELECT 쿼리 실행)하지 않고 실제 엔티티 객체도 생성하지 않습니다.

대신에 데이터베이스 접근을 위임한 프록시 객체를 반환합니다.

Proxy 의 특징

Proxy 객체는 실제 클래스를 상속 받아서 만들어집니다.

  • 하이버네이트가 내부적으로 상속받아서 만듭니다.

  • 개발자는 해당 Entity의 메서드를 그대로 사용할 수 있습니다.

  • Proxy 객체와 원본 Entity 객체와의 타입이 다릅니다.
    • 타입 체크시 주의해야 합니다. (instanceOf 를 사용해야 합니다.)
public void typeCheckWithProxyAndWithoutProxy() {
	Member member = entityManager.find(Member.class, 0L);
	Member memberWithProxy = entityManager.getReference(Member.class, 1L);

	System.out.println("member 클래스 : " + member.getClass());
    System.out.println("memberWithProxy 클래스 : " + memberWithProxy.getClass());
	Assertions.assertNotSame(member.getClass(), memberWithProxy.getClass());

	Assertions.assertTrue(member instanceof Member);
    Assertions.assertTrue(memberWithProxy instanceof Member);
}

위 코드를 실행하면 출력되는 로그는 아래와 같습니다.

member 클래스 : class TIL.jpa.Domain.Member
memberWithProxy 클래스 : class TIL.jpa.Domain.Member$HibernateProxy$OZWfzq9P

Proxy 객체는 실제 객체의 참조(target)를 보관합니다.

Proxy 객체의 메서드를 호출하면 Proxy 객체는 실제 객체의 메서드를 호출합니다.

Entity를 Proxy로 조회할 때, 식별자(PK)값을 파라미터로 전달하는데, Proxy 객체는 해당 식별자 값을 보관합니다.

  • 아래의 코드를 실행하면, Proxy 객체는 초기화되지 않습니다. 즉, 데이터베이스 조회가 일어나지 않습니다.
Member member = entityManager.getReference(Member.class, 0L);

System.out.println("회원 ID = " + member.getId());

Proxy 객체의 초기화

Proxy 객체의 실제 객체인 target은 getReference()가 실행될 때는 생성되어 있지 않습니다.

member.getUserName( )처럼 실제 사용될 때 데이터베이스를 조회하여, 실제 엔티티를 생성합니다. 이를 Proxy 객체의 초기화라 합니다.

Proxy 초기화 예제 코드

public void printUserWithProxy() {
    Member member = entityManager.getReference(Member.class, 0L);

    System.out.println("before use member entity");
    System.out.println("회원 이름 : " + member.getUsername()); // 1. getUserName();
}

Proxy 클래스 예상 코드

class MemberProxy extend Member {

	Member target = null; // 실제 엔티티 참조, 초기에는 생성되어 있지 않습니다.
    
    @Override
    public String getUserName() {
    	
        if (target == null) {
        
        	// 2. 초기화 요청
            // 3. DB 조회
            // 4. 실제 엔티티 생성 및 참조 보관
            this.target = ///;
        }
        
        // 5. target.getUserName();
        return target.getUserName();
    }
}

위 코드를 순서대로 설명해보겠습니다.

  1. entityManager.getReference()로 Proxy 객체를 가져온 다음에, getUserName() 메서드를 호출 하면

  2. MemberProxy 객체에 처음에 target 값이 존재하지 않습니다. JPA가 영속성 컨텍스트에 초기화 요청을 합니다.

  3. 영속성 컨텍스트가 DB에서 조회해서

  4. 실제 Entity를 생성해줍니다.

  5. 그리고 Proxy 객체가 가지고 있는 target(실제 Member)의 getUserName()을 호출해서 결국 member.getUserName()을 호출한 결과를 받을 수 있습니다.

  6. Proxy 객체에 target이 할당 되고 나면, 더이상 Proxy 객체의 초기화 동작은 없어도 됩니다.

Proxy 관련 기능들

Proxy 객체의 초기화 여부 확인

  • 아직 초기화 되지 않은 Proxy 인스턴스는 false 를 반환합니다.
    entityManger.getEntityMangerFactory().getPersistenceUnitUtil().isLoaded(Object entity);

Proxy 객체 강제 초기화

  • 아직 초기화 되지 않은 Proxy 인스턴스를 강제로 초기화 합니다.
  • JPA 표준은 강제 초기화 메서드가 존재하지 않습니다. Hibernate 가 이를 지원합니다.
org.hibernate.Hibernate.initialize(Object entity);

위 기능을 활용한 테스트 코드

public void proxyUtils() {
    EntityManagerFactory emf = entityManager.getEntityManagerFactory();
    Member memberWithProxy = entityManager.getReference(Member.class, 0L);

    boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(memberWithProxy);
    Assertions.assertFalse(isLoaded);

    // proxy 강제 초기화
    Hibernate.initialize(memberWithProxy);
    boolean isLoadedAfterInit = emf.getPersistenceUnitUtil().isLoaded(memberWithProxy);
    Assertions.assertTrue(isLoadedAfterInit);
}

소스 코드

다음 포스트에서는 지연 로딩 및 즉시 로딩에 대해서 알아보도록 하겠습니다.

profile
https://github.com/yuseogi0218

0개의 댓글