JPA - Proxy(프록시)와 Lazy Loading(지연로딩)

YeongUng Kim·2021년 6월 28일
1

JPA

목록 보기
3/4
post-thumbnail

JPA Hibernate를 사용하여 작업하다 보면 엔티티 A에 연관되어 있는 다른 엔티티(Reference) B를 로딩할때 고민되는 부분이 있다. 해당 엔티티를 호출할때 엔티티A 맴버에 있는 다른 엔티티 B를 같이 호출할것이냐의 문제인데, 이건 비즈니스 로직에 따라 결정해야 한다. '만약 A만을 호출하고 B가 필요한 시점에 B를 호출하고 싶다면?' 이에 대한 해답이 바로 지연로딩(Lazy Loading) 되겠다.

우선 지연로딩을 다루기 전에 프록시(Proxy)에 대한 정의와 쓰임을 명확하게 짚고 넘어가야 한다.GoF 디자인패턴에서는 프록시 패턴을 한 문장으로 설명한다.

Proxy Pattern

사용할 객체의 제어권을 위임함으로써, 객체에 대한 클라이언트의 요청을 대신 받아 전달합니다.

대리인 역할을 한다는 뜻. JPA에서도 프록시는 위의 뜻과 일맥상통한다.한마디로 요약하자면 진짜 엔티티가 아닌 대리인, 가짜 엔티티를 말한다.지연로딩 사용시 Member는 Team에 속해있고, Member조회시 Member의 엔티티인 Team을 조회하지 않고 일단 프록시 오브젝트로 Team을 구성하고 실제 Team을 조회할때 쿼리가 나간다. 반대인 즉시로딩은 Member 조회시 Team 엔티티도 한번에 가져온다.

Proxy 특징

  • 처음 한번만 초기화
  • 프록시가 실제 엔티티로 바뀌는게 아니고, 초기화시 프록시를 통해서 엔티티에 접근한다.
  • 프록시는 원본 엔티티를 상속받는다.
  • 영속성 컨텍스트에 이미 엔티티가 존재하면, getReference() 메소드를 호출해도 실제 엔티티가 반환된다.
  • 따라서 준영속상태일때 프록시를 쓰면 문제가 될수 있다.

프록시를 알아봤으니 지연로딩,즉시로딩에 대하여 알아보자

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

지연 로딩(LAZY LOADING)

엔티티 A를 조회시 관련(Reference)되어 있는 엔티티 B를 한번에 가져오지 않는다. 프록시를 맵핑하고 실제 B를 조회할때 쿼리가 나간다.

  • 쿼리가 두번 나간다. A 조회시 한번,B 조회시 한번
  • fetch = FetchType.LAZY로 선언한다(어노테이션)
  • e.g.)
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩
@JoinColumn
private Team team;

즉시 로딩(EAGER LOADING)

엔티티 A 조회시 관련되어 있는 엔티티 B를 같이 가져온다. 실제 엔티티를 맵핑한다. Join을 사용하여 한번에 가져온다.

  • A join B , 쿼리가 한번만 나간다.
  • fetch = FetchType.EAGER로 선언한다
  • e.g.)
@ManyToOne(fetch = FetchType.EAGER) // 즉시로딩
@JoinColumn
private Team team;

지연로딩을 설정해놓으면, find 메소드 호출시 Team은 Proxy 상태이다.
member.getTeam(); 메소드를 호출하는 순간 JPA는 DB를 조회하여 Proxy에 값을 채운다.
그림을 보고 오해할 여지가 있는데, 영속성 컨텍스트에 엔티티가 없다면 getTeam() 호출시 프록시를 실제 엔티티로 바꾸지 않는다.
프록시는 실제 엔티티를 상속받은 오브젝트이다. 따라서 Team 엔티티와 겉모양이 같고 JPA는 이 프록시(Target)에 값을 채워넣는 것이다.


성능과 요구사항을 고려하여 즉시로딩 , 지연로딩을 상황에 맞게 쓰면 된다.
하지만 즉시 로딩에는 문제점이 좀 있다. 결론부터 말하자면,

되도록이면 지연로딩 을 쓰는게 좋다.

왜인지 알아보도록 하자

즉시로딩의 문제점

예상치 못한 SQL Statement 발생

공부하려고 지지고 볶는 코드에는 상관없겠지만, 실무에서는 얘기가 좀 다르다. 엔티티 하나를 조회했는데 이상하게 느리다. 왜일까 봤더니 하나의 엔티티가 물고있는 다른 엔티티가 100였다면? JOIN 쿼리가 수도없이 나갈것이다(100개의 엔티티를 전부 JOIN해서 가져와야 하니까) ㄴㅇㄱ!! 해당 비즈니스 로직에는 한두개의 엔티티만 사용한다고 가정해보자 지연로딩으로 프록시를 가져오고 실제 호출시에만 필요한 엔티티를 가져와야 하는데 즉시로딩은 다 물고 온다. 필요없는 엔티티까지 다 물고 왔으니 리소스 낭비에 성능 문제도 있다. 이런상황 에서는 지연로딩을 쓰는게 맞다.

JPQL N+1 Problem

List<Member> members = em.createQuery("select m from Member m",Member.class).getResultList();

EAGER로 설정하고 해당 코드를 실행할 경우 쿼리가 두번 나간다.
JPQL은 해당 SQL문을 그대로 실행한다. 우선 Member만 조회하고 그 후에 물고있는 Team들을 조회한다. Member 사이즈가 100개면 100번의 쿼리가 더 나가는 것이다. 이처럼 한개의 쿼리를 호출했는데 의도와 다르게 N개의 쿼리가 나가는 것을 N+1 문제라고 한다.
따라서 LAZY LOADING 으로 잡아놓고 Member를 가져올때는 Member만을 가져오고, Member의 Team을 조회해야 할 경우 fetch JOIN 을 사용하여 조회하면 된다.

@ManyToOne , @OneToOne 은 LAZY로 바꿔주자

이러한 이유로 되도록이면 지연로딩을 사용해야한다 하지만 ManyToOne과 OneToOne은 기본값이 EAGER다. LAZY로 바꿔쓰자.


함께 자주 사용하는 엔티티들은 즉시 로딩도 효율적일 수 있다. 요구사항과 비즈니스 로직을 잘 파악하고 적재적소에 지연,즉시 로딩을 사용해야 한다.


출처

  • 자바 ORM 표준 JPA 프로그래밍
profile
기록하지 않으면 까먹어서 만든 블로그..

0개의 댓글