[JPA] 프록시와 지연 로딩

imcool2551·2022년 4월 7일
0

JPA

목록 보기
7/12
post-thumbnail

본 글은 인프런 김영한님의 JPA 로드맵을 기반으로 정리했습니다.

1. 프록시


아래 그림을 살펴보자. Member 엔티티는 Team 엔티티와 1:N 연관관계를 가진다.

Member를 조회할 때 Team도 함께 조회해야될까? 상황에 따라 다르다. 만약 Team 엔티티를 사용하지 않는다면 Team 엔티티를 굳이 가져올 필요 없다. 데이터베이스에서 불필요한 조인 연산으로 성능만 안 좋아진다.

Member와 함께 Team을 함께 조회하는 것을 즉시 로딩이라고 하고 반대로 Member를 조회할 때 Team을 조회하지 않고 Member만 조회하는 것을 지연 로딩이라고 한다.

지연 로딩을 통해 가져온 객체는 프록시가 된다. 위의 경우 Member 엔티티가 Team 엔티티를 지연 로딩을 통해 가져오면, Team 객체는 프록시가 된다. 물론 데이터베이스에서 Member 테이블의 레코드가 Team 테이블의 PK를 참조하고 있는 경우에 한해서 그렇다. Member 테이블에서 Team 테이블을 참조하는 FK가 null이라면 Team 필드는 실제 객체도 프록시도 아닌 null을 가진다.

위의 그림처럼 프록시는 실제 클래스(엔티티)를 상속 받아서 만들어지기 때문에 프록시는 실제 클래스와 겉 모양이 같다.

프록시 객체는 실제 객체와 겉 모양이 똑같고 내부적으로 실제 객체(target)의 참조를 보관한다. 그렇기 때문에 클라이언트 입장에서는 현재 사용하고 있는 객체가 프록시인지 실제 객체인지 구분하지 않고 사용할 수 있다. 만약 지연 로딩을 통해 가져온 프록시라면 아래 그림처럼 동작을 실제 객체에게 위임(delegate)한다.

프록시 객체를 처음 가져오면 초기화되지 않은 상태다. 초기화되지 않았다는 말은 프록시가 참조하는 실제 객체(target)의 값이 텅텅 비어있다는 뜻이다. 단지, 영속성 컨텍스트가 PK를 기준으로 프록시 객체를 보관만 하고 있을 뿐이다. 그렇게 때문에 실제 객체를 사용하려면 초기화를 해줘야 한다. 프록시 객체를 초기화 하는 방법은 매우 쉽다.

코드를 통해 살펴보자.

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

em.getReference()는 엔티티 매니저를 통해 프록시를 반환하는 함수다. em.find()를 호출하면 실제 앤티티가 반환되어버리니 프록시를 반환받기 위해서 사용했다. 이미 말한것처럼 프록시는 실제 엔티티를 상속 받기 때문에 실제 엔티티 타입으로 받을 수 있다. 위의 코드처럼 실제 엔티티 매니저를 통해 직접 프록시를 조회할 일은 거의 없다.

프록시를 초기화하려면 위의 코드처럼 getName()처럼 실제 객체의 값을 요구하면 된다. 그러면 영속성 컨텍스트가 DB를 통해 프록시 객체가 참조하는 실제 객체(target)에 값을 채워넣는다.

getId()를 호출해도 프록시는 초기화되지 않는다. PK는 영속성 컨텍스트가 엔티티를 관리하기 위한 필수 값이기 때문에 프록시도 이미 PK값은 알고 있다.

프록시의 초기화 과정을 그림을 통해 살펴보자.

사용자는 Member 엔티티에 값을 요구한다. 사용자는 현재 자신이 사용하는 엔티티가 프록시인지 실제 객체인지 모른다.

만약 초기화되지 않은 프록시라면 프록시가 내부적으로 영속성 컨텍스트에게 초기화를 요청한다. 그러면 영속성 컨텍스틑 실제 데이터베이스를 조회해서 프록시가 참조하는 실제 객체(target)에 값을 채워넣는다.

프록시가 초기화되도 프록시가 실제 객체로 바뀌지는 않는다. 단지 프록시가 참조하는 실제 객체에 값이 채워지는 것이다. 프록시는 초기화하면 실제 객체에 값이 채워지기 때문에 한 번만 초기화하면 된다.

프록시의 특징을 정리해보자.

  • 프록시는 처음 사용할 때 한 번만 초기화 된다.

  • 프록시 객체가 초기화되도 실제 객체(엔티티)로 바뀌는 것은 아니다.

  • 프록시 객체는 실제 엔티티를 상속 받는다. 그렇기 때문에 타입 체크시 주의해야한다. == 비교가 아닌 instanceof 를 사용하자.

  • 영속성 컨텍스트에 이미 실제 엔티티가 있으면 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티가 반환된다.

  • 준영속 상태의 엔티티는 영속성 컨텍스트의 도움을 받을 수 없기 때문에 프록시를 초기화하면 org.hibernate.LazyInitializationException 예외가 발생한다.

2. 즉시 로딩과 지연 로딩


지연 로딩을 사용하면 연관된 엔티티를 텅 빈 프록시로 조회한다. 그리고 프록시는 실제 엔티티의 데이터가 필요한 순간에 DB를 조회해서 초기화된다.

Member와 Team이 자주 같이 사용된다면 즉시 로딩을 사용하고 싶을 수 있다.

그러나, 즉시 로딩은 가급적 사용하면 안 된다. 즉시 로딩은 JPQL 사용시 N+1 문제를 일으킬 수 있다. JPQL과 N+1 문제는 이후의 글에서 자세히 알아본다.

간단히 말하자면 JPQL은 엔티티를 쿼리하는 문법이다. JPQL은 SQL로 바뀌어서 실행된다. 문제는 JPQL을 통해 가져온 엔티티의 즉시 로딩으로 설정된 연관된 엔티티를 가져오기 위해 N번의 추가 쿼리가 나갈 수 있다는 것이다.

코드로 살펴보자. Member 엔티티는 연관된 Team 엔티티를 즉시 로딩으로 설정했다.

em.createQuery("select m From Member m", Member.class);

Member 엔티티를 쿼리하는 JPQL이다. SQL 로 번역되어 데이터베이스에 쿼리가 나간다. 문제는 JPQL이 select * from Member 처럼 그대로 번역되기 때문에 즉시 로딩으로 설정했든 안 했든 데이터베이스에서 Member 테이블의 데이터만 가져온다는 것이다.

지연 로딩이라면 Team 엔티티를 프록시로 채우면 되지만 즉시 로딩일땐 실제 Team 엔티티에 값을 채우기 위해 데이터베이스에 추가 쿼리가 나간다. 만약 조회된 Member 엔티티가 100개라면 최대 100개의 쿼리가 추가로 나간다. 이것이 N+1 문제이며 성능에 막대한 불이익을 줄 수 있다. 만약 즉시 로딩이 엔티티 여기저기에 독버섯처럼 퍼져있으면 정말 상상하지 못한 쿼리가 나갈 수 있다.

연관된 엔티티를 함께 조회하기 위해 즉시 로딩 대신 다른 대안이 있다. JPQL fetch 조인이나, 엔티티 그래프 기능, batch 설정 등이 있다. 모두 이후의 글에서 자세히 알아본다.

결론적으로 모든 연관관계는 기본적으로 지연 로딩으로 설정하자. @ManyToOne, @OneToOne은 기본이 즉시 로딩이기 때문에 명시적으로 지연 로딩으로 설정해야한다. @OneToMany, @ManyToMany는 기본이 지연 로딩이기 때문에 따로 설정할 필요 없다.

@Entity
public class Member {

  @Id @GeneratedValue
  private Long id;

  @Column
  private String name;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}

애노테이션의 fetch 속성을 변경하면 된다.

profile
아임쿨

0개의 댓글