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 문제가 발생한다.

해결방안

  1. fetch Join
  2. EntityGraph
  3. @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

 

+ Recent posts