LAZY, EAGER

A라는 엔티티에 아래와 외래키가 있다 해보자. Lazy를 지정하지 않으면 default인 EAGER가 적용된다.

Eager 옵션에 JpaRepository에서 제공하는 기본 메소드( findById, findAll )로 A를 조회해 보면 4개의 JOIN 쿼리가 나간다.

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
    @JoinColumn(name = "locate_id", referencedColumnName = "id")
    private Locate locate;

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
    @JoinColumn(name = "riskAnalysis_id", referencedColumnName = "id")
    private RiskAnalysis riskAnalysis;

    @ManyToOne
    @JoinColumn(name = "device_id", referencedColumnName = "id")
    private Device device;

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
    @JoinColumn(name = "condition_id", referencedColumnName = "id")
    private Condition condition;
Hibernate: 
    select
		~~~ 
    from
        data d 
    left join
        device d1_0 
            on d1_0.id=d.device_id 
    left join
        sensor s1_0 
            on s1_0.id=d1_0.sensor_id 
    left join
        locate l1_0 
            on l1_0.id=d.locate_id 
    left join
        risk_analysis ra1_0 
            on ra1_0.id=d.risk_analysis_id 
    left join
        condition c1_0 
            on c1_0.id=d.condition_id 
    where
        d.id=?

그리고 예를 들어, Device가 Sensor라는 외래키를 가지고 있었다면, 그 관계에 해당하는 JOIN 쿼리도 발생한다.

( 재귀적으로 전부 JOIN 쿼리를 발생시킨다. )

 

이번엔 아래와 같이 LAZY를 적용해보자.

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
    @JoinColumn(name = "locate_id", referencedColumnName = "id")
    private Locate locate;

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
    @JoinColumn(name = "riskAnalysis_id", referencedColumnName = "id")
    private RiskAnalysis riskAnalysis;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "device_id", referencedColumnName = "id")
    private Device device;

    @OneToOne(cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
    @JoinColumn(name = "condition_id", referencedColumnName = "id")
    private Condition condition;
Hibernate: 
    select
        ~~
    from
        data d
    where
        d.id=?

JOIN이 없고 해당 FK 객체를 사용할 때, 추가 select 쿼리가 발생한다.

 

JOIN 개수에 따른 성능 차이

fk는 id로 걸려있다. 즉, index는 기본 생성돼 있다. (spring data jpa에서 pk와 uk는 default로 index를 생성한다.)

@Query("SELECT d FROM Data d WHERE d.status = :status AND d.success = :success")

findAll에 where 조건 2개가 적용된 JPQL 쿼리를 실행했을 때 시간이다.

서버 스펙과 환경에 따라 시간은 다르다. 그냥 차이가 이 정도라는 것만 살펴보자.

 

데이터 수 LAZY (JOIN 0개) (ms) EAGER (JOIN 5개) (ms)
1000개 600~800 2100~2400
2000개 1100~1300 4200~4500
3000개 1700~1900 6200~6500
4000개 2300~2500 8400~8700

 

데이터 수가 많아질수록 격차가 점점 커진다. 많은 데이터를 다룰수록 join에 영향이 크다.

 

불필요한 JOIN 수를 줄이고자, FK들은 기본적으로 Lazy로 구성해 두는 편이 좋을 듯하다.

 

그리고 Lazy를 사용하면 Entity 객체로 응답할 때 걸릴 수 있는데 ( LazyInitializationException ~~ ), 모든 Entity는 Dto 처리해놓자. 아직 정하지 않았어도 Entity는 일단 Dto로 처리해 놓는 게 유지 보수에 편리하다.

 

양방향 관계에서 외래키 주인이 아닌 쪽에 Lazy는?

    @OneToOne(mappedBy = "Data", optional = false, cascade = {CascadeType.REMOVE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
    private ExtraData extraData;

    @OneToMany(mappedBy = "Data", cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
    private List<Test> testList;

@OneToMany는 default가 fetch = FetchType.LAZY 로 동작한다. (해당 객체 조회할 때, SELECT 발생)

+) 외래키 주인이 아닌 쪽에서 OneToMany는 N+1 문제와도 관련 있다.

 

Hibernate: 
    select
        ~~~
    from
        data d1_0 
    where
        d1_0.id=?
Hibernate: 
    select
        ~~~
    from
        extra_data ed1_0 
    where
        ed1_0.data_id=?

@OneToOne은 FetchType.LAZY/EAGER 와 무관하게 조회 시에 JOIN이 아닌 SELECT가 발생한다. optional = false 옵션을 주어도 동일하다.

 

위 동작의 이유는?

외래키 주인이 아닌 쪽은 컬럼이 없고, 해당 FK가 NULL인지 아닌지 모르기 때문에 SELECT 가 발생한다.

 

@OneToMany는 왜 Lazy로 동작할 수 있는가?

특성 @OneToOne (엔티티 프록시) @OneToMany (컬렉션 프록시)
초기화 방식 단일 엔티티 프록시로 초기화 컬렉션 프록시 객체로 초기화
데이터 로드 시점 엔티티 필드에 접근할 때 컬렉션 메서드에 접근할 때

컬렉션 프록시는 JPA 구현체(Hibernate)가 제공하는 타입이다.

정확한 자료 구조는 디버깅해 봐야 알겠지만, 컬렉션 프록시는 NULL을 처리할 수 있고, 엔티티 프록시는 NULL을 처리할 수 없다.

NULL을 처리할 수 있다 = NULL이 들어와도 된다. = SELECT 필수 아님

NULL을 처리할 수 없다 = NULL이 들어오면 안 된다. = SELECT 필수