티스토리 뷰
즉시 로딩과 지연 로딩
프록시 학습 처음에 했던 질문. Member를 조회할 때 Team도 함께 조회 해야 할까?
비즈니스 로직에서 단순히 멤버 로직만 사용하는데 함께 조회하면, 아무리 연관관계가 걸려있다고 해도 손해이다.
JPA는 이 문제를 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결 한다.
코드로 이해하기
Member와 Team 사이가 다대일 @ManyToOne 관계로 매핑되어 있는 상황에서,
@ManyToOne 어노테이션에 fetch 타입을 줄 수 있다.
FetchType.LAZY
public class Member extends BaseEntity {
strategy = GenerationType.IDENTITY) (
private Long id;
name = "name") (
private String username;
private Integer age;
EnumType.STRING) (
private RoleType roleType;
private String description;
// 패치 타입 LAZY 설정
fetch = FetchType.LAZY) (
name = "team_id", insertable = false, updatable = false) (
private Team team;
name = "locker_id") (
private Locker locker;
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 객체도 프록시 객체가 아니라 실제 객체이다.
public class Member extends BaseEntity {
...
fetch = FetchType.EAGER) (
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로 명시적으로 설정해서 사용하자
지연 로딩 활용
Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
Member와 Order는 가끔 사용한다 -> 지연 로딩
Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
즉시 로딩은 상상하지 못한 쿼리가 나간다.
Reference
'ICT Eng > JPA' 카테고리의 다른 글
[JPA] 프록시란? (0) | 2019.09.09 |
---|---|
[JPA] 영속성 컨텍스트와 플러시 이해하기 (1) | 2019.08.27 |
[JPA] @MappedSuperclass (2) | 2019.08.26 |
[JPA] 상속관계 매핑 전략(@Inheritance, @DiscriminatorColumn) (0) | 2019.08.26 |
[JPA] @ManyToMany, 다대다[N:M] 관계 (6) | 2019.08.23 |
- Total
- Today
- Yesterday
- ORM
- 레드블랙트리
- vuex
- 자바
- 젠킨스
- JPA
- AWS
- Vue.js
- github
- Recursion
- 스프링부트
- 순환
- Spring
- 정렬
- Wisoft
- Raspberry Pi
- IT융합인력양성사업단
- 한밭대학교
- 알고리즘
- Java
- 인프런
- vuejs
- 한밭이글스
- 무선통신소프트웨어연구실
- 시간복잡도
- Spring Boot
- springboot
- 라즈베리파이
- Algorithm
- RBT
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |