728x90
@ManyToOne(fetch = FetchType.EAGER)
즉시 로딩(EAGER)
- fetch 타입을 EAGER로 설정하면 된다.
- 대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
- 이렇게 하면, 실제 조회할 때 한방 쿼리로 다 조회해온다.(실제 Team을 사용할 때 쿼리 안나가도 된다.)
- 실행 결과를 보면 Team 객체도 프록시 객체가 아니라 실제 객체이다.
출처: https://ict-nroo.tistory.com/132 [개발자의 기록습관:티스토리]
지연 로딩(LAZY)
- 내부 매커니즘은 위의 그림과 같다.
- 로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.
- 후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점에) 초기화가 된다. DB에 쿼리가 나간다.
- getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
- getTeam().getXXX()으로 팀의 필드에 접근 할 때, 쿼리가 나간다.
@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 // 프록시 객체팀의 이름을 출력해보자이 시점에. 실제로 팀 객체의 조회가 필요한 시점에 쿼리가 나간다.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 : teamATeam 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());
대부분 비즈니스 로직에서 Member와 Team을 같이 사용한다면?
- 이런 경우 LAZY 로딩을 사용한다면, SELECT 쿼리가 따로따로 2번 나간다.
- 네트워크를 2번 타서 조회가 이루어 진다는 이야기이다. 손해다.
- 이때는 즉시 로딩(EAGER) 전략을 사용해서 함께 조회하면 된다.
즉시 로딩(EAGER)
- fetch 타입을 EAGER로 설정하면 된다.
- 대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
- 이렇게 하면, 실제 조회할 때 한방 쿼리로 다 조회해온다.(실제 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
public class Member extends BaseEntity {
...
fetch = FetchType.EAGER) (
name = "team_id", insertable = false, updatable = false) (
private Team team;
...
}
프록시와 즉시 로딩 주의할 점
- 실무에서는 가급적 지연 로딩만 사용하다. 즉시 로딩 쓰지 말자.
- 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로 명시적으로 설정해서 사용하자
지연 로딩 활용
- Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
- Member와 Order는 가끔 사용한다 -> 지연 로딩
- Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
- 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
- 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
- JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
Reference
출처: https://ict-nroo.tistory.com/132 [개발자의 기록습관:티스토리]
[JPA] 즉시 로딩과 지연 로딩(FetchType.LAZY or EAGER)
즉시 로딩과 지연 로딩 프록시 학습 처음에 했던 질문. Member를 조회할 때 Team도 함께 조회 해야 할까? 비즈니스 로직에서 단순히 멤버 로직만 사용하는데 함께 조회하면, 아무리 연관관계가 걸려
ict-nroo.tistory.com
'개발 > JPA' 카테고리의 다른 글
@GeneratedValue 자동 생성 전략 (0) | 2022.07.21 |
---|---|
history Envers로 개발 @Configuration @EnableJpaAuditing (0) | 2022.07.21 |
JPA 나만의 정리 (3) - 엔티티(Entity)와 매핑 (0) | 2022.07.21 |
JPA 나만의 정리 (2) - lombok 어노테이션 (0) | 2022.07.21 |
JPA 나만의 정리 (1) - Build 패턴 (0) | 2022.07.21 |