지연로딩(Lazy)
JPA는 지연로딩 전략을 지원한다.
@ManyToOne(fetch = FetchType.LAZY)
위와 같이 @XxxTOXxx 어노테이션의 fetch옵션을 통해 로딩 전략을 설정할 수 있으며 Eager, Lazy 두개의 전략을 설정할 수 있다.
- Eager : 즉시로딩
연관관계 엔티티를 모두 조회한다.
@ManyToXxxx fetch 옵션 기본값이다 - Lazy : 지연로딩
현재 조회하려는 엔티티만을 조회하고, 연관관계 엔터티는 객체탐색으로 탐색하는 순간 조회된다.
@XxxxToMany fetch 옵션 기본값이다
[User Entity]
@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
[테스트 코드]
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
Team team = new Team();
team.setTeamname("TeamA");
em.persist(team);
User user = new User();
user.setUsername("hello");
user.setTeam(team);
em.persist(user);
em.flush();
em.clear();
User findUser = em.find(User.class, 1L);
}
}
[테스트 결과] 단순히 User만 조회했을 뿐인데 JOIN을 통해 연관관계 엔터티 Team까지 조회한다.
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
team1_.team_id as team_id1_8_1_,
team1_.teamname as teamname2_8_1_
from
users user0_
left outer join
team team1_
on user0_.team_id=team1_.team_id
where
user0_.user_id=1
[User Entity] Lazy로딩 전략 설정
@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY) // Lazy 로딩 전략
@JoinColumn(name = "TEAM_ID")
private Team team;
}
[테스트 결과] 연관관계 테이블에 대한 Join이 발생하지 않고 User만 조회해온다.
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
from
users user0_
where
user0_.user_id=1
이렇게 되면 User 엔티티에는 Team에 대한 객체가 존재하기 때문에 User를 조회해 오면서 Team객체는 값이 초기화되어 있지 않은 Proxy객체로 채워지게 된다.
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
Team team = new Team();
team.setTeamname("TeamA");
em.persist(team);
User user = new User();
user.setUsername("hello");
user.setTeam(team);
em.persist(user);
User findUser = em.find(User.class, 1L);
System.out.println(findUser.getTeam().getClass()) // 프록시 객체인지 출력
}
}
insert
into
team
(teamname, team_id)
values
('TeamA', 1)
insert
into
users
(team_id, username, user_id)
values
(1, 'hello', 2)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
from
users user0_
where
user0_.user_id=2
Team = class com.example.demo.domain.Team$HibernateProxy$DBd05rYv
위와 같이 User엔터티에 대한 쿼리만 발생하고 Team은 프록시 객체를 반환한다.
Team엔터티에 대한 실제 쿼리를 출력하기 위해 아래과 같이 테스트 코드를 작성해보자.
@SpringBootTest
@Transactional
@Commit
class DemoApplicationTests {
@PersistenceContext
EntityManager em;
@Test
void Proxy() {
Team team = new Team();
team.setTeamname("TeamA");
em.persist(team);
User user = new User();
user.setUsername("hello");
user.setTeam(team);
em.persist(user);
User findUser = em.find(User.class, 1L);
System.out.println(findUser.getTeam().getClass()) // 프록시 객체인지 출력
System.out.println("=========================================");
findUser.getTeam().getTeamname(); // Team Select 쿼리 발생(Team 프록시 초기화)
System.out.println("=========================================");
}
}
[결과 출력]
insert
into
team
(teamname, team_id)
values
('TeamA', 1)
insert
into
users
(team_id, username, user_id)
values
(1, 'hello', 2)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
from
users user0_
where
user0_.user_id=2
Team = class com.example.demo.domain.Team$HibernateProxy$DBd05rYv
=========================================
select
team0_.team_id as team_id1_8_0_,
team0_.teamname as teamname2_8_0_
from
team team0_
where
team0_.team_id=1
=========================================
Team의 속성을 직접적으로 접근하는 시점에 프록시 객체가 채워지면서 실질적으로 쿼리가 발생한다.
지연 로딩일 때 연관관계 엔티티를 앞서 배운 Proxy객체로 가져오게된다!
즉시로딩(Eager)
User 엔터티에 선언한 연관관계 엔터티인 Team엔터티의 로딩전략을 EAGER로 변경해보자.
@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
[테스트 결과] 앞서 기본 전략과 동일하게 JOIN이 걸려서 하나의 쿼리로 조회한다.
insert
into
users
(team_id, username, user_id)
values
(NULL, 'hello', 1)
select
user0_.user_id as user_id1_10_0_,
user0_.team_id as team_id3_10_0_,
user0_.username as username2_10_0_,
team1_.team_id as team_id1_8_1_,
team1_.teamname as teamname2_8_1_
from
users user0_
left outer join
team team1_
on user0_.team_id=team1_.team_id
where
user0_.user_id=1
Team = class com.example.demo.domain.Team
=========================================
=========================================
Team객체는 프록시 객체가 아닌 진짜 Entity객체를 가져오며, User를 조회함과 동시에 JOIN을 통해 함께 반환받기 때문에 프록시 객체로 반환하지 않고 결과적으로 Team 전용 조회 쿼리가 발생하지 않게된다.
주의점
- 가급적 지연 로딩만 사용한다(실무)
- 즉시 로딩(Eager)을 적용하면 예상하지 못한 SQL이 발생하게 된다
(의도치 않게 @ManyToXxx으로 연관관계가 걸려있는 모든 엔터티를 JOIN으로 모든 엔터티가 조회된다) - 즉시 로딩(Eager)은 JPQL에서 N+1 문제를 야기한다.
List<User> users em.createQuery("select u from User u", User.class).getResultList();
위와 같이 JPQL예시로 코드를 작성할 경우에 Eager전략임에도 불구하고 쿼리가 2번 나가게 된다.
em.find()라는것은 PK를 기준으로 내부적으로 최적화해서 가져오지만, JPQL의 경우 SQL로 그대로 번역이 된다.
당연히 User만 쿼리를 날리게 된다. User를 반환한 뒤 User의 Team필드가 Eager 전략인 것을 확인한다.
Eager는 값이 무조건 채워져 있어야 하기 때문에 Team에 대한 쿼리가 별도로 한번 더 날라간다.
- User에 대한 SELECT 쿼리 발생
- User의 연관관계 Team엔터티의 전략이 EAGER 임을 확인.
- Team엔터티에 대한 쿼리 발생
(EAGER전략은 연관관계 엔터티 Proxy객체 값이 무조건 채워져 있어야 하기 때문) - N+1이란 무엇인가?
만약 User가 두명이고 각각은 서로 다른 Team을 가지고 있다고 가정해본다. [userA : teamA / userB : teamB]
위와 같은 상황에서 모든 user를 List로 가져오게 되면 User 조회쿼리 1번과 TeamA, TeamB 조회쿼리 2개를 가져온다.
Team 데이터가 만약 10개가 존재하게 된다면 10 + 1 현상이 발생하게 되는것이다.
여기서 말하는 1은 처음 리스트로 조회해 오는 User에 대한 갯수이고, 나머지 10 은 User각각이 가지고 있어야 할 Team에 대한 쿼리 조회수 이다. - N+1 해결방안 : LAZY 로딩 정책을 기본으로 가져간다.
- fetch join : Query에 join 뒤에 fetch를 선언한다. (join fetch)
- @EntityGraph : @EntityGraph(attributePaths = ("연관엔터티필드명"))
- Batch Size
- @ManyToOne, @OneToOne은 기본이 즉시로딩이다.
(지연 로딩인 Lazy로 설정한다.) - @OneToMany, @ManyToMany는 기본이 지연로딩이다.
'Hibernate > JPA(EntityManager)' 카테고리의 다른 글
[JPA] UPDATE 변경감지와 병합 (DirtyChecking / Merge) (0) | 2023.07.14 |
---|---|
[JPA] 영속성 전이와 고아객체 (CASCADE, orphaRemoval) (0) | 2023.07.13 |
[JPA] JPA에서 Proxy 객체란? (0) | 2023.07.13 |
[JPA] 상속 관계 매핑 @Inheritance , @Discriminator____ (0) | 2023.06.30 |
[JPA] 값 타입 컬렉션 (@ElementCollection / @CollectionTable) (0) | 2023.06.29 |