ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 조회 시 N+1 문제 해결로 쿼리 최적화
    프로젝트/법잘알 2024. 2. 23. 20:41

    기능 개발을 할 때에는 쿼리에 대해 고려하지 않고 일단 개발을 진행했다. 개발을 완료하고 테스트를 하는 과정에서 쿼리를 호출할 때 N+1 문제가 발생하는 것을 확인했다.

     

     

     

    SuggestionController

    @GetMapping("/list")
    public ResponseEntity<List<SuggestionListResponse>> suggestionList(
            @AuthenticationPrincipal PrincipalDetails principal,
            @RequestParam(name = "category", required = false, value = "category") String category,
            @RequestParam(name = "likeCount", required = false) Boolean likeCount) {
    
        List<SuggestionListResponse> allSuggestion = suggestionService.findAllSuggestion(category, likeCount, principal.getUser());
        return ResponseEntity.ok(allSuggestion);
    }

    위의 api를 예로 보면 findAllSuggestion()이라는 service 계층의 함수는 SuggestionRepository의 findAllSuggestion()을 호출한다. 아래는 findAllSuggestion()의 함수이다.

     

    public class SuggestionRepositoryImpl implements SuggestionRepositoryCustom {
    
        private final JPAQueryFactory queryFactory;
    
        public SuggestionRepositoryImpl(EntityManager em) {
            this.queryFactory = new JPAQueryFactory(em);
        }
    
        @Override
        public List<Suggestion> findAllSuggestion(Category category, Boolean likeCount) {
            JPAQuery<Suggestion> query = queryFactory.selectFrom(suggestion);
    
            if(category != null) {
                query.where(suggestion.category.eq(category));
            }
    
            if (likeCount != null && likeCount) {
                query.orderBy(suggestion.likeList.size().desc());
            } else {
                query.orderBy(suggestion.createdDate.desc());
            }
            return query.fetch();
        }
    }

    querydsl로 작성된 findAllSuggestion() 함수는 N+1 문제를 발생시켰다.

     

    위의 사진처럼 response가 나오는데, 사용자의 이름과 likeCount 등으로 인해 suggestion을 호출할 때 user 테이블과 like 테이블의 데이터를 가져오는데, LAZY 로딩으로 인해서 실제 필요할 때에 데이터를 찾는 쿼리가 나가기 때문에 N+1 문제를 발생시키는 것이다.

     

     

    이론만을 가지고 해결하기보다는 실제로 N+1 문제가 발생했을 때에 로직 호출 시간이 얼마나 길어지는지 확인하기 위해 suggestion에 30만개의 데이터를 insert 해봤다.

     

    일부러 많이 넣긴 했는데 30만개나 들어가서 그런지 1분 44초가 걸렸다.

    (api를 호출할 때 너무 많이 넣은건 아닌가 싶어서 후회했었다...)

     

     

     

    fetch join으로 쿼리 튜닝

    public List<Suggestion> findAllSuggestion(Category category, Boolean likeCount) {
        JPAQuery<Suggestion> query = queryFactory.selectFrom(suggestion)
                .leftJoin(suggestion.user, user).fetchJoin()
                .leftJoin(suggestion.likeList, like).fetchJoin();

    querydsl의 select 절에서 user와 like 테이블을 leftJoin을 통해서 join 한 다음에 fetch join을 통해서 N+1 문제가 발생하는 것이 아니라 조건에 해당하는 데이터를 처음의 쿼리에 모두 가져오는 것으로 변경했다.

     

    suggestion 데이터를 get할 때에는 건의 글의 작성자와 좋아요의 데이터까지 항상 받아오기 때문에 fetch join을 사용하는 것이 효과적이라고 할 수 있다.

     

    시간이 1분 44초라는 어마한 시간에서 7초라는 짧은 시간으로 단축되었다.

     

    만약에 어플리케이션이 실제로 사용되었다면 아까 만들었던 30만개라는 데이터가 생기는 것이 무리는 아닐 것이다. 만약에 fetch join을 사용하지 않았더라면 서비스가 정상적으로 사용되지는 않았을 것이다.

     

     

    하지만 7초라는 시간도 매우 길다. ai 로직을 돌리는 것도 아닌데 단순히 조회를 하는 기능이 7초나 소모되는 서비스는 없다. 30만개의 데이터를 모두 조회하는 것은 일반적이지 않으며 페이징을 통해서 데이터를 나누는 것이 일반적이다.

     

     

    이번 포스팅에서는 극단적이기는 하지만 30만개의 데이터가 모두 필요한 시점에서의 N+1 문제를 해결하면 시간이 얼마나 단축될까? 라는 궁금증을 해결하였다.

     

     

    페이징의 내용도 많기 때문에 다음 포스팅에서 진행하겠다.

Designed by Tistory.