JPA(Java Persistence API)는 엔티티를 조회할 때 즉시 로딩(EAGER)과 지연 로딩(LAZY) 2가지 방식이 있습니다. 즉시 로딩은 엔티티를 조회할 때 연관된 엔티티까지 한 번에 가져오며, 지연 로딩은 엔티티 조회 시 연관된 엔티티를 가져오지 않고, 필요한 시점에 가져오는 방식입니다.
FetchType이란?
JPA가 엔티티를 조회할 때, 해당 엔티티와 연관관계를 맺은 엔티티를 어떻게 가져올지 설정하기 위함입니다. JPA는 사용자가 직접 쿼리를 생성하지 않아도 엔티티의 필드를 보고 상황에 맞게 쿼리를 생성합니다. 따라서 하나의 엔티티에 연관관계를 맺은 엔티티 매핑되어 있다면 같이 조회할 것인지 따로 조회할 것인지 선택할 수 있습니다.
따로 설정하지 않을 경우 default 값은 다음과 같습니다.
- @XxxToOne -> fetch = FetchType.EAGER
- @XxxToMany -> fetch = FetchType.LAZY
즉시 로딩(EAGER)
엔티티를 조회할 때 연관된 엔티티가 즉시 로딩으로 설정되어 있다면 같이 조회됩니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.EAGER) // Member를 조회할 때 Team 같이 조회한다.
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
}
JPQL를 통해 Member 엔티티를 조회합니다.
Member findMember = em.createQuery("select m from Member m", Member.class)
.getSingleResult();
Hibernate:
select
member0_.member_id as member_i1_0_0_,
member0_.username as username2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.team_id as team_id1_1_1_
from
Member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
즉시 로딩은 Member 엔티티를 조회하는 시점부터 Team 엔티티까지 함께 조회하는 쿼리를 보내게 됩니다.
🤔 즉시 로딩, 외부 조인(left outer join)이 발생되는 이유?
즉시 로딩을 사용할 때, 실행되는 sql은 내부 조인(inner join) 또는 외부 조인(left outer join)으로 실행됩니다. 이러한 이유는 연관된 엔티티를 즉시 로딩할 때, 하나의 쿼리로 모든 정보를 가져오기 위해서입니다.
외래 키에 null이 허용될 경우 내부 조인은 연관된 엔티티가 null이면 연관된 엔티티와 해당 엔티티를 조회할 수 없게 됩니다. 그러나 외부 조인보다 내부 조인이 성능에 유리합니다.
외래 키가 not null 제약 조건을 가지고 있다면 값이 항상 있다는 것을 보장하기 때문에 이러한 경우 내부 조인을 사용하는 것이 좋습니다. JPA는 이러한 설정을 @JoinColumn으로 nullable = false를 설정하여 not null 제약 조건을 걸 수 있습니다. 또한 @ManyToOne(optinal = false)를 사용하는 방법도 있습니다.
지연 로딩(LAZY)
연관된 엔티티를 프록시 객체로 생성 후, 사용될 때 실제 엔티티를 조회하게 됩니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY) // Member를 조회할 때 Team은 조회되지 않는다.
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
}
JPQL을 통해 Member 엔티티를 조회합니다.
Member findMember = em.createQuery("select m from Member m", Member.class)
.getSingleResult();
Hibernate:
select
member0_.member_id as member_i1_0_0_,
member0_.username as username2_0_0_,
member0_.team_id as team_id3_0_0_
from
Member member0_
where
member0_.member_id=?
지연 로딩은 Member 엔티티를 조회할 때 Team 엔티티를 조회하지 않으며, 프록시 객체가 대신하여 Team 필드에 담아지게 됩니다. 이후 Team 엔티티를 사용하는 시점에 Team 엔티티를 조회하는 쿼리가 발생합니다.
Team 엔티티를 사용해봅시다.
Team findTeam = findMember.getTeam();
select
team0_.id as id1_3_0_
from
Team team0_
where
team0_.id=?
위와 같이 Team 엔티티를 사용할 때 Team을 조회하는 쿼리가 발생하게 됩니다.
프록시와 즉시 로딩 주의사항
실무에서는 성능 최적화를 위해 가급적이면 지연 로딩으로 설정해야 합니다. 즉시 로딩은 예상치 못한 sql이 발생될 우려가 있으며, 불필요한 자원이 소모될 수 있습니다.
또한 JPQL을 사용할 때 N + 1 문제를 일으킬 수 있습니다. 예를 들어 'em.createQuery("select m from Member m", Member.class)'와 같은 쿼리를 실행할 때, Member 엔티티와 즉시 로딩으로 연관된 엔티티의 개수만큼 쿼리가 추가로 발생할 수 있습니다.
따라서 기본 값을 지연 로딩으로 설정하여, 필요한 경우 fetch join을 통해 연관된 엔티티를 한 번에 가져오는 것이 좋습니다. 이 외 다른 방법으로 엔티티 그래프, 애노테이션, 배치 사이즈 등을 사용할 수 있습니다.
주의할 점은 @XxxToOne는 기본적으로 즉시 로딩이기 때문에 지연 로딩으로 변경해야 합니다. 반면에 @XxxToMany는 기본적으로 지연 로딩으로 적용되어 있어 기본 값으로 사용하면 됩니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런
김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도
www.inflearn.com
'Spring > JPA' 카테고리의 다른 글
N+1 문제 (0) | 2024.04.28 |
---|---|
영속성 전이(CASCADE)와 고아 객체(ORPHAN) (0) | 2024.04.28 |
프록시(Proxy) (0) | 2024.04.28 |
상속관계 매핑 (0) | 2024.04.24 |
연관관계 매핑 정리 (0) | 2024.04.23 |