DEV ℧ Developer Diary

[JPA] 즉시 로딩과 지연 로딩

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

즉시 로딩과 지연 로딩

바로 전 포스팅에서 배운 Proxy 객체를 이용해, Member 객체와 Team 객체와 같이 연관관계가 걸려있는 Entity의 조회에 대해 알아봤다.

여기서 Porxy객체를 조회해 오는 방법은 크게 두가지가 있는데, 이에 대한 조회 방법을 알아보자.

지연로딩

지연로딩

member1을 로딩시 fetch = FetchType.LAZY가 선언 되어있는 Team 객체를 Proxy객체로 가져온다.

지연로딩

그리고 해당 Team에 대한 객체를 실제로 호출할시 DB로 쿼리를 날려 초기화를 실행한다.

위의 개념을 예제코드를 통해 알아보도록 하자.

지연 로딩 LAZY을 사용해서 프록시로 조회

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

fetch = FetchType.LAZY 을 통해 Team의 연관관계를 지연로딩으로 선언했다.

해당하는 Team 객체를 호출 해보자.

Team team1 = new Team();
team1.setName("team1");
entityManager.persist(team1);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1);
entityManager.persist(member1);

entityManager.flush();
entityManager.clear();

Member findMember = entityManager.find(Member.class, member1.getId());
System.out.println("findMember.getTeam : " + findMember.getTeam().getClass());
Hibernate:
    select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_
    from
        Member member0_
    where
        member0_.id=?
findMember.getTeam : class com.dhaudgkr.jpa05.lazyloading.domain.Team$HibernateProxy$QTAJeivP

Member Entity에 속해있는 Team 객체를 Proxy객체로 조회해 오는것을 확인 할 수 있다. 그렇다면 Team 객체를 호출해 보자.

System.out.println("before call Team");
System.out.println("team : " + findMember.getTeam().getName());
System.out.println("after call Team");
before call Team
Hibernate:
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_
    from
        Team team0_
    where
        team0_.id=?
team : team1
after call Team

findMember.getTeam().getName() 을 이용해 team의 정보를 호출하자 조회 쿼리가 호출되는 것을 확인 할 수 있다.

예를 들어 전체 애플리케이션에서 90%이상 Member만 쓰고 Team을 사용하지 않는다는 가정하에는 지연로딩을 선언해서 Member만 사용을 하게된다.

그렇다면 Team을 같이 사용하는 경우가 다반사라고 하면 어떻게 해야할까?

즉시로딩

즉시로딩1

member1을 로딩시 fetch = FetchType.EAGLE가 선언 되어있는 Team 객체를 가능한한 조인 쿼리를 날려 함께 조회해 온다.

즉시로딩2

즉시 로딩 EAGER를 사용해서 함께 조회

사용법은 지연로딩과 동일하다, FetchType을 EAGLE로 선언해주면 된다.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
Team team1 = new Team();
team1.setName("team1");
entityManager.persist(team1);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1);
entityManager.persist(member1);

entityManager.flush();
entityManager.clear();

Member findMember = entityManager.find(Member.class, member1.getId());
System.out.println("findMember.getTeam : " + findMember.getTeam().getClass());
select
        member0_.id as id1_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        member0_.USERNAME as username2_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.TEAM_ID=team1_.id
    where
        member0_.id=?
findMember.getTeam : class com.dhaudgkr.jpa05.eagleloading.domain.Team

위에서 Member에 대해 호출 할 시 Team에 대한 데이터를 모두 조회하고, team에 대한 객체도 Proxy가 아닌 실제 Entity 객체를 가져오는 것을 볼 수 있다.

프록시와 즉시로딩에서 주의 할점

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. (JPA의 유명한 문제)
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

N + 1?

N + 1에 대해서 간략하게 알아보도록 하자.

Team team1 = new Team();
        team1.setName("team1");
        entityManager.persist(team1);

        Team team2 = new Team();
        team2.setName("team2");
        entityManager.persist(team2);

        Member member1 = new Member();
        member1.setUsername("member1");
        member1.setTeam(team1);
        entityManager.persist(member1);

        Member member2 = new Member();
        member2.setUsername("member2");
        member2.setTeam(team2);
        entityManager.persist(member2);

        entityManager.flush();
        entityManager.clear();

        List<Member> members = entityManager.createQuery("select m from Member m", Member.class).getResultList();
Hibernate:
    /* select
        m
    from
        Member m */ select
            member0_.id as id1_0_,
            member0_.TEAM_ID as team_id3_0_,
            member0_.USERNAME as username2_0_
        from
            Member member0_

Hibernate:
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_
    from
        Team team0_
    where
        team0_.id=?

Hibernate:
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_
    from
        Team team0_
    where
        team0_.id=?

Member 객체만 불러올시 Team객체까지 추가로 쿼리가 나간다.

이렇게 의도한 쿼리(N) 이외에 의도치 않은 쿼리들이 추가로 나가는것(+1)으로 해당 현상을 N+1 이라고 한다.

N+1현상이 일어나는 원인은 JPA가 아닌 JPQL은 entityManager.createQuery("select m from Member m", Member.class)으로 선언한 쿼리가 그대로 SQL문으로 번역이 된다. 그 과정에서 연관관계에 있는 객체들 까지 가져오게 된다. 만약 해당 연관관계가 Team외에 더 많고, Member의 수가 더많다면 의도치 않은 조회쿼리가 더많이 나갈 수 있게 된다.

해결을 위해서는 다음과 같은 방법이 있다.

먼저 로딩에 대한 관계를 모두 지연로딩으로 깔아준다. 이후 대표적인 해결방안으로는 3가지 방안이 있다.

  1. 해당 현상을 해결 하기 위해서는 fetch join을 사용한다. 이 방법은 모든 데이터를 조회해오기 때문에 추가로 값을 조회해도 조회쿼리가 나가지 않는다.

Team 객체를 LAZY의 지연로딩으로 설정을 한다면, Member 객체를 조회해도 Team 객체는 조회해오지 않게된다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;

하지만 JPQL에서 fetch join 을 적용해 team을 조회한다면?

List<Member> members = entityManager.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
Hibernate:
    /* select
        m
    from
        Member m
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as id1_1_1_,
            member0_.TEAM_ID as team_id3_0_0_,
            member0_.USERNAME as username2_0_0_,
            team1_.name as name2_1_1_
        from
            Member member0_
        inner join
            Team team1_
                on member0_.TEAM_ID=team1_.id

위의 예제외 같이 Member에 Team 객체를 조인해서 모두 가져온다. 실무에서는 해당 방법을 많이 사용하므로, 알아두는것이 좋다.

나머지 방법에 대해서는 추가로 포스팅을 통해 후술하도록 하겠다.

지연로딩의 활용

해당 내용은 이론적인 내용 일뿐 실무 에서는 무조건 지연로딩만 사용해야한다!!!

  • Member와 Team은 자주 함께 사용 -> 즉시 로딩
  • Member와 Order는 가끔 사용 -> 지연 로딩
  • Order와 Product는 자주 함께 사용 -> 즉시 로딩

지연로딩의 활용1

지연로딩의 활용2

지연로딩의 활용3

지연로딩의 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! (뒤에서 설명)
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.