article thumbnail image
Published 2021. 5. 16. 12:10
반응형

지연로딩과 즉시로딩이란?


지연로딩(LAZY)

  • 연관된 엔티티를 한방 쿼리로 조회 하지 않고, 필요한 시점에 따로 쿼리를 만드는 것을 뜻 합니다.
  • 매번 연관된 모든 데이터를 조회할 필요가 없을 경우 유용 합니다.

즉시로딩(EARGER)

  • 매번 연관된 엔티티가 모두 필요할 경우 한방 쿼리를 만드는 것을 뜻 합니다.
  • 즉, 매번 조인 쿼리가 나가게 됩니다.

즉시로딩과 지연로딩 예제


Beverage와 BeverageBrand는 N : 1

음료 엔티티( 사이다, 콜라와 같은 음료 종류 엔티티 )

package dugi;

import javax.persistence.*;

@Entity
public class Beverage {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "beverage_id")
    private Long id;
    private String name;

    // getter, setter
}

음료 브랜드 엔티티 ( 음료 종류에 따른 회사 엔티티 )

package dugi;

import javax.persistence.*;

@Entity
public class BeverageBrand {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "beverage_brand_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "beverage_id")
    private Beverage beverage;

      // getter setter
}

데이터는 아래와 같이 미리 생성해 놓았습니다.

준비가 끝났습니다. 이제 먼저 즉시로딩 결과를 확인 해보겠습니다.

BeverageBrand beverageBrand = em.find(BeverageBrand.class, 1L);
System.out.println("결과");
System.out.println("beverage class(EAGER) = " + beverageBrand.getBeverage().getClass());
System.out.println("beverage name = " + beverageBrand.getBeverage().getName());

결과를 확인 해보면 아래와 같습니다.
beverage 테이블과 조인된 쿼리가 나갔습니다. 클래스 정보를 봤을 때도 우리가 아는 일반적인 클래스 정보가 나왔습니다.
그런데 BerageBrand를 조회할 때 Berage 정보가 굳이 필요가 없다?? 그냥 가끔 필요하다 라고 한다면,
저렇게 조인이 되버리면 테이블이 여러개가 될 경우 어떤 상황에서든 저런 쿼리가 나가기 때문에 어떤 의도인지 알기 어렵고,
성능에 영향을 주게 됩니다.
그래서 지연 로딩이라는 것이 필요 합니다.

지연로딩 확인하기

BeverageBrand beverageBrand = em.find(BeverageBrand.class, 1L);
System.out.println("결과");
System.out.println("beverage class(LAZY) = " + beverageBrand.getBeverage().getClass());
System.out.println("getName 실행 전");
System.out.println("beverage name = " + beverageBrand.getBeverage().getName());

다만, BeverageBrand 엔티티에서 EAGER -> LAZY로 변경 되었습니다.

@ManyToOne(fetch = FetchType.LAZY)

결과는 아래와 같습니다.
한방 쿼리가 아닌 BeverageBrand만 조회한 후, getName을 할 때 Beverage 조회 쿼리를 따로 날렸습니다.
이렇게 되면, 즉시로딩의 단점이 보완 되는 것 입니다.
하지만 단점 역시 있습니다. 즉시로딩과 달리 쿼리가 추가적으로 나간다는 것이지요.
만약 반대로 1 : N 관계에서 1쪽에서 지연로딩으로 조회를 하고 N쪽의 List를 get 한다고 생각 해보면
N쪽의 Row 마다 쿼리가 하나 씩 나가게 되어, 1000건이면 먼저 나간 1쪽의 쿼리 한개당 1000개의 추가 쿼리가 생성되게 됩니다.
하지만 이는 본인이 원해서 나가는 쿼리니까 뭐.. 문제라고 하기는 좀 그렇네요..
이와 내용을 아래 JPQL에서 다루겠습니다.

그런데 위의 결과를 보면 즉시로딩과 다른 점이 있습니다. 바로 class 정보가 다릅니다.
지연 로딩은 proxy라는 클래스 정보로 변경 되었습니다.

Proxy 객체란?


위에서 LAZY 설정 시 프록시 객체를 가지고 있는 것을 확인 해봤습니다. 프록시 객체란 무엇일까요??
바로 가짜 객체를 의미 합니다. 실제 엔티티의 껍데기이죠.
실제 엔티티에 정보를 가지고 있기 위해서는 실제 쿼리가 실행 되어야 하기 때문에 껍데기만 그대로 복사해서 getName()
하는 순간에 쿼리를 생성하여 실제 객체에 담고, 껍데기에 실제 객체에서 가져오는 것 입니다.
그림으로 살펴 보겠습니다.

  1. getName()을 했을 때 프록시 객체를 만들어 진짜 객체의 껍데기만 가지고 있습니다.
  2. 아직 아무런 정보도 없기 때문에 영속성 컨텍스에서 찾습니다. 하지만 당연히 영속성 컨텍스트에도 없겠죠.
  3. 영속성 컨텍스트에 없기 떄문에 DB에 쿼리를 날립니다.
  4. 가져온 데이터를 실제 엔티티에 삽입 합니다.
  5. 프록시 객체는 실제 엔티티에서 값을 가져 옵니다.
    이런 과정으로 지연 로딩은 동작하게 됩니다.

JPQL의 N + 1( 1 + N이 더 이해하기 쉬운듯.. )


JPQL이란 Java Persistence Query Language의 약자로써, 객체 지향 쿼리 언어 입니다.
테이블을 바라 보는게 아닌, 객체(엔티티)를 바라보고 작업이 되는 것 입니다.
쿼리 언어이기 때문에 일반적인 쿼리와 거의 동일하기 때문에 문법이 어렵지는 않습니다.

EAGER에서의 문제점
하지만 N + 1의 함정이 숨어 있죠.
정말 무서운 함정인데요. 내가 원하지도 않았는데 쿼리가 마구 나가는 현상입니다.

아래는 JPQL을 이용하여 BeverageBrand를 select All 하는 코드 입니다.
일반적인 생각으로는 BeverageBrand만 조회가 되어야겠죠?? 그럼 결과를 보겠습니다.

List<BeverageBrand> findBB = em.createQuery("select bb from BeverageBrand bb", BeverageBrand.class)
                            .getResultList();

EAGER 설정 시 결과


하지만 결과는 Beverage 쿼리까지 2건이 추가로 나갔습니다. 왜그럴까요??
위에 DB에 저장된 데이터를 한 번 다시 확인 하시면 BeverageBrand의 beverage_id가 사이다, 콜라로 총 두개가 존재 합니다.
이는 Beverage에 존재하는 데이터들이죠. 이것 때문에 이런 예상치 못한 쿼리가 발생한 것 입니다.
jpql 쿼리문을 보면 특정 컬럼을 조회한 것이 아니라 쿼리로 치자면 select * from BeverageBrand와 같은 것 입니다.
여기서 모든 컬럼내에서 당연히 Beverage도 있기 때문에 객체 입장에서 Beverage객체가 비어있으면 안되기 때문에 메꾸기 위해
추가 쿼리를 날린 것 입니다. 만약 사이다, 콜라 뿐 아니라 1만개의 음료 종류가 있었다면 1만개의 추가 쿼리가 발생하는 것 입니다.
한방 쿼리로 생성되는 것이 아닌 것이죠. 그래서 JPA 사용 시 기본 설정을 항상 LAZY로 두는 것이 중요합니다.

LAZY 설정 시 결과


EAGER와 다르게 getName() 할 때 쿼리를 날리는 것을 볼 수 있습니다.

정리


LAZY 설정으로 사용하고, EAGER는 쓰지 말자. 이유는?

  • 물론 한방 쿼리가 더 자주 필요하다면 EAGER를 쓰면 좋겠지만, 예상치 못한 조인이 발생하기 때문에 더 혼란을 줄 수 있다.
  • JPQL에서 N + 1( 1 + N이 더 이해하기 쉬운듯.. ) 문제가 발생한다. 너무 심각...
  • 그러면 JPQL에서는 한방 쿼리로 못쓰나요?? 라고 묻는다면 ??
    fetch join을 사용하면 됩니다.

fetch join
jpql에서는 한방 쿼리를 위해 fetch join이란 것이 존재 합니다. LAZY로 사용하면서 한방 쿼리가 필요할 때 사용하면 되는 것이죠.

List<BeverageBrand> findBB = 
em.createQuery("select bb from BeverageBrand bb join fetch bb.beverage", BeverageBrand.class)
.getResultList();
반응형
복사했습니다!