개요
이번 글에서는 두 개 이상의 OneToMany 관계에서 발생한 이슈와 이를 최적화하는 과정에 대해 작성하려 합니다.
문제 상황
OneToMany 관계의 엔티티들이 있습니다.
엔티티
Movie 엔티티
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Movie extends BaseEntity {
@Id
@Column(name = "movie_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@Enumerated(EnumType.STRING)
private FilmRatings filmRatings;
@OneToMany(mappedBy = "movie")
private List<Gallery> galleries = new ArrayList<>();
@OneToMany(mappedBy = "movie")
private List<MovieCountries> movieCountries = new ArrayList<>();
@OneToMany(mappedBy = "movie")
private List<MoviePersonnel> moviePersonnels = new ArrayList<>();
@OneToMany(mappedBy = "movie")
private List<MovieGenre> movieGenres = new ArrayList<>();
}
MoviePersonnel 엔티티
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MoviePersonnel extends BaseTimeEntity {
@Id
@Column(name = "movie_personnel_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "personnel_id")
private Personnel personnel;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "movie_id")
private Movie movie;
...
}
Personnel 엔티티
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Personnel extends BaseTimeEntity {
@Id
@Column(name = "personnel_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
Movie 엔티티는 4개의 엔티티와 일대다 연관관계에 있고 모두 LAZY Loading입니다.
여기서 galleries를 제외한 다른 3개의 엔티티는 다른 엔티티와 다대일 연관관계에 있습니다.
위에서 예시를 위해 일부만 가져온 MoviePersonnel와 Personnel 간의 관계라고 보시면 됩니다.
그리고 Movie 엔티티와 일대다 관계에 있는 galleries, movieCountries, moviePersonnels, movieGenres을 모두 가져와야 하는 서비스 메서드가 있습니다.
public MovieDetailResponse getMovieDetail(Long movieId, Long userId) {
Movie movie = movieRepository.findMovieDetailInfo(movieId)
.orElseThrow(MovieNotFoundException::new);
...
}
조인 안 하고 값 조회 해보기
우선 JPQL에서 다른 엔티티를 조인하지 않고 Movie 엔티티를 조회해 보겠습니다.
예시 JPQL
@Query("SELECT m FROM Movie m " +
"WHERE m.id = :movieId AND m.isDeleted = false ")
Optional<Movie> findMovieDetailInfo(@Param("movieId") Long movieId);
default_batch_fetch_size: 1000 적용 안 함
select
m1_0.movie_id,
...
m1_0.title
from
movie m1_0
where
m1_0.movie_id=?
and m1_0.is_deleted=0
2024-11-22T00:50:01.808+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_countries_id,
m1_0.countries_id,
m1_0.created_at,
m1_0.last_modified_at
from
movie_countries m1_0
where
m1_0.movie_id=?
2024-11-22T00:50:01.814+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
c1_0.countries_id,
c1_0.created_at,
c1_0.last_modified_at,
c1_0.nation
from
countries c1_0
where
c1_0.countries_id=?
...
select
c1_0.countries_id,
c1_0.created_at,
c1_0.last_modified_at,
c1_0.nation
from
countries c1_0
where
c1_0.countries_id=?
2024-11-22T00:50:01.818+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_personnel_id,
m1_0.character_name,
m1_0.created_at,
m1_0.last_modified_at,
m1_0.movie_position,
m1_0.personnel_id
from
movie_personnel m1_0
where
m1_0.movie_id=?
2024-11-22T00:50:01.820+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
p1_0.personnel_id,
p1_0.created_at,
p1_0.last_modified_at,
p1_0.name,
p1_0.profile_image
from
personnel p1_0
where
p1_0.personnel_id=?
...
select
p1_0.personnel_id,
p1_0.created_at,
p1_0.last_modified_at,
p1_0.name,
p1_0.profile_image
from
personnel p1_0
where
p1_0.personnel_id=?
2024-11-22T00:50:01.830+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.movie_id,
g1_0.gallery_id,
g1_0.created_at,
g1_0.image_url,
g1_0.last_modified_at
from
gallery g1_0
where
g1_0.movie_id=?
2024-11-22T00:50:01.831+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_genre_id,
m1_0.created_at,
m1_0.genre_id,
m1_0.last_modified_at
from
movie_genre m1_0
where
m1_0.movie_id=?
2024-11-22T00:50:01.832+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.genre_id,
g1_0.created_at,
g1_0.genre_type,
g1_0.last_modified_at
from
genre g1_0
where
g1_0.genre_id=?
2024-11-22T00:50:01.833+09:00 DEBUG 39090 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.genre_id,
g1_0.created_at,
g1_0.genre_type,
g1_0.last_modified_at
from
genre g1_0
where
g1_0.genre_id=?
Movie와 연관된 다른 엔티티(movieCountries, galleries 등)는 기본적으로 지연 로딩(Lazy Loading)으로 설정되어 있어 Movie 엔티티를 가져올 때 동시에 다른 엔티티를 가져오지는 않습니다. 다만 애플리케이션 레벨에서 실제 연관된 엔티티의 데이터를 사용하기 위해 접근하면 실제 데이터가 필요하므로 해당 엔티티와 매핑되어 있는 테이블로 추가 select 쿼리가 실행되는 N + 1 문제가 발생합니다.
페치 조인도 안하고 default_batch_fetch_size 옵션도 안한 상태에서 Movie엔티티를 조회하고 애플리케이션 레벨단에서 Movie엔티티와 일대다 관계의 엔티티의 데이터를 사용하면 위와 같이 콘솔에 찍힌 것처럼 N + 1 문제가 발생합니다.
default_batch_fetch_size: 1000 적용
select
m1_0.movie_id,
m1_0.average_rating,
m1_0.created_at,
m1_0.deleted_at,
m1_0.film_ratings,
m1_0.is_deleted,
m1_0.like_counts,
m1_0.last_modified_at,
m1_0.plot,
m1_0.poster_url,
m1_0.release_date,
m1_0.review_counts,
m1_0.runtime,
m1_0.title
from
movie m1_0
where
m1_0.movie_id=?
and m1_0.is_deleted=0
2024-11-22T00:49:11.940+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_countries_id,
m1_0.countries_id,
m1_0.created_at,
m1_0.last_modified_at
from
movie_countries m1_0
where
m1_0.movie_id=?
2024-11-22T00:49:11.947+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
c1_0.countries_id,
c1_0.created_at,
c1_0.last_modified_at,
c1_0.nation
from
countries c1_0
where
c1_0.countries_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
2024-11-22T00:49:11.951+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_personnel_id,
m1_0.character_name,
m1_0.created_at,
m1_0.last_modified_at,
m1_0.movie_position,
m1_0.personnel_id
from
movie_personnel m1_0
where
m1_0.movie_id=?
2024-11-22T00:49:11.954+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
p1_0.personnel_id,
p1_0.created_at,
p1_0.last_modified_at,
p1_0.name,
p1_0.profile_image
from
personnel p1_0
where
p1_0.personnel_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
2024-11-22T00:49:11.959+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.movie_id,
g1_0.gallery_id,
g1_0.created_at,
g1_0.image_url,
g1_0.last_modified_at
from
gallery g1_0
where
g1_0.movie_id=?
2024-11-22T00:49:11.960+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_genre_id,
m1_0.created_at,
m1_0.genre_id,
m1_0.last_modified_at
from
movie_genre m1_0
where
m1_0.movie_id=?
2024-11-22T00:49:11.962+09:00 DEBUG 39067 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.genre_id,
g1_0.created_at,
g1_0.genre_type,
g1_0.last_modified_at
from
genre g1_0
where
g1_0.genre_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
이번에는 default_batch_fetch_size: 1000을 설정하고 쿼리를 실행해 보았습니다. 실행 결과, WHERE IN 절을 사용해 데이터를 조회하면서 기존에 N번 실행되던 쿼리가 1번으로 줄어든 것을 확인할 수 있었습니다. 실제 실행된 쿼리를 보면 IN 연산이 추가되어 반복 실행되던 쿼리가 단일 쿼리로 최적화됩니다.
여기서 Movie와 일대다 관계인 엔티티들에 대해서는 IN 연산이 추가되지 않은것에 의문이 생길 수 있습니다.
그 이유는 현재 조회된 Movie 엔티티가 1개뿐이기 때문입니다.
예를 들어 위 상황에서 Movie와 일대다 관계의 엔티티 데이터를 가져오기 위해 추가된 SQL문은 아래와 같습니다.
아래와 같이 하나의 SQL만 발생해서 IN 연산을 사용할 필요가 없어 Movie엔티티와 일대다 관계에 있는 엔티티를 가져오기 위해 IN 연산이 추가 되지 않았습니다. 물론 조회된 Movie 엔티티가 1개가 아닌 복수개라면 일대다 관계에 있는 엔티티들에도 IN 연산이 추가 됩니다.
SELECT * FROM movie_countries WHERE movie_id = ?;
SELECT * FROM gallery WHERE movie_id = ?;
SELECT * FROM movie_genre WHERE movie_id = ?;
SELECT * FROM movie_personnel WHERE movie_id = ?;
N + 1 문제 해결하기 - MultipleBagFetchException 발생
이러한 N + 1 문제를 해결하기 위해서 페치 조인을 사용하거나 default_batch_fetch_size 옵션을 주로 사용합니다.
그렇다면 이번에 페치 조인을 사용해보겠습니다.
아래와 같이 JPQL문을 작성했고 실행 했습니다.
@Query("SELECT m FROM Movie m " +
"LEFT JOIN FETCH m.movieCountries mc " +
"JOIN FETCH mc.countries c " +
"LEFT JOIN FETCH m.moviePersonnels mp " +
"JOIN FETCH mp.personnel p " +
"LEFT JOIN FETCH m.movieGenres mg " +
"JOIN FETCH mg.genre genre " +
"LEFT JOIN FETCH m.galleries g " +
"WHERE m.id = :movieId AND m.isDeleted = false ")
Optional<Movie> findMovieDetailInfo(@Param("movieId") Long movieId);
하지만 이렇게 1:N 관계의 자식 테이블 여러곳에 Fetch Join을 사용하면 아래와 같이 에러가 발생합니다.
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.ureca.filmeet.domain.movie.entity.Movie.galleries, com.ureca.filmeet.domain.movie.entity.Movie.movieCountries]
이 문제는 2개 이상의 OneToMany 자식 테이블에 Fetch Join을 선언했을때 발생합니다.
JPA에서 Fetch Join의 조건은 다음과 같습니다.
- ToOne은 몇개든 사용 가능합니다
- ToMany는 1개만 가능합니다.
어떻게 하면 MultipleBagFetchException 에러 없이 N+1 문제를 최대한 회피할 수 있을까요?
두 개 이상의 OneToMany 관계에서 N+1 문제 해결하기
해결책 Hibernate default_batch_fetch_size
해결책은 하이버네이트의 default_batch_fetch_size 옵션에 있습니다.
다시 한번 JPA의 N+1 문제를 바라보겠습니다.
N+1 문제란 결국 부모 엔티티와 연관 관계가 있는 자식 엔티티들의 조회 쿼리가 문제입니다.
부모 엔티티의 Key 하나하나를 자식 엔티티 조회로 사용하기 때문인데요.
1개씩 사용되는 조건문을 in절로 묶어서 조회하면 어떨까요?
바로 이 개념으로 사용되는 것이 바로 hibernate.default_batch_fetch_size 옵션입니다.
해당 옵션은 지정된 수만큼 in절에 부모 Key를 사용하게 해줍니다.
즉, 1000개를 옵션값으로 지정하면 1000개 단위로 in절에 부모 Key가 넘어가서 자식 엔티티들이 조회되는 것이죠.
단순하게 생각해도 쿼리 수행수가 1/1000이 됩니다.
바로 코드를 만들고 콘솔에 찍히는 쿼리로 확인해보겠습니다.
우선 default_batch_fetch_size 옵션을 추가했습니다.
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 1000
@Query("SELECT m FROM Movie m " +
"LEFT JOIN FETCH m.moviePersonnels mp " +
"JOIN FETCH mp.personnel p " +
"WHERE m.id = :movieId AND m.isDeleted = false ")
Optional<Movie> findMovieDetailInfo(@Param("movieId") Long movieId);
select
m1_0.movie_id,
...
m1_0.title
from
movie m1_0
left join
movie_personnel m2_0
on m1_0.movie_id=m2_0.movie_id
join
personnel p1_0
on p1_0.personnel_id=m2_0.personnel_id
where
m1_0.movie_id=?
and m1_0.is_deleted=0
2024-11-22T02:43:40.875+09:00 DEBUG 40255 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_countries_id,
m1_0.countries_id,
m1_0.created_at,
m1_0.last_modified_at
from
movie_countries m1_0
where
m1_0.movie_id=?
2024-11-22T02:43:40.882+09:00 DEBUG 40255 --- [nio-8080-exec-2] org.hibernate.SQL :
select
c1_0.countries_id,
c1_0.created_at,
c1_0.last_modified_at,
c1_0.nation
from
countries c1_0
where
c1_0.countries_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
2024-11-22T02:43:40.888+09:00 DEBUG 40255 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.movie_id,
g1_0.gallery_id,
g1_0.created_at,
g1_0.image_url,
g1_0.last_modified_at
from
gallery g1_0
where
g1_0.movie_id=?
2024-11-22T02:43:40.890+09:00 DEBUG 40255 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_id,
m1_0.movie_genre_id,
m1_0.created_at,
m1_0.genre_id,
m1_0.last_modified_at
from
movie_genre m1_0
where
m1_0.movie_id=?
2024-11-22T02:43:40.891+09:00 DEBUG 40255 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.genre_id,
g1_0.created_at,
g1_0.genre_type,
g1_0.last_modified_at
from
genre g1_0
where
g1_0.genre_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
발생 쿼리 개수
보시는것처럼 그동안 where xxx_id = ? 였던 조회 쿼리가 where xxx_id in (?, ?, ..) 으로 변경 된 것을 볼 수 있습니다.
앞서 조인 안하고 바로 조회했을때 발생한 총 20번이 수행되던 쿼리가 총 6번의 쿼리 수행으로 개선되었습니다.
20번과 6번이라고 하면 체감이 안될 수 있습니다.
이 개선은 Movie의 결과가 많으면 많을수록 쿼리 수행 횟수가 획기적으로 개선됩니다.
만약 1만개의 Movie (부모 엔티티)가 조회된다면
- 옵션 미적용시
- 총 20,001번의 쿼리가 수행됩니다.
- Movie 조회 쿼리 1번
- 각 Movie의 movieCountries 조회 쿼리가 10,000번
- 각 Movie의 moviePersonnels 조회 쿼리가 10,000번
- 옵션 적용시 (1000개)
- 총 21번의 쿼리가 수행됩니다.
- Movie 조회 쿼리 1번
- 각 Movie의 movieCountries 조회 쿼리가 10번 (10,000 / 1,000)
- 각 Movie의 moviePersonnels 조회 쿼리가 10번 (10,000 / 1,000)
최대 1,000 분의 1로 쿼리수를 줄일 수 있습니다.
Tip)
보통 옵션값을 1,000 이상 주지는 않습니다.
in절 파라미터로 1,000 개 이상을 주었을때 너무 많은 in절 파라미터로 인해 문제가 발생할수도 있기 때문입니다.
지금 옵션은 1000으로 두었기 때문에 movie가 1000개를 넘지 않으면 단일 쿼리로 수행 된다는 장점도 있습니다.
Tip)
같은 방법으로 Fetch 적용시 발생하는 페이징 문제도 동일하게 해결됩니다.
1:N 관계에서의 페이징 문제는 Join으로 인해 1에 대한 페이징이 정상작동 하지 않기 때문입니다.
결론
결론을 내리자면
- hibernate.default_batch_fetch_size를 글로벌 설정으로 사용해 N+1 문제를 최대한 in 쿼리로 기본적인 성능을 보장하게 한다.
- @OneToOne, @ManyToOne과 같이 1 관계의 자식 엔티티에 대해서는 모두 Fetch Join을 적용하여 한방 쿼리를 수행한다.
- @OneToMany, @ManyToMany와 같이 N 관계의 자식 엔티티에 관해서는 가장 데이터가 많은 자식쪽에 Fetch Join을 사용한다.
- Fetch Join이 없는 자식 엔티티에 관해서는 위에서 선언한 hibernate.default_batch_fetch_size 적용으로 100~1000개의 in 쿼리로 성능을 보장한다.
쿼리 분리하기
발생 쿼리 개수
@Query("SELECT m FROM Movie m " +
"LEFT JOIN FETCH m.moviePersonnels mp " +
"JOIN FETCH mp.personnel p " +
"WHERE m.id = :movieId AND m.isDeleted = false ")
Optional<Movie> findMovieDetailInfo(@Param("movieId") Long movieId);
public interface MovieCountriesRepository extends JpaRepository<MovieCountries, Long> {
@Query("SELECT mc FROM MovieCountries mc " +
"JOIN FETCH mc.countries c " +
"WHERE mc.movie.id = :movieId")
List<MovieCountries> findMovieCountriesByMovieId(@Param("movieId") Long movieId);
}
public interface MovieGenreRepository extends JpaRepository<MovieGenre, Genre> {
@Query("SELECT mg FROM MovieGenre mg " +
"JOIN FETCH mg.genre g " +
"WHERE mg.movie.id = :movieId")
List<MovieGenre> findMovieGenresByMovieId(@Param("movieId") Long movieId);
}
select
m1_0.movie_id,
...
m1_0.title
from
movie m1_0
left join
movie_personnel m2_0
on m1_0.movie_id=m2_0.movie_id
join
personnel p1_0
on p1_0.personnel_id=m2_0.personnel_id
where
m1_0.movie_id=?
and m1_0.is_deleted=0
2024-11-22T09:37:06.803+09:00 DEBUG 42700 --- [nio-8080-exec-2] org.hibernate.SQL :
select
g1_0.movie_id,
g1_0.gallery_id,
g1_0.created_at,
g1_0.image_url,
g1_0.last_modified_at
from
gallery g1_0
where
g1_0.movie_id=?
2024-11-22T09:37:06.807+09:00 DEBUG 42700 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_countries_id,
c1_0.countries_id,
c1_0.created_at,
c1_0.last_modified_at,
c1_0.nation,
m1_0.created_at,
m1_0.last_modified_at,
m1_0.movie_id
from
movie_countries m1_0
join
countries c1_0
on c1_0.countries_id=m1_0.countries_id
where
m1_0.movie_id=?
2024-11-22T09:37:06.810+09:00 DEBUG 42700 --- [nio-8080-exec-2] org.hibernate.SQL :
select
m1_0.movie_genre_id,
m1_0.created_at,
g1_0.genre_id,
g1_0.created_at,
g1_0.genre_type,
g1_0.last_modified_at,
m1_0.last_modified_at,
m1_0.movie_id
from
movie_genre m1_0
join
genre g1_0
on g1_0.genre_id=m1_0.genre_id
where
m1_0.movie_id=?
다만, 쿼리를 분리하는 방식은 Hibernate의 default_batch_fetch_size 방식과 비교했을 때, 데이터베이스를 여러 번 조회하게 되므로 DB 왕복 비용이 추가될 수 있습니다. 따라서 default_batch_fetch_size를 사용하는 방식과 쿼리를 분리해 조회하는 방식의 성능을 직접 비교한 후, 프로젝트에 더 적합한 방법을 선택하는 것이 좋습니다.
마무리 하며
이번 글에서는 두 개 이상의 OneToMany 관계에서 fetch join을 사용했을 때 발생한 MultipleBagFetchException을 해결하고, 이를 최적화하는 방식에 대해 작성했습니다.
두 개 이상의 OneToMany 관계에서 fetch join은 자주 사용될 수 있는 부분이기 때문에, 이 문제를 해결해 둔다면 추후 필요한 상황에서 유용하게 활용할 수 있을 것이라 생각합니다. 이번 경험을 통해 많은 것을 배울 수 있었고, 앞으로도 좋은 참고 자료가 될 것 같습니다.
참고
'프로젝트 > Filmeet' 카테고리의 다른 글
콘텐츠 기반 추천과 유사 사용자 기반 추천을 결합한 하이브리드 추천 시스템 만들기 (3) | 2024.12.19 |
---|---|
Redis 분산 락을 활용하여 동시성 문제 해결하기 (0) | 2024.12.04 |
@Query with "not in" not work with empty List parameter (0) | 2024.12.02 |
동적 조건 처리를 위한 Querydsl 사용하기 (0) | 2024.11.21 |
Filmeet 컬렉션 기능 (0) | 2024.11.21 |