JPA - 프록시와 지연로딩 & 즉시로딩

DevSeoRex·2022년 11월 23일
1
post-thumbnail

JPA에서 프록시 객체를 사용하는 이유

// 회원과 팀 정보를 출력하는 예제
public void printUserAndTeam(String memberId) {
	Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 이름 : " + member.getUsername());
    System.out.println("소속팀 : "  + team.getName());
}

// 회원 정보만 출력하는 예제
public void printUser(String memberId) {
	Member member = em.find(Member.class, memberId);
    System.out.println("회원 이름 : " + member.getUsername());
}

위의 예제를 보면, printUserAndTeam( ) 메서드는 회원과 회원과 연관된 팀의 이름을 모두 출력한다.
반면에 printUser( ) 메서드는 회원의 이름만 출력한다.

printUser( ) 메서드는 회원 엔티티만 사용하는데, em.find( )를 사용해 회원과 연관된 팀 엔티티까지
조회하는 것은 효율적이지 못하다.

💡 JPA는 이런 문제를 해결하기 위해, 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 지연로딩     기능을 제공한다.

  • team.getName( )과 같은 엔티티의 값을 실제 사용하는 시점에 데이터베이스에서 데이터를 조회한다.
  • 지연 로딩을 사용하려면 실제 엔티티 객체 대신에 가짜 객체를 사용하는데 이것을 프록시 객체라고 한다.

프록시 객체

EntityManager.find( )

  • 보통 JPA에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find( )를 사용한다.
  • 영속성 컨텍스트에 엔티티가 있으면 바로 꺼내오지만, 없을 경우 데이터베이스에 조회한다.

EntityManager.getReference( )

  • 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager.getReference( ) 메서드를 사용하면 된다.
  • 이 메서드를 사용하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티도 생성하지 않는다.
  • 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

프록시 객체의 특징

  • 프록시 클래스는 실제 클래스를 상속받아 만들어 진다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용해도 된다.

프록시 위임(delegate)

  • 프록시 객체는 실제 객체에 대한 참조(target)을 보관한다.
  • 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.

프록시 객체의 초기화

  • 프록시 객체는 Team.getName( )과 실제로 사용될 때 데이터베이스를 조회하여 실제 엔티티를 생성한다.

  • 프록시 초기화 과정
      1. 프록시 객체에 member.getName( )을 호출해서 실제 데이터 조회
      1. 프록시 객체에 엔티티가 생성되어 있지 않으므로, 영속성 컨텍스트에 실제 엔티티 생성 요청(초기화)
      1. 영속성 컨텍스트가 데이터베이스 조회를 통해 실제 엔티티 객체 생성
      1. 프록시 객체는 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관
      1. 프록시 객체는 실제 엔티티 객체의 getName( )을 호출하여 결과 반환

프록시의 특징

  • 프록시 객체는 한 번만 초기화된다.
  • 프록시 객체가 초기화된다고, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로, 타입 체크시에 == 대신 instanceof 연산자를 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 존재할 경우, EntityManager.getReference( )를 호출해도 실제 엔티티를 반환한다.
  • 준영속 상태의 프록시를 초기화하면 문제가 발생한다(LazyInitializationExcepiton 예외 발생)

💡 JPA 표준 명세에는 준영속 상태의 엔티티를 초기화할 때 어떤 일이 발생할지 정의되어 있지 않다.

프록시와 식별자

  • 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달한다.
  • 프록시 객체는 이 식별자 값을 보관하고 있다.
// 프록시 조회 예제
Team team = EntityManger.getReference(Team.class, "team1"); // 식별자 보관
team.getId(); // 초기화 하지 않는다.
  • 프록시 객체는 식별자 값을 가지고 있으므로, team.getId( )를 호출해도 초기화하지 않는다.
  • 엔티티 접근 방식이 프로퍼티(@Access(AccessType.PROPERTY))일 경우에만 초기화하지 않는다.
  • 엔티티 접근 방식이 필드(@Access(AccessType.FIELD))일 경우 프록시 객체를 초기화 한다.

프록시 객체 활용

  • 프록시는 연관관계를 설정할 때 유용하게 사용할 수 있다.
// 프록시 사용 예제
Member member = EntityManager.find(Member.class, "member1");
Team team = EntityManager.getReference(Team.class, "team1"); // SQL을 실행하지 않는다.
member.setTeam(team);
  • 연관관계를 설정할때는 식별자 값만 사용하므로, 프록시 사용시 데이터베이스 접근 횟수를 줄일 수 있다.
  • 연관관계 설정시 엔티티 접근 방식을 필드로 설정해도 프록시는 초기화되지 않는다.

프록시 확인

  • JPA가 제공하는 메서드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
// 프록시 초기화 여부 확인
boolean isLoad = EntityManager.getEntityManagerFactory()
					.getPersistenceUnitUtil().isLoaded(entity);

// 프록시 초기화 여부 확인 다른 방법
boolean isLoad = EntityManagerFactory.getPersistenceUnitUtil().isLoaded(entity);

// 조회한 엔티티 프록시 객체 여부 확인(클래스 명 출력)
System.out.println("memberProxy = " + member.getClass().getName());
// 결과 : memberProxy = jpabook.domain.Member_$$_javassit_0

💡클래스 이름 출력결과는 프록시를 생성하는 라이브러리에 따라 출력 결과가 달라질 수 있다.

프록시 강제 초기화

  • 하이버네이트에서 제공하는 initalize( ) 메서드를 사용하면 프록시를 강제로 초기화할 수 있다.
// 프록시 강제 초기화 예제
org.hibernate.Hibernate.initalize(order.getMember()); // 프록시 초기화

💡JPA 표준에는 프록시 강제 초기화 메서드가 없다. 강제로 초기화 하려면 프록시의 메서드를 호출하면 된다.

즉시 로딩(EAGER LOADING)과 지연(LAZY LOADING)로딩

JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있다.

  • 즉시 로딩(EAGER LOADING) : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
    • em.find(Member.class, "member1")를 호출할 경우, 회원 엔티티와 연관된 엔티티들도 함께 조회된 다.
    • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩(LAZY LOADING) : 연관된 엔티티를 실제 사용할 때 조회한다.
    • member.getTeam( ).getName( ) 과 같이 조회한 팀 엔티티를 실제 사용하는 시점에 팀 엔티티를 조회한다.
    • 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)

즉시 로딩

// 즉시로딩 예제
@Entity
public class Member{
	// ...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinCoulmn(name = "TEAM_ID")
    private Team team;
    // ...
}

Member member em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
  • 즉시 로딩을 사용하고 있으므로, 회원을 조회하는 순간 팀도 함께 조회한다.
  • 회원과 팀 두 테이블을 조회해야 해서 쿼리를 2번 실행할 것 같지만 한번 실행한다.
  • 대부분의 JPA 구현체는 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리를 사용한다.

NULL 제약조건과 JPA 조인 전략

즉시 로딩 실행 SQL에서 JPA는 내부조인(INNER JOIN)이 아닌 외부조인을 사용했다.
외래키가 NULL 값을 허용할 경우, 예를 들어 회원과 팀의 경우라면 팀에 소속되지 않은 회원이 있을 가능성이
있기 때문에 내부조인을 사용하면 팀은 물론이고 회원 데이터도 조회할 수 없다

조인을 성능으로 따지자면, 외부 조인보다 내부 조인이 성능과 최적화면에서는 훨씬 유리하다.
내부 조인을 사용하려면 외래키에 NOT NULL 제약조건을 줘서 값이 있다는 것을 보장하면 된다.

JPA에게 이런 사실을 알려주려면 한가지 설정만 해주면 된다.

// NOT NULL 제약조건 설정 예제
@Entity
public class Member {
	// ...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID", nullable = false)
    private Team team;
    // ...
}

💡nullable = false 대신 optional = false 를 사용해도 내부 조인을 사용할 수 있다.

지연 로딩

  • 지연 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 지정하면 된다.
// 지연 로딩 설정 예제
@Entity
public class Member{
	// ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    // ...
}

// 지연 로딩 실행 예제
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용

지연로딩 실행 과정

    1. em.find(Member.class, "member1")을 호출하면 회원만 조회한다.
    1. 조회한 회원의 team 멤버변수에 프록시 객체를 넣어둔다.
    1. team.getName( )을 호출하면 실제 사용되야 하기 때문에 프록시 객체는 데이터를 로딩한다(초기화)

💡 영속성 컨텍스트에 이미 찾는 엔티티가 있을경우 실제 객체를 반환한다.

프록시와 컬렉션 래퍼

// 컬렉션 래퍼 사용 예제
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// 출력 결과 : orders = org.hibernate.collection.internal.PersistentBag
  • 하이버네이트는 엔티티를 영속상태로 만들때 엔티티에 컬렉션이 있으면 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다
  • 하이버네이트가 제공하는 내장 컬렉션을 컬렉션 래퍼라고 한다.
  • 엔티티는 프록시 객체를 사용해서 지연 로딩을 수행하듯이, 컬렉션은 컬렉션 래퍼가 그 역할을 대신한다.
  • 컬렉션 래퍼의 경우 member.gerOrders( )와 같이 컬렉션을 반환 받는 메서드를 사용하면 초기화되지 않는다.
  • 컬렉션 래퍼의 경우 member.getOrders( ).get(0)과 같이 실제 데이터를 조회할 때 초기화된다.

JPA의 기본 페치(Fetch) 전략

fetch 속성의 기본 설정 값

  • @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
  • @OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)

💡 모든 연관관계에 지연 로딩을 사용할 것을 추천한다.

컬렉션에서 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.

    💡 예를 들어, A 테이블을 N,M 두 테이블과 일대다 조인하면 SQL 실행 결과는 N * M이 된다. 너무 많은      반환하고, 애플리케이션 성능이 저하될 수 있다.

  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

    💡 예를 들어, 회원 테이블과 팀 테이블을 조인할 경우, 회원 테이블의 외래 키에 not null 제약조건을 주면      모든 회원은 팀에 소속되므로 항상 내부조인 사용이 가능하다.
         반면에, 팀 테이블에서 회원 테이블로 일대다 관계를 조인할 경우 회원이 한명도 없는 팀을 내부 조인하면      팀까지 조회되지 않는 문제를 만날 수 있다.

JPA는 일대다 관계를 즉시 로딩할 때 항상 외부조인을 사용한다.

FetchType.EAGER 설정 & 조인 전략 정리

  • @ManyToOne, @OneToMany
    • (optional = false) : 내부 조인
    • (optional = true) : 외부 조인

  • @ManyToMany, @OneToOne
    • (optional = false) : 외부 조인
    • (optional = true) : 내부 조인

    출처 : 자바 ORM 표준 JPA 프로그래밍 기초편(인프런, 김영한) & 자바 ORM 표준 JPA 프로그래밍(도서) (김영한 저, 에이콘 출판사) /

1개의 댓글

comment-user-thumbnail
2022년 11월 25일

오우

답글 달기