[JPA] 프록시, 즉시로딩, 지연로딩

yookyungmin·2023년 8월 31일
0

전에 프로젝트를 진행하면서 프록시는 가짜 객체를 조회한다는 걸로 알고 있었는데 블로그 작성을 해보려 한다.

프록시 객체

  • 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다.
  • 예를 들어 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만, 그렇지 않을 때도 있다.

회원 엔티티, 팀 엔티티가 있고 두 가지 경우를 조회를 할때

// 회원과 팀 정보를 출력
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());
}

아래 코드의 경우 회원 엔티티만 사용하므로 em.find()로 회원을 조회할 때 연관된 팀 엔티티까지 함께 조회 해 두는것은 효율적이지 않다.

  • JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 DB 조회를 지연하는, 지연 로딩을 지원한다.
    지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 DB 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.

프록시 초기화 과정을 분석

1. 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청 하는데 초기화라고 한다
3. 영속성 컨텍스트는 데이터 베이스를 조회해해서 실제 엔티티 객체를 생성한다.
4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
  • 프록시 색체를 초기화 한다고 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 프록시 객체가 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 DB 조회할 필요 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환.
  • 초기화는 영속성 컨텍스트의 도움을 ㅏㅂㄷ아야 가능하다.
  • 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트는 org.hibernate.LazyInitaializationException 예외를 발생 시킨다.

프록시와 식별자

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

프록시 강제 초기화

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

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

@ManyToOne(fetch = FetchType.EAGER)

  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회된다.

@ManyToOne(fetch = FetchType.LAZY)

  • member.getTeam().getName() 과 같이 팀 엔티티를 실제 사용하는 시점에 조회를 한다.
  • 지연 로딩(LAZY LOADING) : 연관된 엔티티를 실제 사용할 때 조회한다.

NULL 제약 조건과 JPA 조인 전략

외래키가 NULL 값을 허용할 경우 팀에 소속하지 않은 회원과 팀을 내부 조인하면 팀은 물론이고 회원 데이터도 조회할 수 없다.

JPA는 이런 상황을 고려해 외부 조인(LEFT OUTER JOIN)을 사용한다.
하지만 외부 조인 보다 내부 조인이 성능과 최적화에 더 유리하다.
외래키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다. 따라서 이때는 내부 조인만 사용해도 된다.

@Entity
public class Member{
	@ManyToOne(fetch = FetchType.EAGER)
    @JoinColum(name = "TEAM_ID", nullabe = false)
    private TEAM team;
}

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

프록시와 컬렉션 래퍼

  • 하이버네이트는 엔티티를 영속 상태로 만들 떄 엔티티에 컬렉션이 있으면 추적하고 관리할 목적으로 원본 컬렉션을 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고한다.
  • 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 하므로 프록시라고도 불러도 상관 없다.
  • member.getOrders()를 호출해도 컬렉션은 초기화 되지 않는다.
  • member.getOrders().get(0) 를 사용해야 초기화

JPA 기본 페치 전략

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

JPA 기본 페치 전략은 연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연 로딩 - 비용이 많이 들고, 자칫하면 너무 많은 데이터를 로딩 할수 있기 때문

추천하는 방법은 모든 연관관계에 지연로딩을 사용하는 것이다.

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

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

  • 커렉션 즉시 로딩은 항상 외부 조인을 사용한다.
    팀테이블에서 회원 테이블로 일대다 관계를 조인 할 경우 회원이 한명도 없는 팀을 내부조인하면 팀까지 조회되지 않는 문제를 만날 수 있다.

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

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

@ManyToOne, @OneToOne :

  • (optional = false): 내부조인
  • (optional = true) : 외부조인

@OneToMany, @ManyToMany :

  • (optional = false) : 외부조인
  • (optional = true) : 외부조인

0개의 댓글