Develop/JPA

[자바 ORM 표쥰 JPA 프로그래밍] 16일차 - LAZY 로딩 & EAGER 로딩

자라선 2021. 9. 2. 14:32

1:N

예를 들어서 Member 엔티티만 사용하고자 한다면 연관되어있는 Team 엔티티까지 DB에서 조회할 필요 있을까?

또 Member 엔티티와 Team 엔티티를 동시에 쓰고자 할때 무조건 한번의 SELECT로 다 가져올 필요가 있을까?

 

JPA는 이러한 이슈를 가지고 성능 향상을 위해 2가지의 엔티티 로딩 방식을 지원한다.

 

1. EAGER 로딩 (즉시 로딩)

이름 그대로 SELECT 하는 즉시 바로 연관된 모든 엔티티를 DB로부터 데이터를 영속성 컨텍스트에 가져오는 것을 말한다.

@Data
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private long id;

    private String name;

    // 즉시 로딩으로 조회하도록 명시
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}
        // EAGER 로딩
        final Member member = em.find(Member.class, 2l);    // Member 엔티티 호출
        member.getName();   // Member 엔티티 사용

엔티티에 @ManyToOnefetch 속성값을 FetchType.EAGER 라고 명시하여 Member에서 관계된 Team 엔티티는 Member 를 조회할때 즉시로딩하라고 된 것이다.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.name as name2_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        team1_.TEAM_ID as TEAM_ID1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

실제 코드를 실행한다면 JPA는 JOIN 문을 사용하여 한번에 SQL를 생성하는 것을 볼수 있다.

        // EAGER 로딩
        final Member member = em.find(Member.class, 2l);    // Member 엔티티 호출
        
        // 즉시 로딩으로 한번에 영속성 컨텍스트로 불러왔기 때문에 다시 SQL를 생성하지않는다.
        final Team team = member.getTeam();
        System.out.println(team.getName());

 

이렇게 EAGER 로딩을 하여 DB를 통해 한꺼번에 관계된 데이터를 조회한다.

당연히 한번에 불러왔기 때문에 더이상 DB 커넥션이 필요없어 적은 데이터라면 비교적 성능이 좋아질 수는 있다.

 

추가

SQL에서 JOIN을 보자면 LEFT JOIN 이 된 것을 볼수있는데 이는 JPA가 INNER JOIN으로 사라지는 ROW를 방지하기 위함이다. 

하지만 모든 JOIN을 LEFT JOIN으로 하게 된다면 성능 하락의 원인이 될 수도있기 때문에 이를 방지하기 위해서는 NOT NULL 제약조건을 활용하여 JPA에게 전달해주어야한다.

    // 즉시 로딩으로 조회하도록 명시, NOT NULL을 명시하여 INNER JOIN이 가능하다고 알림
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID", nullable = false)
    private Team team;

엔티티에서 제약조건으로 NOT NULL 처리를 해야지만 JPA는 INNER JOIN 을 사용해도 문제가 없다는 것을 확인하게 되고 코드를 실행시 SQL 를 확인했을때 INNER JOIN으로 조회한 것을 볼수 있다.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.name as name2_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        team1_.TEAM_ID as TEAM_ID1_3_1_,
        team1_.name as name2_3_1_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

 

2. LAZY 로딩 (지연로딩)

즉시 로딩과는 반대로 우선 주 엔티티를 조회후 관계된 엔티티는 사용할때 조회하는 인스턴스한 방식이다.

@Data
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private long id;

    private String name;

    // 즉시 로딩으로 조회하도록 명시
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}
        // LAZY 로딩
        final Member member = em.find(Member.class, 2l);    // Member 엔티티 호출, 그러나 TEAM은 호출하지않는다.

        final Team team = member.getTeam();
        // 지연 로딩이기 때문에 getName() 으로 사용될때 DB로 SQL을 전달하여 Team 엔티티를 조회한다.
        System.out.println(team.getName());

관계가 있는 엔티티의 매핑 @ManyToOne 설정에 fetch 속성으로 FetchType.LAZY 를 명시하였다.

그리고 위와 같은 코드를 작성하여 실행하면 아래와 같은 SQL이 생성되어 조회된것을 확인할 수 있다.

 

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.name as name2_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
        
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

즉시 로딩과는 다르게 SQL이 2번 실행이 된 것을 볼수있는데 처음에 Member 엔티티를 조회할때 한번 그 후 team.getName() 를 조회할때 한번 실행된 것이다.

 

이러한 지연 로딩은 불필요한 엔티티의 조회를 방지하고 많은 량의 데이터라면 지연 로딩을 통해 부분적으로 조회하는 것이 성능 면에서는 좋다.

 

결과적으로 본다면 즉시로딩과 지연로딩 둘다 같지만 조회 순서가 다르다 보니 사용처에 따라 성능이 갈라질수 있다.

 

3. 기본 로딩 전략

따로 fetch 로 명시하지않아도 JPA는 매핑 상황에 따라 자동으로 즉시나 지연 로딩으로 설정하게 된다.

 

- @ManyToOne, @OneToOne: 즉시 로딩(FetchType.EAGER)

- @OneToMany, @ManyToMany: 지연 로딩(FetchType.LAZY)

 

JPA가 이러한 방식으로 자동으로 로딩 설정을 하는 이유는 성능과 관계가 있다.

 

Member 와 Team 는 N:1 관계를 갖고 있어 Member 엔티티에서는 @ManyToOne 으로 매핑관계를 설정한다.

Member 엔티티에서는 관계된 Team 엔티티는 1개를 초과할 수 없기 때문에 즉시 로딩을 한다 하더라도 큰 문제가 발생하지는 않는다.

 

하지만 Team 엔티티는 @OneToMany의 매핑 관계로 다수의 Member 엔티티를 조회하여 가져와야하는데 얼마나 많은 Member 일지도 모르며 여러개의 Team 엔티티 를 조회한다면 Member 의 갯수도 곱셈처리되어 성능에 문제가 생길것이다.

 

그렇기 때문에 관계가 다수이면 지연 로딩, 관계가 단일이면 즉시 로딩으로 기본 설정이 되어있는 것이다.

 

4. 지연 로딩을 권장

즉시 로딩은 최대한 사용을 미루며 최대한 지연 로딩을 기준으로 테이블 및 엔티티를 설계하는 것이 좋다.

 

상황에 따라서 즉시 로딩이 더 나을 상황도 많기는 하지만 그건 후에 리펙토링하여도 문제가 없을 뿐더러 만약 하나의 엔티티에 2개 이상의 엔티티의 관계로부터 즉시로딩을 사용하였다면 성능 하락의 큰 원인이 될 수 있기 때문이다.