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

무1민·2023년 7월 18일
0

JPA

목록 보기
1/3

즉시 로딩과 지연 로딩

  • 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)

  • fetch 타입을 EAGER로 설정하면 된다.
  • 대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
  • 이렇게 하면, 실제 조회할 때 한방 쿼리로 다 조회해온다.(실제 Team을 사용할 때 쿼리가 추가로 안나가도 된다.)

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

  • 실무에서는 가급적 지연 로딩만 사용하자. 즉시 로딩을 쓰지 말자
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
    • @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있으면, 조인이 5개가 일어난다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    • em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다.(한방 쿼리)
    • 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
      • "select m from Member m"에서 이 문장에서 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를 조회해서 가져온 후, Member들의 Team이 비어있으니까 채워서 반환시키기 위해 TEAM을 각각의 쿼리를 날려서 가져온다.
  • Member 수 만큼 쿼리를 추가로 날린다는 소리다.
  • 이처럼, N+1의 문제의 의미는
    • 아래처럼 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.
Hibernate:
   		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을 이용해서 한방쿼리를 사용해 쓸 수 있다.
  • @ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들은 기본이 즉시 로딩(EAGER) 이다.
    • 꼭 LAZY로 명시적으로 설정해서 사용하자
  • @XXXToMany는 기본이 지연 로딩이다.
profile
야호

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

정말 좋은 정보 감사합니다!

답글 달기