지연 로딩
예를 들어 Member와 Team이 서로 다대일 연관관계 매핑 되어있다고 하자.
이런 상황에서 기본적으로 Member를 조회하게 된다면 Member에 대한 select 쿼리가 나가고 그 안에 Team에 대한 Join 쿼리도 존재할 것이다. 코드로 보자.
JpaMain.class
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUserName("member1");
em.persist(member);
Team team = new Team();
team.setName("team1");
em.persist(team);
member.setTeam(team);
// DB에 쿼리 날리고 영속성 컨텍스트 비움
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
tx.commit();
} catch (Exception e) {
tx.rollback(); //오류 발생 시 롤백
} finally {
//종료
em.close();
}
emf.close();
}
}
출력>>
Hibernate:
select
member0_.MEMBER_ID as member_i1_1_0_,
member0_.createdBy as createdb2_1_0_,
member0_.createdDate as createdd3_1_0_,
member0_.modifiedBy as modified4_1_0_,
member0_.modifiedDate as modified5_1_0_,
member0_.TEAM_ID as team_id7_1_0_,
member0_.USERNAME as username6_1_0_,
team1_.TEAM_ID as team_id1_4_1_,
team1_.createdBy as createdb2_4_1_,
team1_.createdDate as createdd3_4_1_,
team1_.modifiedBy as modified4_4_1_,
team1_.modifiedDate as modified5_4_1_,
team1_.name as name6_4_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team
출력 결과를 보면 Member에 대해서 Team이 left outer join 했다고 한다. 잘 모르겠지만 암튼 조인 된 듯 하다. 여기서 확인 할 수 있는 것은 한번의 Select 쿼리를 통해서 Member와 Team이 모두 조회 되었다는 것이다.
자 그렇다면 Member의 Team의 @ManyToOne의 fetch를 LAZY로 바꿔보자.
@ManyToOne(fetch = FetchType.LAZY)
Member.class
@Entity
@Getter @Setter
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false) // 매핑할 컬럼명이 "name". + not null 설정
private String userName;
@ManyToOne(fetch = FetchType.LAZY) //지연 로딩, Member 클래스만 조회함. Member.team은 프록시 객체
@JoinColumn(name = "TEAM_ID")
private Team team;
// 이하 생략
}
지금 Member.team을 보면 @ManyToOne(fetch = FetchType.LAZY)로 되어있는 것을 확인 할 수 있다. 그리고 이는 다음을 의미한다.
"Member를 조회할 때 Team은 프록시로 가져온다"
그러면 이 상태에서 다시 한번 JpaMain 클래스를 실행 해보자.
결과
Hibernate:
select
member0_.MEMBER_ID as member_i1_1_0_,
member0_.createdBy as createdb2_1_0_,
member0_.createdDate as createdd3_1_0_,
member0_.modifiedBy as modified4_1_0_,
member0_.modifiedDate as modified5_1_0_,
member0_.TEAM_ID as team_id7_1_0_,
member0_.USERNAME as username6_1_0_
from
Member member0_
where
member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team$HibernateProxy$lVxj7lsb
결과를 보면 Team이 조회되지 않았다. 또한 findMember.team의 클래스를 확인해보면 프록시임을 확인 가능하다. 그렇다면 JpaMain에 다음 코드도 추가해보자.
추가 코드
System.out.println("=====================");
String teamName = findMember.getTeam().getName(); // 초기화, 쿼리 날아감
System.out.println("=====================");
String teamName2 = findMember.getTeam().getName(); // Team 엔티티가 영속성 컨텍스트에 이미 올라와서 쿼리 안나감
System.out.println("=====================");
결과
Hibernate:
select
member0_.MEMBER_ID as member_i1_1_0_,
member0_.createdBy as createdb2_1_0_,
member0_.createdDate as createdd3_1_0_,
member0_.modifiedBy as modified4_1_0_,
member0_.modifiedDate as modified5_1_0_,
member0_.TEAM_ID as team_id7_1_0_,
member0_.USERNAME as username6_1_0_
from
Member member0_
where
member0_.MEMBER_ID=?
findMember.getTeam().getClass() = class hellojpa.Team$HibernateProxy$2QtKq9bM
=====================
Hibernate:
select
team0_.TEAM_ID as team_id1_4_0_,
team0_.createdBy as createdb2_4_0_,
team0_.createdDate as createdd3_4_0_,
team0_.modifiedBy as modified4_4_0_,
team0_.modifiedDate as modified5_4_0_,
team0_.name as name6_4_0_
from
Team team0_
where
team0_.TEAM_ID=?
=====================
=====================
다시 결과를 보면 이제는 Team에 대한 select문이 생겼다. 이는 프록시 객체인 Team을
String teamName = findMember.getTeam().getName();로 조회하는 과정에서 초기화가 일어났기 때문이다.
초기화 과정에서 DB에 쿼리를 날려서 Team에 대한 select문이 생긴 것이고 그 후에는 Team 엔티티가 영속성 컨텍스트에 저장이 되었기 때문에 두번째 getName()에서는 쿼리가 나오지 않는 것을 확인 할 수 있다.
중간 정리
- FetchType.LAZY로 지연로딩이 가능하다. 지연로딩을 하면 프록시로 객체를 가져온다.
- FetchType.EAGER로 즉시 로딩이 가능하다. 즉시 로딩은 조회한 객체와 연관 객체가 같이 사용되는 경우가 많은 경우 유리하다.
- 하지만 왠만하면 지연로딩을 사용하는 것이 좋다. 연관 객체가 많은 경우 EAGER로 조회하면 쿼리가 폭발한다.
- @xxxToMany는 디폴트가 FetchType.LAZY이다.
- @xxxToOne은 디폴트가 FetchType.EAGER니 꼭 FetchType.LAZY로 바꾸자.
- JPQL에서 FetchType.EAGER는 n+1 문제를 발생시킨다.
여기서 "JPQL에서 FetchType.EAGER는 n+1 문제를 발생시킨다." 이 부분만 설명해보자면 JPQL로 Member를 조회하는 경우 Member.team이 FetchType.EAGER라면 이것은 "Member가 조회될 때 Team도 반드시 내용이 있어야한다!" 라는 의미이다. 근데 문제는 JPQL은 SQL 처럼 Member를 조회하면 Member만 반환하기 때문에 JPQL로 조회하는 당시에는 Team이 조회되지 않는다. 그래서 JPA는 Member를 조회 후 Team이 EAGER인 것을 확인하면 다시 Team에 대한 Select 쿼리를 발생시킨다. 즉, 하나만 조회했는데 쿼리는 두번 나갔다 해서 이런 것을 "n+1 문제"라고 한다.
결론은 모든 연관관계에 지연로딩 FetchType.LAZY를 쓰자.
'Spring > JPA' 카테고리의 다른 글
JPA- 임베디드 타입 (0) | 2023.03.22 |
---|---|
JPA- Cascade와 고아 객체 (0) | 2023.03.21 |
JPA- 프록시 (0) | 2023.03.16 |
JPA- @MappedSuperClass (0) | 2023.03.12 |
JPA- 상속관계 매핑 (0) | 2023.03.12 |