5 분 소요

📌 Topic

  • 💡 지연 로딩
  • 💡 즉시 로딩
  • 💡 JPQL fetch join
  • 💡 프록시와 즉시 로딩 주의점

⚡ 01. Member를 조회할 때 Team도 함께 조회해야 할까?

프록시_01

이전 장(프록시)에서 설명한 예시와 같은 맥락이다.
단순히 맴버의 데이터만 필요한 경우 팀까지 조인을 해야 하는가?

✅ 지연 로딩 사용

@Entity
public class Member {

    //..중략

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    @JoinColumn
    private Team team;

    //..중략
}

우선 지연 로딩을 사용하기 위해서는 FetchType.LAZY키워드를 추가해줘야 한다. 다음은 실제 호출부에서 Member 엔티티를 조회하는 예제를 살펴보자.

✅ 지연 로딩 사용(Member 엔티티만 조회)

try {
    Member member = new Member();
    member.setUsername("member");
    em.persist(member);

    em.flush();
    em.clear();

    Member m = em.find(Member.class, member.getId());

    tx.commit();
} catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
} finally {
    em.close();
}

현재 Member와 Team은 N : 1 연관관계 매핑으로 설정이 되어있다. 해당 쿼리를 날리면 기존에는 Member와 Team을 조인하여 데이터를 가져왔다. 하지만 현재는 지연 로딩을 사용하여 Team 엔티티의 데이터는 가져오지 않는다.

🖨 출력 결과

Hibernate:
    select
        member0_.MEMBER_ID as member_i1_9_0_,
        member0_.INSERT_MEMBER as insert_m2_9_0_,
        member0_.createdDate as createdd3_9_0_,
        member0_.lastModifiedBy as lastmodi4_9_0_,
        member0_.lastModifiedDate as lastmodi5_9_0_,
        member0_.team_TEAM_ID as team_tea7_9_0_,
        member0_.USERNAME as username6_9_0_
    from
        Member member0_
    where
        member0_.MEMBER_ID=?

Team 엔티티의 정보는 가져오지 않고 Member의 정보만 가져온 상태다.

✅ 지연 로딩 사용(Team 엔티티 같이 조회)

try {
    Team team = new Team();
    team.setName("첫번째 팀1");
    em.persist(team);

    Member member = new Member();
    member.setUsername("member");
    member.setTeam(team);
    em.persist(member);

    em.flush();
    em.clear();

    Member m = em.find(Member.class, member.getId());

    System.out.println("m = " + m.getTeam().getClass());

    System.out.println("========================");
    m.getTeam().getName(); // 실제로 프록시 객체가 사용이 되는 순간
    System.out.println("========================");

    tx.commit();
} catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
} finally {
    em.close();
}

emf.close();

여기서 짚고 가야 하는 부분은 ‘언제 team을 조회하는 쿼리가 날라가는가?’ 이다. 해당 코드에서는 member의 참조 변수(m)를 통해 team을 직접 호출하는 경우 쿼리가 날라간다.

🖨 출력 결과

Hibernate:
    select
        member0_.MEMBER_ID as member_i1_9_0_,
        member0_.INSERT_MEMBER as insert_m2_9_0_,
        member0_.createdDate as createdd3_9_0_,
        member0_.lastModifiedBy as lastmodi4_9_0_,
        member0_.lastModifiedDate as lastmodi5_9_0_,
        member0_.team_TEAM_ID as team_tea7_9_0_,
        member0_.USERNAME as username6_9_0_
    from
        Member member0_
    where
        member0_.MEMBER_ID=?
m = class com.hello.jpatest.Team$HibernateProxy$jTEcGBQ5
========================
Hibernate:
    select
        team0_.TEAM_ID as team_id1_14_0_,
        team0_.name as name2_14_0_
    from
        Team team0_
    where
        team0_.TEAM_ID=?
========================

위에서 말했다시피 team 엔티티의 경우, 실제 team 객체가 사용이 되는 경우 위와 같이 조회 쿼리를 날린다. 또한 team은 프록시 객체를 가져오게 된다.

// 프록시 객체 조회
m = class com.hello.jpatest.Team$HibernateProxy$jTEcGBQ5

⚡ 지연 로딩

2022-02-21_지연로딩_01

member1을 로딩할 때 team1 엔티티는 지연 로딩으로 셋팅되어 프록시 객체 반환.

✅ 지연 로딩 LAZY를 사용하여 프록시로 조회

2022-02-21_지연로딩_02

Member member = em.find(Member.class, 1L);

우선 첫 번째 사진에서는 Team 엔티티가 지연 로딩으로 설정이 되어 있기 때문에 Member 객체를 가져온 후에 가짜 객체(프록시) Team을 가져오게 된다.

2022-02-21_지연로딩_03

Team team = member.getTeam();
team.getName(); // 실제 사용하는 시점에 초기화

두 번째는 member 참조 변수를 통해 team 객체를 얻은 후에 실제로 team.getName()을 선언하는 순간 DB 초기화가 이루어진다. 이 부분에서 헷갈리면 안되는 부분은 team 객체를 가져올 때 초기화가 되는 것이 아니라 프록시 객체를 터치할 때 초기화가 이루어진다.

⚡ 02. Member와 Team을 자주 함께 사용한다면?

2022-02-21_지연로딩_03

FetchType.LAZY의 경우 쿼리가 두번 나가고, 네트워크 역시 두번 탄다.

지금까지는 Member와 Team 엔티티에서 Member의 데이터만 필요한 경우를 살펴보았다. 이번에는 비즈니스 상에서 Member와 Team이 함께 사용이 되어야 하는 경우는 어떻게 해야 하는지 알아보자.

✅ 즉시 로딩 사용

@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn
private Team team;

즉시 로딩을 사용하기 위해 FetchType.EAGER 키워드를 추가 하였다.

✅ 즉시 로딩 사용(Member 조회)

try {
    Team team = new Team();
    team.setName("첫번째 팀1");
    em.persist(team);

    Member member = new Member();
    member.setUsername("member");
    member.setTeam(team);
    em.persist(member);

    em.flush();
    em.clear();

    Member m = em.find(Member.class, member.getId());

    System.out.println("m = " + m.getTeam().getClass());

    System.out.println("========================");
    System.out.println("teamName = " + m.getTeam().getName());
    System.out.println("========================");

    tx.commit();
} catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
} finally {
    em.close();
}

emf.close();

이전과 달라진 부분은 존재하지 않는다. 다음 출력 결과를 확인 해보자.

🖨 출력 결과

Hibernate:
    select
        member0_.MEMBER_ID as member_i1_9_0_,
        member0_.INSERT_MEMBER as insert_m2_9_0_,
        member0_.createdDate as createdd3_9_0_,
        member0_.lastModifiedBy as lastmodi4_9_0_,
        member0_.lastModifiedDate as lastmodi5_9_0_,
        member0_.team_TEAM_ID as team_tea7_9_0_,
        member0_.USERNAME as username6_9_0_,
        team1_.TEAM_ID as team_id1_14_1_,
        team1_.name as name2_14_1_
    from
        Member member0_
    left outer join
        Team team1_
            on member0_.team_TEAM_ID=team1_.TEAM_ID
    where
        member0_.MEMBER_ID=?
m = class com.hello.jpatest.Team
========================
teamName = 첫번째 팀1
========================

즉시 로딩을 사용하는 경우 프록시 객체가 아닌 초기화가 끝난 실제 객체(엔티티)를 가져오게 된다. 즉, Member와 Team을 조인한 결과를 반환한다는 의미다.

⚡ 즉시 로딩

2022-02-21_즉시로딩_04

위 예제에서 살펴 보았지만 Member와 Team이 한 번에 사용이 되야하는 경우에는 즉시 로딩을 사용하는 것이 이점이 있다. 또한 JPA 구현체는 가능하면 조인을 사용하여 SQL을 한 번에 조회한다.

🔥 프록시와 즉시 로딩 주의

만약 테이블 1, 2개가 조인이 되 있는 경우는 상관이 없겠지만, 수십개의 테이블이 조인 관계로 형성이 되어 있는경우..? 즉시 로딩을 사용하게 되면 해당 모든 테이블을 조인하여 조회하는 불상사가 발생할 것이다.

위의 경우는 단편적인 예시 중 하나입니다.
결론은 실무에서 즉시 로딩 사용은 반드시 지양해야 합니다.

  1. 실무에서는 가급적 지연 로딩만 사용해야 한다.
  2. 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생.
  3. 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  4. @ManyToOne @OneToOne은 기본이 즉시 로딩 (Default : EAGER)
  5. @OneToMany, @ManyToMany는 기본이 지연 로딩 (Default : LAZY)

@ManyToOne, @OneToOne 시리즈는 기본이 즉시로딩, 직접 설정 해야 한다.
@ManyToMany, @OneToMany 시리즈는 기본이 지연 로딩

List<Member> members =
    em.createQuery("select m from Member m", Member.class).getResultList();

// SELECT * FROM MEMBER
// EAGER을 발견..
// SELECT * FROM TEAM WHERE TAM_ID = xxx
  • JPQL을 사용하는 경우 FetchType이 EAGER이여도 쿼리가 두번 나간다?
  • JPQL은 SQL을 그대로 번역한다.
    • Member를 조회하여 가져왔는데 Member에 EAGER이 존재한다.
    • 즉시 로딩을 사용하기 때문에 Team 객체를 조회하는 쿼리가 한번 더 나간다.

🍃 JPQL N + 1 해결 방안

실무에서는 웬만하면 지연 로딩으로 설정한다. 만약 여러개의 테이블에서 데이터를 조회하는 경우에는 fetch join을 사용하여 해당 이슈를 처리한다.

  1. 지연 로딩으로 모든 연관관계를 설정한다.
  2. Fetch JOIN을 사용한다.
    1. RunTime 상황에서 동적으로 원하는 부분을 선택하여 가져올 수 있는 기능.
  3. Batch Size 등등
List<Member> members =
    em.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_.MEMBER_ID as member_i1_9_0_,
            team1_.TEAM_ID as team_id1_14_1_,
            member0_.INSERT_MEMBER as insert_m2_9_0_,
            member0_.createdDate as createdd3_9_0_,
            member0_.lastModifiedBy as lastmodi4_9_0_,
            member0_.lastModifiedDate as lastmodi5_9_0_,
            member0_.team_TEAM_ID as team_tea7_9_0_,
            member0_.USERNAME as username6_9_0_,
            team1_.name as name2_14_1_
        from
            Member member0_
        inner join
            Team team1_
                on member0_.team_TEAM_ID=team1_.TEAM_ID
  • JPQL fetch join을 사용하는 경우 지연로딩이여도 두 테이블의 데이터를 한 번에 가져올 수 있다.

⚡ 지연 로딩 활용

2022-02-21_지연로딩활용

아래에서 설명하는 내용은 상당히 이론적인 내용이다.
위에서 언급 했다시피 실무에서는 반드시 지연 로딩을 사용해야 한다.

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

💻 지연 로딩 활용(실무)

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

참고 자료

댓글남기기