본문 바로가기
프레임워크/JPA

[JPA] 즉시로딩 지연로딩

by Yikanghee 2022. 3. 23.

즉시로딩 지연로딩

  • 지연로딩를을 사용하는 이유?
    • 만약 Member 엔티티에 Team이라는 엔티티가 속해 있는데 Member 엔티티를 조회할 때마다 Team엔티티도 함께 조회해야만 할까?
    • 답은 비즈니스 로직에서 필요하지 않을 때가 있는데 항상 Team을 함께 가져와서 사용할 필요는 없다
    • JPA는 낭비를 하지 않기 위해서 지연로딩을 사용한다

코드

  • Member와 Team 사이가 @ManyToOne 관계로 매핑되어 있는 상황에서, 어노테이션에 fetch 타입을 줄 수 있다
  • FetchType.LAZY
// 패치 타입 LAZY 설정
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id", insertable = false, updatable = false)
    private Team team;
  • 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 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 로딩이 설정 되어있는 Team 엔티티는 프록시 객체로 가져온다
    • 후에 실제 객체를 사용하는 시점에 초기화가 된다. DB 쿼리가 나간다
    • 대부분 비즈니스 로직에서 Member와 Team 을 같이 사용한다면
      • 이런경우는 LAZY 로딩을 사용한다면, SELECT 쿼리가 따로따로 2번 나간다
      • 네트워크를 2번 타서 조회가 이루어져 손해가 생긴다
      • 이때는 즉시로딩을 사용한다
  • 즉시로딩이란?
    • fetch 타입을 EAGER로 설정하면 된다
    @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
    
  • 프록시와 즉시 로딩 주의할 점
    • 즉시 로딩을 사용하면 예상치 못한 SQL이 발생한다
      • @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있다고 생각해보면
      • 조인이 5개 일어난다. 실무에서는 테이블이 더 많다
    • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
      • 실무에서 복잡한 쿼리를 많이 풀어내기 위해서 뒤에서 학습할 JPQL을 많이 사용한다
      • 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
        • "select m from Member m" 이 문장으로 당연히 Member만 SELECT 하게 된다.
        • 그러니 Member를 다 가져온것이다
      • 이때 문제가 생긴다 LAZY면 프록시를 넣으면 되는데, EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다
      • 이 말은 MEMBER를 와 연관된 TEAM을 모두 가져와야 한다는 소리다
      • 이게 N+1문제이다
      • 멤버가 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을 각각 쿼리 날려서 가져온다
    • 실무에서 이러한 문제가 터지면 답이없다..
    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 전략을 사용하자
    • 실무에서 대부분 멤버 팀을 함께 사용하는 경우가 있는데, 이런 경우에는 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 쓸 수 있다
    • @ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들은 기본이 즉시 로딩(EAGER) 이다. - 주의

이 글은 김영한님의 강의를 공부한 글입니다

댓글