프로젝트 중 Query dsl 을 사용하는데 오히려 Native query 보다 가독성이 떨어지고 코드길이가 길어진다는 생각이 들었습니다. 그래서 어떻게 하면 Query Dsl 의 가독성을 향상시키게 리팩토링을 할 수있을까 하며 QueryDsl 기존의 가독성이 떨어지는 코드를 글 처럼 읽을 수 있도록 리팩토링 하는 과정입니다.
조회를 어떻게하면 더 최적화 시킬 수 있을까 고민하다 Index 까지 설정하여 성능을 최적화 시켜보고자 합니다. 무작정 Index 를 도입하기보단 Select의 비율이 높은 테이블 부터 설정을 하였습니다.
@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 을 활용하면 좀더 직관적이게 표현 할 수 있습니다.
@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);
}
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 '!'
조회성능에 가장 핵심이 되는부분입니다.
이 두가지를 기준으로 판단하였습니다. 먼저 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 를 기준으로 조회를 합니다.
@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를 잡았습니다.
@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개의 데이터로 테스트 해보겠습니다.
먼저 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
Index 설정후
분명 Index 를 설정해서 key에도 난수가 아닌 명시해놓은 study_rule_idx 가 적혀있는데도 Extra 가 null로 나온다.
where id = 1으로 설정하여 index 로 조회하면 특정 값만 받아오기 때문에
Index 사용 조회
orm을 사용한다면 N + 1 문제를 겪게 됩니다. 이를 해결하기 위해선 다양한 방법 이있지만 fetchJoin 과 batchsize 로 해결하였습니다.
@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=?
@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 만으로도 해결이 가능합니다.
클린코드 작성과 성능 향상을 좀 더 고민을 하는 시간이었습니다.
많은 도움이 되었습니다, 감사합니다.