Post
연관관계
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Post extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = " post_id")
private Long id;
@Column(name = "content", nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="member_id")
private Member member;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Picture> pictures = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Likes> likes = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTag> postTags = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostComment> postComments = new ArrayList<>();
Post
Entity는 화면과 같이다수의 연관 관계가 설정된 자식 엔티티들이 존재하는데 , Picture, Like, Comment
와 OneToMany
연관관계를, Tag
는 ManyToMany
연관관계를 형성하고있다.
- 부모인
Member
Entity 와는 ManyToOne
연관 관계를 맺고있다.
N + 1 문제
- 이번 Main Project에서는 Pre-Project에서 만났던 N+1과 같은 DB와 관련된 문제를 해결하고, 성능을 최적화 하기 위해 노력을 기울였다.
- 게시물 정보를 불러오기 위해 Post 엔티티를 조회할 때, 연관된 자식 엔티티도 함께 조회할 경우 N+1 문제가 발생한다. 그렇다면, 지금부터 N+1문제가 무엇인지, 그리고 어떻게 해결하는지 알아보자.
N+1 문제란?
- 쿼리 1번으로 N건의 부모 엔티티를 조회했을 때, 조회된 부모의 수 만큼 자식 테이블 쿼리가 N번 추가 발생하는 현상을 말한다.
Why?
Fetch Type
Eager
- JPQL과 함께 사용시 N+1 문제 발생
- JPA가 JPQL로 SQL을 생성할 때는 글로벌 Fetch 전략을 무시하고 JPQL만 가지고 생성하기 때문에 find() 메소드로 조회할 때와 달리 Join을 통해서 연관관계를 가져오지 않기 때문에 N+1 문제 발생
- 사용하지 않는 데이터도 매번 불러오게 된다
Lazy
- 전체 데이터 가져온 이후 연관 관계인 하위 엔티티를 사용할때 다시 조회할때 발생
How?
Fetch Join
- 특정 엔티티를 조회할 때 연관된 엔티티 혹은 컬렉션을 함께 한번에 조회하는 기능
Batch Size
- 지정한 수만큼 in절에 부모 Key를 사용하게 해주는 옵션
해결과정
문제 1. MulitpleBagFetchException
- Post 엔티티를 조회할 때, N +1 문제를 해결하기 위해
OneToMany
관계인 엔티티 여러곳에 Fetch Join
을 적용 → MulitpleBagFetchException
에러 발생
원인
Fetch Join의 조건
- ToOne 관계는 몇개든 사용 가능
- ToMany 관계는 1개만 사용 가능
해결 1.
OneToMany
관계의 자식 엔티티들중 가장 데이터가 많은 자식쪽에 Fetch Join
을 사용하고, ToOne
관계의 자식엔티티들은 모두 Fetch Join
을 적용하여 한방 쿼리 수행
- 그렇다면 나머지
OneToMany
관계의 자식엔티티들은 FetchJoin
없이 어떻게 N+1
문제를 해결할 수있을까?
BatchSize
를 적용하여, 부모 엔티티의 Key
가 1개씩 사용 되던 조건문을 in절
로 묶어서 한번에 조회 함으로써 N+1
문제를 해결
- BatchSize 적용과정
- 부모 엔티티가 지연로딩된 ToMany 관계의 자식 엔티티(컬렉션)을 최초 조회 할때 이미 조회하여 영속성 컨텍스트에서 관리되고 있는 부모 엔티티들의 ID를 모아서 Where 조건문에
WHERE Y.X_ID IN (?, ?, ?...)
와 같은 SQL IN 구문에 담아 자식엔티티 조회 쿼리를 날려 필요한 자식엔티티 데이터들을 한번에 조회
- 위에서 설명한 방법을 적용하였더니,
MultipleBagFetchException
에러는 발생하지 않았으나, 페이징할때 사용하던 기존의 SQL LIMIT
구문이 등장하지 않음
- 쿼리결과를 전부
메모리
에 적재한 뒤 어플리케이션 단에서 Pagination
작업을 수행한다는 경고 로그가 발생
원인
OneToMany
관계를 Join
하면 카타시안 곱
이 적용되어 DB Row수가 뻥튀기되어 조회되기 때문에 Limit
구문을 사용하는 쿼리로 페이지네이션을 적용하기 어려워 조회된 결과를 모두 메모리
로 가져와 JPA가 페이지네이션
계산을 진행
OneToMany
관계의 컬렉션을 Fetch Join
하면서, 동시에 Pagination API
를 사용하면 OutOfMemoryError
가 발생할 수 있어, 이 둘을 동시에 사용해서는 안된다.
- 반대로
ManyToOne
관계의 엔티티를 Fetch Join
할때는 DB Row의 수가 변경되지 않아 문제없이 Pagination
이 적용가능
ToMany
관계의 자식엔티티들은 FetchJoin없이
어떻게 N+1
문제를 해결할 수있을까?
해결 2.
- BatchSize를 적용
- Batch Size를 적용한 결과
where comments0_.post_id in (?, ?, ?)
를 포함하는 Comment 조회 쿼리 1개로 줄어들었다.
- 최종적으로
ToOne
관계인 자식 엔티티는 FetchJoin
을 , ToMany
관계인 자식엔티티는 BatchSize
를 적용하여 N+1
과 문제와 Pagination
관련 문제를 해결