티스토리 뷰


즉시 로딩과 지연 로딩

  • 프록시 학습 처음에 했던 질문. Member를 조회할 때 Team도 함께 조회 해야 할까?

    • 비즈니스 로직에서 단순히 멤버 로직만 사용하는데 함께 조회하면, 아무리 연관관계가 걸려있다고 해도 손해이다.

  • JPA는 이 문제를 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결 한다.

코드로 이해하기

  • Member와 Team 사이가 다대일 @ManyToOne 관계로 매핑되어 있는 상황에서,

  • @ManyToOne 어노테이션에 fetch 타입을 줄 수 있다.

  • FetchType.LAZY

    @Entity
    @Getter
    @Setter
    public class Member extends BaseEntity {

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

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

       private Integer age;

       @Enumerated(EnumType.STRING)
       private RoleType roleType;

       @Lob
       private String description;

       // 패치 타입 LAZY 설정
       @ManyToOne(fetch = FetchType.LAZY)
       @JoinColumn(name = "team_id", insertable = false, updatable = false)
       private Team team;

       @OneToOne
       @JoinColumn(name = "locker_id")
       private Locker locker;

       @OneToMany(mappedBy = "member")
       private List<MemberProduct> memberProducts = new ArrayList<>();

       public void changeTeam(Team team) {
           this.team = team;
           this.team.getMembers().add(this);
      }
    }
  • 메인 함수에서 팀과 멤버를 저장하고 조회 해보자.

    • Member를 조회하고, Team 객체의 클래스를 확인해보면 Proxy 객체가 조회 된다.

    Team team = new Team();
    team.setName("teamA");
    em.persist(team);

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

    member.changeTeam(team);

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

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

    System.out.println(findMember.getTeam().getClass());
    Hibernate: 
       select
          member0_.id as id1_4_0_,
          member0_.createdBy as createdB2_4_0_,
          member0_.createdDate as createdD3_4_0_,
          member0_.lastModifiedBy as lastModi4_4_0_,
          member0_.lastModifiedDate as lastModi5_4_0_,
          member0_.age as age6_4_0_,
          member0_.description as descript7_4_0_,
          member0_.locker_id as locker_10_4_0_,
          member0_.roleType as roleType8_4_0_,
          member0_.team_id as team_id11_4_0_,
          member0_.name as name9_4_0_,
          locker1_.id as id1_3_1_,
          locker1_.name as name2_3_1_
       from
          Member member0_
      left outer join
          Locker locker1_
               on member0_.locker_id=locker1_.id
       where
          member0_.id=?
           
    class hello.jpa.Team$HibernateProxy$e97rdqZR // 프록시 객체
  • 팀의 이름을 출력해보자

    • 이 시점에. 실제로 팀 객체의 조회가 필요한 시점에 쿼리가 나간다.

      Team team = new Team();
      team.setName("teamA");
      em.persist(team);

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

      member.changeTeam(team);

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

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

      System.out.println(findMember.getTeam().getClass());
      System.out.println("TEAM NAME : " + findMember.getTeam().getName());
      Hibernate: 
         select
            member0_.id as id1_4_0_,
            member0_.createdBy as createdB2_4_0_,
            member0_.createdDate as createdD3_4_0_,
            member0_.lastModifiedBy as lastModi4_4_0_,
            member0_.lastModifiedDate as lastModi5_4_0_,
            member0_.age as age6_4_0_,
            member0_.description as descript7_4_0_,
            member0_.locker_id as locker_10_4_0_,
            member0_.roleType as roleType8_4_0_,
            member0_.team_id as team_id11_4_0_,
            member0_.name as name9_4_0_,
            locker1_.id as id1_3_1_,
            locker1_.name as name2_3_1_
         from
            Member member0_
        left outer join
            Locker locker1_
                 on member0_.locker_id=locker1_.id
         where
            member0_.id=?
             
      class hello.jpa.Team$HibernateProxy$z4JtUeLD // 프록시 객체

      Hibernate:
         select
            team0_.id as id1_8_0_,
            team0_.createdBy as createdB2_8_0_,
            team0_.createdDate as createdD3_8_0_,
            team0_.lastModifiedBy as lastModi4_8_0_,
            team0_.lastModifiedDate as lastModi5_8_0_,
            team0_.name as name6_8_0_
         from
            Team team0_
         where
            team0_.id=?
             
      TEAM NAME : teamA

지연 로딩(LAZY)

  • 내부 매커니즘은 위의 그림과 같다.

  • 로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.

  • 후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점에) 초기화가 된다. DB에 쿼리가 나간다.

    • getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.

    • getTeam().getXXX()으로 팀의 필드에 접근 할 때, 쿼리가 나간다.

대부분 비즈니스 로직에서 Member와 Team을 같이 사용한다면?

  • 이런 경우 LAZY 로딩을 사용한다면, SELECT 쿼리가 따로따로 2번 나간다.

  • 네트워크를 2번 타서 조회가 이루어 진다는 이야기이다. 손해다.

  • 이때는 즉시 로딩(EAGER) 전략을 사용해서 함께 조회하면 된다.

즉시 로딩(EAGER)

  • fetch 타입을 EAGER로 설정하면 된다.

  • 대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.

  • 이렇게 하면, 실제 조회할 때 한방 쿼리로 다 조회해온다.(실제 Team을 사용할 때 쿼리 안나가도 된다.)

  • 실행 결과를 보면 Team 객체도 프록시 객체가 아니라 실제 객체이다.

    @Entity
    @Getter
    @Setter
    public class Member extends BaseEntity {

      ...

       @ManyToOne(fetch = FetchType.EAGER)
       @JoinColumn(name = "team_id", insertable = false, updatable = false)
       private Team team;

      ...
    }
    Team team = new Team();
    team.setName("teamA");
    em.persist(team);

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

    member.changeTeam(team);

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

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

    System.out.println(findMember.getTeam().getClass());
    System.out.println("TEAM NAME : " + findMember.getTeam().getName());
    tx.commit();
    실행 결과

    Hibernate:
       select
          member0_.id as id1_4_0_,
          member0_.createdBy as createdB2_4_0_,
          member0_.createdDate as createdD3_4_0_,
          member0_.lastModifiedBy as lastModi4_4_0_,
          member0_.lastModifiedDate as lastModi5_4_0_,
          member0_.age as age6_4_0_,
          member0_.description as descript7_4_0_,
          member0_.locker_id as locker_10_4_0_,
          member0_.roleType as roleType8_4_0_,
          member0_.team_id as team_id11_4_0_,
          member0_.name as name9_4_0_,
          locker1_.id as id1_3_1_,
          locker1_.name as name2_3_1_,
          team2_.id as id1_8_2_,
          team2_.createdBy as createdB2_8_2_,
          team2_.createdDate as createdD3_8_2_,
          team2_.lastModifiedBy as lastModi4_8_2_,
          team2_.lastModifiedDate as lastModi5_8_2_,
          team2_.name as name6_8_2_
       from
          Member member0_
      left outer join
          Locker locker1_
               on member0_.locker_id=locker1_.id
      left outer join
          Team team2_
               on member0_.team_id=team2_.id
       where
          member0_.id=?
           
    class hello.jpa.Team

    TEAM NAME : teamA

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

  • 실무에서는 가급적 지연 로딩만 사용하다. 즉시 로딩 쓰지 말자.

    • JPA 구현체도 한번에 가저오려고 하고, 한번에 가져와서 쓰면 좋지 않나?

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.

    • @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있다고 생각해보자.

    • 조인이 5개 일어난다. 실무에선 테이블이 더 많다.

  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

    • 실무에서 복잡한 쿼리를 많이 풀어내기 위해서 뒤에서 학습할 JPQL을 많이 사용한다.

    • em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다.(한방 쿼리)

    • 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.

      • "select m from Member m" 이 문장으로 당연히 Member만 SELECT 하게 된다.

      • MEMBER를 쭉 다 가져와서 보니까

    • 어 근데, Member 엔티티의 Team의 fetchType이 EAGER네?

      • LAZY면 프록시를 넣으면 되겠지만, EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다.

      • 따라서, Member를 다 가져오고 나서, 그 Member와 연관된 Team을 다시 다 가져온다.

    • 코드로 이해하기

      • 멤버가 2명이고, 팀도 2개다. 각각 다른 팀이다.

      • 모든 멤버를 조회해보자.

        Team team1 = new Team();
        team1.setName("teamA");
        em.persist(team1);

        Team team2 = new Team();
        team2.setName("teamB");
        em.persist(team2);

        Member member1 = new Member();
        member1.setUsername("memberA");
        em.persist(member1);
        member1.changeTeam(team1);

        Member member2 = new Member();
        member2.setUsername("memberB");
        em.persist(member2);
        member2.changeTeam(team2);

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

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

        tx.commit();
      • 실행 결과를 보면,

      • 일단 멤버를 조회해서 가져온다.

      • 그리고 나서 Member들의 Team이 비어있으니까 채워서 반환시키기 위해서 TEAM을 각각 쿼리 날려서 가져온다.

      • 멤버가 수천 수만명이라고 생각하면...... 아찔해진다.

      • N + 1의 문제의 의미

        • 아래 처럼 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.

        Hibernate: 
           /* select
               m
           from
               Member m */ select
                  member0_.id as id1_4_,
                  member0_.createdBy as createdB2_4_,
                  member0_.createdDate as createdD3_4_,
                  member0_.lastModifiedBy as lastModi4_4_,
                  member0_.lastModifiedDate as lastModi5_4_,
                  member0_.age as age6_4_,
                  member0_.description as descript7_4_,
                  member0_.locker_id as locker_10_4_,
                  member0_.roleType as roleType8_4_,
                  member0_.team_id as team_id11_4_,
                  member0_.name as name9_4_
               from
                  Member member0_
        Hibernate:
           select
              team0_.id as id1_8_0_,
              team0_.createdBy as createdB2_8_0_,
              team0_.createdDate as createdD3_8_0_,
              team0_.lastModifiedBy as lastModi4_8_0_,
              team0_.lastModifiedDate as lastModi5_8_0_,
              team0_.name as name6_8_0_
           from
              Team team0_
           where
              team0_.id=?
        Hibernate:
           select
              team0_.id as id1_8_0_,
              team0_.createdBy as createdB2_8_0_,
              team0_.createdDate as createdD3_8_0_,
              team0_.lastModifiedBy as lastModi4_8_0_,
              team0_.lastModifiedDate as lastModi5_8_0_,
              team0_.name as name6_8_0_
           from
              Team team0_
           where
              team0_.id=?
    • 결론. 실무에서는 LAZY 로딩 전략을 가져가자.

      • 근데 실무에서 대부분 멤버 팀을 함께 사용하는 경우가 있는데, 그러면 LAZY로 해놓고 계속 쿼리 두방 날려서 조회 해올까요?

      • 이런 경우를 위해서 JPQL의 fetch join 을 통해서 해당 시점에 한방 쿼리로 가져와서 쓸 수 있다.

      • 추가적으로 엔티티그래프와 어노테이션으로 푸는 방법, 배치 사이즈 설정으로 해결하는 방법이 있다.

      • 대부분 fetch join으로 해결 한다.

  • @ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들은 기본이 즉시 로딩(EAGER) 이다.

    • 꼭 LAZY로 명시적으로 설정해서 사용하자

  • @OneToMany와 @ManyToMany는 기본이 지연 로딩(LAZY)이다.

지연 로딩 활용

  • Member와 Team을 자주 함께 사용한다 -> 즉시 로딩

  • Member와 Order는 가끔 사용한다 -> 지연 로딩

  • Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩

  • 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고

  • 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.

  • JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.

  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

Reference







댓글