Movie 쿼리, 로직 튜닝

Kim Dong Kyun·2023년 4월 20일
1

1. Delete 쿼리 개선

  • 딜리트 쿼리 시 Select 문에 칼럼이 다 포함 될 필요가 없다.
  • 다음은 현재 쿼리와 로직이다.

1. 개선 전 쿼리

Hibernate: 
    select
        m1_0.id,
        m1_0.director,
        m1_0.genre,
        m1_0.in_use,
        m1_0.movie_name,
        m1_0.original_title,
        m1_0.poster_image_url,
        m1_0.release_date,
        m1_0.running_time,
        m1_0.synopsis 
    from
        movie m1_0 
    where
        m1_0.id=?
Hibernate: 
    delete 

...

2. 개선 전 로직

@Override
    public void deleteMovieById(Long movieId) {
        // join 이 일어나지 않는 가벼운 쿼리로 객체 탐색
        Movie foundMovie = jpaQueryFactory.selectFrom(movie)
                .where(movie.id.eq(movieId))
                .fetchOne();

        // N+1 delete 를 막기 위해. Native query 로 한번에 가능하지만, 위험하므로 아래와 같이 작성
        if (foundMovie != null) {
            jpaQueryFactory.delete(movieVideo)
                    .where(movieVideo.movie.id.eq(movieId))
                    .execute();

            jpaQueryFactory.delete(movieImage)
                    .where(movieImage.movie.id.eq(movieId))
                    .execute();

            jpaQueryFactory.delete(castMember)
                            .where(castMember.movie.id.eq(movieId))
                                    .execute();

            // Movie 엔티티 삭제
            jpaQueryFactory.delete(movie)
                    .where(movie.id.eq(movieId))
                    .execute();
        }
    }

3. 개선 후 쿼리

Hibernate: 
    select
        1 
    from
        movie m1_0 
    where
        m1_0.id=?
Hibernate: 
    delete 

...
  • Select 1 from~ 식으로 쿼리가 더 가벼워짐

4 . 개선 후 로직


 @Override
    public void deleteMovieById(Long movieId) {
        Integer one = jpaQueryFactory.selectOne()
                .from(movie)
                .where(movie.id.eq(movieId))
                .fetchOne();

        if (one == null){
            throw new IllegalArgumentException("해당하는 영화가 없습니다.");
        }
        
            jpaQueryFactory.delete(movieVideo)
                    .where(movieVideo.movie.id.eq(movieId))
                    .execute();

            jpaQueryFactory.delete(movieImage)
                    .where(movieImage.movie.id.eq(movieId))
                    .execute();

            jpaQueryFactory.delete(castMember)
                            .where(castMember.movie.id.eq(movieId))
                                    .execute();

            // Movie 엔티티 삭제
            jpaQueryFactory.delete(movie)
                    .where(movie.id.eq(movieId))
                    .execute();
        }
  • if 문의 스코프를 변경해서 직접 Exception 던지는 것으로 변경
  • queryDSL .selectOne() 매서드를 사용

2. 동적 조회 null 핸들

  • queryDSL은 BooleanExpression 타입을 이용한 동적 조회를 지원한다.
  • where 절의 조건문으로 사용 할 수 있으며, 모양은 다음과 같다.
private BooleanExpression searchByMovieName(String movieName) {
        return Objects.nonNull(movieName) ? movie.movieName.contains(movieName) : null;
    }

    private BooleanExpression searchByDirector(String director) {
        return Objects.nonNull(director) ? movie.director.contains(director) : null;
    }

위와 같이 영화 이름, 디렉터 두 가지 조건으로 검색 할 수 있게 하는 식.

그러나, 이 두 개의 조건에 모두 null이 들어오면 모~든 데이터가 조회된다. -> 매우 위험하다.

1. 이전 로직

@Override
    public List<MovieResponseDto> searchMovieByCond(MovieSearchCond movieSearchCond) {
        
        List<Movie> movieList = jpaQueryFactory
                .selectFrom(movie)
                .leftJoin(movie.movieImages, movieImage).fetchJoin()
                .leftJoin(movie.movieVideos, movieVideo).fetchJoin()
                .leftJoin(movie.castMembers, castMember).fetchJoin()
                .where(
                        searchByMovieName(movieSearchCond.getMovieName()),
                        searchByDirector(movieSearchCond.getDirector()),
                        movie.inUse.eq(true))
                .fetch();

        return MovieResponseDto.toDto(movieList);
    }

2. 이후 로직

 @Override
    public List<MovieResponseDto> searchMovieByCond(MovieSearchCond movieSearchCond) {

        if (movieSearchCond.getMovieName() == null && movieSearchCond.getDirector() == null){
            throw new IllegalArgumentException("항목을 입력 해 주세요");
        }

        List<Movie> movieList = jpaQueryFactory
                .selectFrom(movie)
                .leftJoin(movie.movieImages, movieImage).fetchJoin()
                .leftJoin(movie.movieVideos, movieVideo).fetchJoin()
                .leftJoin(movie.castMembers, castMember).fetchJoin()
                .where(
                        searchByMovieName(movieSearchCond.getMovieName()),
                        searchByDirector(movieSearchCond.getDirector()),
                        movie.inUse.eq(true))
                .fetch();

        return MovieResponseDto.toDto(movieList);
    }

위와 같이 두 항목 다 null일 경우 핸들해줘야 한다.

.fetchFirst()

  • fetchOne() 은 조건에 맞는 모든 필드 중 하나만 가져온다
  • 그러나 limit(1)을 걸면, 정말 fetchOne()의 의도대로 하나만 딱 가져온다. 즉 성능 개선 가능하다

    .fetchFirst() == .limit(1).fetchOne()

@Override
    public boolean existsByMovieId(Long movieId) {
        return jpaQueryFactory.selectOne()
                .from(movie)
                .where(movie.id.eq(movieId))
                .fetchFirst() != null;
        // fetchFirst == .limit(1).fetchOne()
    }
select
        1 
    from
        movie m1_0 
    where
        m1_0.id=? fetch first ? rows only

0개의 댓글