우아한 query Dsl 로 전환(Feat.Index, N + 1)

박우영·2023년 8월 10일
0

자바/코틀린/스프링

목록 보기
31/35

개요

프로젝트 중 Query dsl 을 사용하는데 오히려 Native query 보다 가독성이 떨어지고 코드길이가 길어진다는 생각이 들었습니다. 그래서 어떻게 하면 Query Dsl 의 가독성을 향상시키게 리팩토링을 할 수있을까 하며 QueryDsl 기존의 가독성이 떨어지는 코드를 글 처럼 읽을 수 있도록 리팩토링 하는 과정입니다.

조회를 어떻게하면 더 최적화 시킬 수 있을까 고민하다 Index 까지 설정하여 성능을 최적화 시켜보고자 합니다. 무작정 Index 를 도입하기보단 Select의 비율이 높은 테이블 부터 설정을 하였습니다.

Query Dsl


Refactoring

AS-IS

    @Override
    public Page<JobStatistic> filterList(int sectorNum, int careerCode, String place, String subject, Pageable pageable) {
        JPAQuery<JobStatistic> query;
        if (careerCode == -1) {
            query = jpaQueryFactory.select(jobStatistic)
                    .from(jobStatistic)
                    .where(
                            jobStatistic.subject.contains(subject), jobStatistic.place.contains(place),
                            jobStatistic.sectorCode.eq(sectorNum))
                    .orderBy(jobStatistic.id.desc());
        } else {
            query = jpaQueryFactory.select(jobStatistic)
                    .from(jobStatistic)
                    .where(
                            jobStatistic.subject.contains(subject), jobStatistic.place.contains(place),
                            jobStatistic.sectorCode.eq(sectorNum), jobStatistic.career.eq(careerCode))
                    .orderBy(jobStatistic.id.desc());
        }


        List<JobStatistic> content = query
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = query.fetchCount();

        return new PageImpl<>(content, pageable, total);
    }

기존의 코드입니다. 채용정보를 조회하기 위한 쿼리인데 분야, 경력, 위치, 제목 등을 입력받아
페이징 하는 쿼리인데 가독성이 매우 떨어집니다. 무엇보다 if 문으로 작성되어있는 조건문 안에 있는 코드의 많은 것들이 중복되는것을 확인 할 수 있는데

BooleanExpression 을 활용하면 좀더 직관적이게 표현 할 수 있습니다.

TO-BE

    @Override
    public Page<JobStatistic> filterList(int sectorNum, int careerCode, String place, String subject, Pageable pageable) {
        JPAQuery<JobStatistic> query = jpaQueryFactory
                .select(jobStatistic)
                .from(jobStatistic)
                .where(
                        eqSector(sectorNum),
                        eqCareer(careerCode),
                        eqSubject(subject),
                        eqPlace(place)
                )
                .orderBy(jobStatistic.id.desc());
        List<JobStatistic> content = query
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = query.fetchCount();

        return new PageImpl<>(content, pageable, total);
    }
    private BooleanExpression eqSector(int sectorNum) {
        return sectorNum == -1 ? null : jobStatistic.sectorCode.eq(sectorNum);
    }
    private BooleanExpression eqCareer(int careerCode) {
        return careerCode == -1 ? null : jobStatistic.career.eq(careerCode);
    }
    private BooleanExpression eqPlace(String place) {
        return place == null ? null : jobStatistic.place.contains(place);
    }
    private BooleanExpression eqSubject(String subject) {
        return subject == null ? null : jobStatistic.subject.contains(subject);
    }

Query

Hibernate: 
    select
        j1_0.id,
        j1_0.career,
        j1_0.company,
        j1_0.dead_line,
        j1_0.place,
        j1_0.sector,
        j1_0.sector_code,
        j1_0.start_date,
        j1_0.subject,
        j1_0.url 
    from
        job_statistic j1_0 
    where
        j1_0.sector_code=? 
        and j1_0.subject like ? escape '!' 
        and j1_0.place like ? escape '!' 
    order by
        j1_0.id desc limit ?,
        ?
Hibernate: 
    select
        count(j1_0.id) 
    from
        job_statistic j1_0 
    where
        j1_0.sector_code=? 
        and j1_0.subject like ? escape '!' 
        and j1_0.place like ? escape '!'

Index


Index 설정 어떻게 했는가?

조회성능에 가장 핵심이 되는부분입니다.

  1. 조회의 비율이 가장높은 엔티티
  2. Index 로 설정할 Column

이 두가지를 기준으로 판단하였습니다. 먼저 Front-End에서 가장 많이 조회가 이루어지는 Controller 입니다.

    @GetMapping("/v1/studyrules/{studyid}")
    @Operation(summary = "미션 studyId로 조회", description = "스터디 아이디로 스터디 규칙리스트 조회.", tags = "StudyRule-조회")
    public RsData<List<StudyRuleListDto>> studyRuleFromStudy(@PathVariable("studyid") Long studyId) {
        List<StudyRule> studyRuleList = studyRuleService.getStudyRuleFromStudy(studyId);
        List<StudyRuleListDto> collect = studyRuleList.stream()
                .map(StudyRuleListDto::new)
                .toList();
        return RsData.of("S-1", "성공", collect);
    }

StudyRule 이라는 Entity 에서 조회가 이루어지는데 StudyId 를 기준으로 조회를 합니다.

QueryDsl

    @Override
    public List<StudyRule> findStudyRuleFromStudy(Long studyId) {
        return jpaQueryFactory.selectFrom(studyRule)
                .leftJoin(studyRule.study, study)
                .leftJoin(studyRule.problems, problem)
                .fetchJoin()
                .where(study.id.eq(studyId))
                .fetch();
    }

QueryDsl 은 다음과 같이 구현하였습니다. study 1:m studyrule 1:m problem
으로 이루어져 있고 where 절에는 studyId 가 들어가있습니다.
따라서 studyId 를 기준으로 Index를 잡았습니다.

Index 비교해보기

    @PostMapping("/v1/test")
    @Operation(summary = "테스트", description = "StudyRule 생성", tags = "StudyRule-생성")
    public RsData<CreateStudyRuleResponse> test(@RequestBody @Valid CreateStudyRuleRequest request) {
        Long studyRuleId = 0L;
        for (int i = 0; i < 10000; i++) {
            studyRuleId = studyRuleService.create(request);
        }
        CreateStudyRuleResponse createStudyRuleResponse = new CreateStudyRuleResponse();
        createStudyRuleResponse.setId(studyRuleId);
        return RsData.successOf(createStudyRuleResponse);
    }

처음에 데이터 100개정도씩 테스트를해봤는데 유의미한 속도차이를 느끼지못해 데이터를 생성하여 약 80000개의 데이터로 테스트 해보겠습니다.

AS-IS

먼저 Query Dsl 을 활용하여 발생하는 쿼리를 SQL로 작성하여 확인해봤습니다.

EXPLAIN SELECT study_rule.*
FROM study_rule
LEFT JOIN study ON study_rule.study_id = study.id
LEFT JOIN problem ON study_rule.id = problem.study_rule_id
WHERE study.id > 1;

EXPLAIN

TO-BE

Index 설정후

분명 Index 를 설정해서 key에도 난수가 아닌 명시해놓은 study_rule_idx 가 적혀있는데도 Extra 가 null로 나온다.

where id = 1으로 설정하여 index 로 조회하면 특정 값만 받아오기 때문에

Index 사용 조회

N + 1

orm을 사용한다면 N + 1 문제를 겪게 됩니다. 이를 해결하기 위해선 다양한 방법 이있지만 fetchJoin 과 batchsize 로 해결하였습니다.

AS-IS

    @Override
    public Page<Article> findAllByTitle(String title, Pageable pageable) {

        JPAQuery<Article> query = queryFactory.selectFrom(article)
                .where(article.title.contains(title));

        List<Article> result = query
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        long total = query.fetchCount();
        return new PageImpl<>(result, pageable, total);
    }

Hibernate: select a1_0.id,a1_0.board_id,a1_0.content,a1_0.create_at,a1_0.member_id,a1_0.title,a1_0.update_at,a1_0.view_count from article a1_0 where a1_0.title=? limit ?,?
Hibernate: select count(a1_0.id) from article a1_0 where a1_0.title=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?
Hibernate: select m1_0.id,m1_0.article_id,m1_0.create_at,m1_0.update_at,m1_0.user_id from member m1_0 where m1_0.id=?

TO-BE

    @Override
    public Page<Article> findAllByTitle(String title, Pageable pageable) {

        JPAQuery<Article> query = queryFactory.selectFrom(article)
                .innerJoin(article.member, member)
                .fetchJoin()
                .innerJoin(article.board, board)
                .fetchJoin()
                .where(article.title.contains(title));

        List<Article> result = query
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        long total = query.fetchCount();
        return new PageImpl<>(result, pageable, total);
    }
Hibernate: 
    select
        count(a1_0.id) 
    from
        article a1_0 
    where
        a1_0.title=?
Hibernate: 
    select
        m1_0.id,
        m1_0.article_id,
        m1_0.create_at,
        m1_0.update_at,
        m1_0.user_id 
    from
        member m1_0 
    where
        m1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,...)

간단한 단건 조회같은건 fetchJoin을 하지않고 batchsize 만으로도 해결이 가능합니다.

클린코드 작성과 성능 향상을 좀 더 고민을 하는 시간이었습니다.

참고


stackoverflow
우아한테크 유튜브

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

많은 도움이 되었습니다, 감사합니다.

답글 달기