일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Greedy
- spring
- kotlin
- Servlet
- Spring Boot
- Proxy
- Exception
- AOP
- transaction
- Android
- JDBC
- 스프링 핵심 기능
- SpringBoot
- 인프런
- java
- db
- QueryDSL
- pointcut
- 백준
- 알고리즘
- Thymeleaf
- jpa
- 스프링
- springdatajpa
- JPQL
- 스프링 핵심 원리
- 그리디
- 김영한
- 자바
- http
- Today
- Total
개발자되기 프로젝트
JPA Fetch type, N + 1 이슈 본문
1. Fetch Type이란?
Entity 를 조회할 경우 해당 Entity와 연관관계에 있는 Entity를 어떻게 가져올 것인지 설정하는 값이다.
- 연관 관계에 있는 Entity 를 모두 가져온다 → Eager
- 연관 관계에 있는 Entity 를 getter 로 접근할 때 가져온다 → Lazy
2. Lazy fetch
Lazy Fetch는 연관관계에 있는 Entity를 바로 가져오는 것이 아니라 연관관계의 entity를 getter로 호출할 때만 가져온다.
User class를 보자. user와 userHIstory는 1:N관계이다.
User를 호출할 때 마다 UserHistory도 항상 불러와야 할까?? 항상 필요하지도 않은데..?
UserHistory가 필요한 시점에만 쿼리를 실행하면 되지 않을까?
그렇다면 언제 UserHistory가 필요하다고 판단할까?
getter를 통해 UserHistory를 불러오는 경우에 JPA에서 UserHistory가 필요하다고 판단이 가능.
ex> user.getUserHistoris()
하지만 lazy fetch는 항상 가능한 것이 아니라,
session(영속성 컨텍스트가 entity관리하는 상태, @Transactional)이 열려있을 경우만 가능하다.
2-1. 주의 사항
예를들어, Review 객체에 id, title, scroe, content, book, user 등의 filed가 있다고 하자.
review 객체에 대해 ToString을 실행하면 각 filed를 모두 조회한다.
이 때 연관관계에 있는 entity까지 모두 조회하는 query가 실행된다.
이처럼 Lazy fetch를 적용하더라도, 불필요한 query가 실행될 수 있다.
이러한 상황을 막기위해 ToString시 필요가 없는 entity에 대해서는 @ToString.Exclude를 붙여주자.
Lazy fetch의 장점을 최대한 활용하기 위함이다.
@ManyToOne
@ToString.Exclude
private Book book;
@ManyToOne
@ToString.Exclude
private User user;
추가로 @ToString.Exclude는 연관관계를 가진 entity들에 대해 ToString시 순환참조를 막기위한 방법으로 사용된다.
3. Eager fetch
Eager fetch는 연관관계의 entity를 바로바로 호출하는 것이다.
예를들어 User와 UserHistory는 1:N 연관관계이다.
Lazy type은 userHistory를 user에서 getter를 통해 호출될 때 만 userHistory 를 호출하지만.
Eager type은 항상 user호출 시 userHistory도 같이 호출된다.
4. N + 1 Issue
A와 B가 1 : N 관계라고 하자.
1개의 A entity를 조회할 경우 A에 연관된 n개의 B entity가 독릭접인 쿼리를 통해 조회되는 현상이다.
즉, 예상하기로는 A를 조회하는 1개의 쿼리로 B까지 오겠지...??
하지만 현실을 n개의 B entity를 별도로 조회하는 쿼리가 돌아간다..
5. N+1 issue 만들기..?
강제로 N+1 issue 상황을 만들어보자.
<data.sql>
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values (1, 'JPA', '오우야', 5.0, 1, 1);
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values (2, '백엔드', 'ㄷㄷㄷ', 1.0, 2, 2);
<Comment class> : comment ~ review 연관관계
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String comment;
@ManyToOne
private Review review;
}
<Review class> : comment ~ review 연관관계
@OneToMany
@JoinColumn(name = "review_id")
private List<Comment> comments;
1:N 연관관계는 아래 글 참고
<Test>
@Test
void reviewTest(){
List<Review> reviews = reviewRepository.findAll();
System.out.println(reviews);
}
에러가 발생한다.
could not initialize proxy - no Session
no Session이기 때문에 Test code에 @Transactional을 붙여주자.
@Test
@Transactional
void reviewTest(){
List<Review> reviews = reviewRepository.findAll();
System.out.println(reviews);
}
참고로 연관관계에 따라 기본 fetch 설정이 다르다.
@ManyToOne | EAGER |
@OneToMany | LAZY |
@OneToOne | EAGER |
따라서 Review class에서 @ManyToOne에 fetch type을 lazy로 변경해주자.
추가로 @OneToMany인 comments에 대해서는 EAGER로 변경.
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;
이제 review를 조회할 때 Book과 User를 호출하는 query가 동작하지 않을 것이다.
Review를 조회하고 review id에 해당하는 comments를 조회했다.
짠! Review만 쿼리돌아가겠지 했는데, n개의 comment 쿼리까지 돌아갔따. N+1 상황이다.
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
[Review(super=BaseEntity(createdAt=2021-07-11T14:27:48.128183, updatedAt=2021-07-11T14:27:48.128183), id=1, title=JPA, content=오우야, score=5.0, comments=[]), Review(super=BaseEntity(createdAt=2021-07-11T14:27:48.130341, updatedAt=2021-07-11T14:27:48.130341), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[])]
N+1 issue 준비가 끝났다.
그리고 EAGER와 LAZY는 쿼리 시점의 차이이지 N+1 쿼리 횟수에 대한 영향을 끼치지 않음.
6. EAGER, LAZY 차이점
lazy type은 필요한 시점에 쿼리가 실행한다.
EAGER 대비 쿼리 성능에 관련된 이점을 얻을 수 있다.
Fetch Type은 쿼리 시점에 대한 차이이지 N+1에대한 해법은 아니다.
<Review class> fetch type 확인!
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;
<Test>
@Test
@Transactional
void reviewTest(){
List<Review> reviews = reviewRepository.findAll();
//System.out.println(reviews);
System.out.println("전체를 가져왔다");
System.out.println(reviews.get(0).getComments());
System.out.println("첫 번째 리뷰의 코멘트들을 가져옴");
System.out.println(reviews.get(1).getComments());
System.out.println("두 번째 리뷰의 코멘트들을 가져옴");
}
<결과>
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
전체를 가져왔다
[]
첫 번째 리뷰의 코멘트들을 가져옴
[]
두 번째 리뷰의 코멘트들을 가져옴
먼저 review 조회하는 query가 실행되었따.
그리고 comments를 가져오는 쿼리는 별도로 실행되지 않고 캐시에서 불러온 것을 알 수 있다.
(왜 비었지..??)
<Review class> : comment의 fetch타입을 lazy로 변경
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private List<Comment> comments;
<결과> getComments()시점에 comment 조회하는 query가 실행되었다.
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
전체를 가져왔다
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
첫 번째 리뷰의 코멘트들을 가져옴
Hibernate:
select
comments0_.review_id as review_i3_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.comment as comment2_4_1_,
comments0_.review_id as review_i3_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
두 번째 리뷰의 코멘트들을 가져옴
즉, lazy type은 필요한 시점에 쿼리가 실행한다.
EAGER 대비 쿼리 성능에 관련된 이점을 얻을 수 있다.
7. N+1 Issue 해결
1) @Query를 통해 fetch join 쿼리를 custom으로 만듦.
<BookRepository>
public interface ReviewRepository extends JpaRepository<Review, Long> {
@Query("select distinct r from Review r join fetch r.comments")
List<Review> findAllByFetchJoin();
}
inner join이 들어가서 comments까지 같이 조회가 가능하다.
즉 한 번의 쿼리로 끝!
Hibernate:
select
review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
left outer join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.833729, updatedAt=2021-07-11T16:06:12.833729), id=1, title=JPA, content=오우야, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.836600, updatedAt=2021-07-11T16:06:12.836600), id=1, comment=킹치만..), Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.839291, updatedAt=2021-07-11T16:06:12.839291), id=2, comment=오히려좋아)])
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.835227, updatedAt=2021-07-11T16:06:12.835227), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.840736, updatedAt=2021-07-11T16:06:12.840736), id=3, comment=오히려안좋아)])
2) @EntityGraph
@EntityGraph(attributePaths = "comments")
@Query("select r from Review r")
List<Review> findAllByEntityGraph();
Hibernate:
select
review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
left outer join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.833729, updatedAt=2021-07-11T16:06:12.833729), id=1, title=JPA, content=오우야, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.836600, updatedAt=2021-07-11T16:06:12.836600), id=1, comment=킹치만..), Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.839291, updatedAt=2021-07-11T16:06:12.839291), id=2, comment=오히려좋아)])
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.835227, updatedAt=2021-07-11T16:06:12.835227), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.840736, updatedAt=2021-07-11T16:06:12.840736), id=3, comment=오히려안좋아)])
8. GitHub : 210711 fetch type, N+1
'JPA > Trouble shooting' 카테고리의 다른 글
Dirtycheck, 성능이슈 (0) | 2021.07.12 |
---|---|
영속성 컨텍스트로 인해 발생하는 issue (0) | 2021.07.11 |