Lazy Loading, Eager Loading에 관한 고찰

·2023년 8월 9일

프로젝트-요즘카페

목록 보기
2/12

Fetch Type의 종류 및 특징

JPA 연관관계에서는 Fetch Type을 정해줄 수 있다.
Lazy, Eager 둘 중 하나를 선택할 수 있다.

무엇을 하는 설정이냐면,
JPA를 사용해서 엔티티를 조회해올 때 연관 관계의 엔티티들도 Select 해올 것인지 아니면 프록시 객체로 대체할 것인지를 정하는 것이다.

  • FetchType.Lazy
    • 프록시 객체로 대체한다
    • 읽기 트랜잭션이 끝나지 않은 한 필요할 때 Select절로 조회해올 수 있다.
      • 쓰기 트랜잭션의 경우도 당연히 가능하다
      • OSIV가 켜져있을 경우 컨트롤러 영역에서도 조회 가능하다(DB 커넥션이 유지되기 때문)
  • FetchType.Eager
    • 실제 DB 데이터를 조회해와서 만든 엔티티로 매핑한다
    • 프록시가 아니기 때문에 트랜잭션의 존재 유무와 상관없이 언제든 연관 객체를 사용할 수 있다.

JPA의 연관 관계는 Fetch Type의 관점에서 두 가지로 나눌 수 있다

  • ~ToMany
    • 컬렉션으로 가지는 연관 관계의 경우는 FetchType.Lazy가 기본 설정
  • ~ToOne
    • 단일 객체로 가지는 연관 관계의 경우는 FetchType.Eager가 기본 설정

위와 같이 필요한 선행 지식을 간단하게 정리해봤다.

의문

주변에서 JPA를 공부하고 사용하는 사람들을 보면 공통적으로 항상 연관 관계들의 Fetch Type을 Lazy로 명시한다.
만약 즉시 로딩을 한다면 필요 없는 순간에도 연관 관계에 대한 Select 문을 날리기 때문이다.

사실 지연 로딩만 쓰는 것이 가장 좋다면, 왜 JPA 개발자들은 ~ToOne 관계의 기본을 Eager로 했을까?

즉시 로딩이 좋은 경우가 있을 것이라고 가정하고 두 가지 상황을 살펴보자.

예제1 - 일대다 관계

Team과 Member가 1:N의 연관 관계를 가진다

@Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JoinColumn(name = "teamId")
    @OneToMany(cascade = CascadeType.PERSIST)
    private List<Member> members;
}
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
  1. 우선 기본 정책인 Lazy인 경우에서 팀을 조회해오고 게터로 멤버를 꺼내는 상황을 테스트 해본다.

    위와 같이 추가적인 Select문이 실행된다.

  2. Eager로 설정하고, 팀을 조회한 뒤 게터로 멤버를 꺼내는 상황을 테스트 해본다.

    위와 같이 Outer Join으로 가져온다.

고찰

만약 Team을 사용할 때 항상 Member가 필요하다면 Eager 전략이 더 좋을 수도 있다고 생각한다.
다만 주의해야할 점으로는

  • 정말 항상 필요한가?
    • 항상 필요하지 않다면 Lazy로 해두고 필요할 때만 Fetch Join을 사용하는 등의 방법이 좋다고 생각한다
  • 항상 모두 가져와서 로드해도 메모리가 문제되지 않는가?
    • Member가 Team마다 1000명씩 있다면 Team 1000개를 가져올 때는 멤버 인스턴스가 최대 1000000개일 것이다.
    • 항상 필요하다고 해도, OOM이 발생할 수 있기 때문에 @BatchSize를 걸고 Fetch Join을 사용하는 등의 방법이 좋을 수 있다

위의 주의해야할 점을 모두 충족한다면 Eager가 더 나을 수도 있다.

하지만 충족한다고 생각하더라도 비즈니스 요구사항이 변경되면 바뀔 수 있다. 이 경우 Lazy로 바꾸고 관련 부분에 대한 N+1 문제를 해결하기 위해 리팩토링할 cost가 매우 클 수 있다.

위와 같은 생각으로 컬렉션을 연관 관계로 가질 때는 항상 Lazy를 사용하는 것이 좋아보인다.

예제2 - 일대일 관계

이번에는 Team과 Member가 1:1의 연관 관계를 가진다

  1. Lazy일 때, 팀을 조회하고 게터로 멤버를 꺼내는 상황을 테스트 해본다.

    당연히 위와 같이 Select 문이 실행된다.

  2. Eager일 때, 팀을 조회하고 게터로 멤버를 꺼내는 상황을 테스트 해본다.

    Outer Join으로 가져온다.

고찰

예제1과 비슷해보이지만 주의해야할 점이 한 가지가 줄었다.
메모리 로드 문제를 걱정하지 않아도 된다.

아래의 조건 중 하나를 충족한다면 Eager를 사용하는 것이 좋아보인다.

  • 항상 필요한 연관 관계이다
    • 당연히 Join으로 가져오는 것이 좋다고 생각한다.
  • 연관 관계 엔티티가 그리 크지 않고 필요하지 않을 때가 많이 없다
    • 성능상으로는 Lazy로 설정하고 필요할 때만 Join해서 가져오는 것이 좋다고 생각한다.
    • 하지만 필요하지 않을 때가 거의 없다면, 그 순간을 위해 Fetch Join을 준비하는 것이 유지 보수 관점에서 더 이득일지는 잘 모르겠다.
      1. Fetch Join을 하기 위해서는 JPQL을 사용하거나 @EntityGraph를 사용해야 한다.
      2. 만약 JPA Repository를 쓴다면 Global Fetch 전략보다 우선시하기 위해 사용하는 모든 메서드를 오버라이드해야 한다.
    • 휴먼 에러가 발생할 수 있는 포인트를 늘리면서 필요할 때만 Fetch Join을 사용하는 것이 더 좋을 지는 때에 따라 다를 것 같다.

위와 같은 생각으로 단일 객체를 연관 관계로 가질 때는 상황마다 다를 것 같다.

profile
渽晛

0개의 댓글