https://www.youtube.com/watch?v=u69GNLILgzM
목차
- 1. N+1 문제란?
- 2. 2차 프로젝트 예시
- 3. 문제가 발생하게 된 원인
- 4. 해결책
JPA에서 N+1 문제란?
- 데이터를 조회할때, 요청이 1개의 쿼리로처리되길 기대했는데 N개의 추가 쿼리가 발생하는 현상이다
- 1+N 이라고 생각해도 된다.
- 다양한 연관관계(@OneToMany, @ManyToOne, @OneToOne 등) 에서 발생한다.
프로젝트 예시
- 회원(users) 테이블
- 게시글(post) 테이블
- 시리즈(Series) 테이블
- User(1) : Series(N)
- User(1) : Post(N)
- Series (N) : Post (N)
User.class
class User{
@Id
private Long id;
@OneToMany(mappedBy = "user")
private final List<Series> series = new ArrayList<>();
@OneToMany(mappedBy = "user")
private final List<Post> posts = new ArrayList<>();
}
Series.class
class Series{
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "series")
private final List<Post> posts = new ArrayList<>();
}
Post.class
class Post{
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private final User user;
@ManyToOne
@JoinColumn(name = "series_id")
private final Series series;
}
Velog의 시리즈 목록보기
요구사항
- 해당 유저가 가진 시리즈의 목록
- 시리즈 제목
- 시리즈에 속한 게시글 개수
SeriesRepository.class
List<Series> findByUserId(Long userId);
SeriesService.class
public List<SeriesInfo> getMySeriesList(Long userId){
//유저 아이디로 시리즈를 조회한다.
List<Series> foundSeries = seriesRepository.findByUserId(userId);
List<SeriesInfo> seriesInfoList = new ArrayList<>(); //응답하기 위한 객체
for(Series series : foundSeries){ //조회한 시리즈의 제목과 속한 게시글의 개수 추출
seriesInfoList.add(
new SeriesInfo(series.getTitle(), series.getPosts().size())
)
}
return seriesInfoList;
}
- 결과는 시리즈를 가져왔는데 게시글을 한번 더 조회한다.
- JPA N+1 문제가 발생했다.
- @OneToMany 기본 전략이 LAZY 이기에 추가적인 쿼리릍 통해 가져오고있다.
- 지연로딩(LAZY)이란 ? 특정 엔티티를 조회할때 필요한 시점에 연관된 객체의 데이터를 불러오는 전략이다.
- 처음 해결방안으로는 fetchType.Eager로 즉시 로딩을 사용하여 특정 엔티티를 조회할떄 한번에 연관된 엔티티를 가져오는것이다.
class Series{
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "series", fetch = FetchType.EAGER)
private final List<Post> posts = new ArrayList<>();
}
- 위처럼 사용하므로써 불필요한 쿼리를 사용한다.
- JPA N+1 문제는 Fetch 전략이 원인이 아니다.
- JPA는 JPQL을 통해서 SQL문을 생성해서 객체를 조회한다.
- JPQL(Java PErsistence Query Language)
- DB테이블이 아니라 엔티티의 객체를 대상으로 검색하는 객체 지향 쿼리다.
- 즉시 로딩(EAGER)인 경우
- 1. JPQL에서 만든 SQL을 통해 데이터(단일객체)를 조회
- 2. JPA에서 Fetch 전략 확인. 즉시 로딩이기 떄문에 연관 엔티티를 추가조회한다.
- 3. 2번과정으로 인해 N+1 문제 발생한다.
- 지연로딩(LAZY)인 경우
- 1. JPQL에서 만든 SQL을 통해 데이터(단일객체)를 조회
- 2. JPA에서 Fetch 전략을 확인. 지연로딩이기 떄문에 추가조회 X
- 3. 하지만, 하위엔티티 사용하면 추가조회. 결국 N+1 문제가 발생한다.
해결방안
- fetch Join
- EntityGraph
- @BatchSize
- Fetch Join
- JOIN FETCH 문을 통해서 같이 조회할 객체를 직접 지정하는 방식이다.
- 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화한다.
- Inner JOIN 방식이다.
- 참고 : 일반 JOIN은 적용되지 앟는다.
- 아래와 같이 변경하여 join fetch 를 통해 해결할 수 있다. Querydsl 로도 가능하다.
@Query("SELECT s FROM Series s JOIN FETCH s.posts WHERE s.user.id = :userId")
List<Series> joinPostFindByUserId(@Param(value="userId") Long userId);
- @EntityGraph
- Outer Join 방식
- 중복데이터 발생
@EntityGraph(attributes = {"posts"}, type= EntityGraphType.FETCH)
List<Series> findByUserId(Long userId);
fetch Join & @EntityGraph 단점
- Pagination 구현시 limit이 적용되지 않는다.
- 데이터베이스에서 FULL Scan한 이후 모든 데이터를 메모리에 올린 이후 Limit에 맞게 데이터를 만든다.
- 두개 이상의 Collection에 적용할 수 없다.
- MultipleBagFetchException 예외 발생
@BatchSize
- 여러개의 엔티티를 조회할때 지정된 Size만큼 WHERE 절이 같은 여러개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어준다.
class Series{
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@BatchSize(size = 10)
@OneToMany(mappedBy = "series")
private final List<Post> posts = new ArrayList<>();
}
더 공부하면 좋을주제들
- 왜 fetch join과 pageable 과 같이 사용했을때 Limit이 적용되지 않을가?
- 데이터베이스 구조와 Batch의 구조가 다르다.
- 두개 이상의 Collection fetch join과 MultipleBagFetchException
- JPA에서 DTO 직접조회
- @Fetch
'무언가에 대한 리뷰 > 테크영상리뷰' 카테고리의 다른 글
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 🐤 샐리의 트랜잭션 (0) | 2023.12.04 |
---|---|
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 우르의 Lock & JPA Lock (0) | 2023.10.12 |
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 잉, 페퍼의Spring Data JPA 삽질일지 (0) | 2023.10.12 |
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 무민의 JVM Stack & Heap (0) | 2023.10.11 |
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 멍토의 Blocking vs Non-Blocking, Sync vs Async (0) | 2023.10.11 |