1차 refactoring (query dsl도입) : native query -> query dsl
2차 refactoring (기능 확장) : query dsl의 where, order by절을 service로직에서 구현하게 설정
query dsl함수 위에 builder class를 두어서 필요한 where, order by절을 service 로직에서 미리 주입 받는다
장점 : 다양한 where, order by를 주입받아 project list를 조회함으로 확장에 용이하게 refactoring되었음
단점 : 다양한 where, order by에 필요한 모든 table을 언제나 left join해줘야 한다 (성능 저하)
추가 보안 예정 : left join이 필요한 table들 또한 외부 service로직에서 받아서 처리한다
my page에서 조회하는 project list api 기능에 해당 project 마다의 funding정보가 추가되었다
이를 이전 방식으로 구현하기 위해서는 모든 project list api조건에 funding table들을 left join해줘야한다
추가로 left join이 필요한 table들
free funding entity
option entity
option funding entity
coupon entity
my page에서 project list api에 funding 정보를 추가하기 위해서 모든 project list api에 총합 4개의 table을 left join해야 한다
이는 엄청난 성능 저하를 발생시키고, 유지보수를 어렵게 만들 것이다
project list api를 처음 실행 시켰을 때 log aop에 의해 측정된 4000ms의 시간을 보게되었다
뒤늦게 알았지만 첫 번째 api 실행은 원래 오래 걸린다고 한다 (대충 db connection, jpa가 준비하는데 시간이 걸린다고 한다)
어찌됬든 api 호출에 4초 라는 시간을 본이상 api속도를 무시할 수는 없었고 추가 기능확장인 funding table들을 모두 left join하면 더 느려질 것을 알기 때문에 from절 외부 주입을 더 이상 미룰 수 없었다
project list조회에 기본적으로 필요한 user(author정보), img, like, project view 4개 table만 기본 join
이후 service로직에서 필요한 table을 추가 left join하여 사용
사실 where, order by부분과 크게 다르지 않을 것이라 예상했지만 구현하다 보니 여러 문제점을 만나게되었다
(정말 다양한 문제를 마주쳤고 정말 여러번 수정해야만 했다)
left join을 위해 from절에서 dsl함수로 넘야 할 정보가 생각 보다 복잡하다
List<QEntity tagetEntity, BooleanExpression on절 조건, boolean isFetch여부>
어느 정도 이전 함수의 수정을 감만했음으로 builder class에서 바로 dsl query를 생성한다 (select 부분만 작성한다)
class Builder {
public Builder(JPAQueryFactory queryFactory, Long userId) {
this.userId = userId;
mainQuery = queryFactory
.select(Projections.constructor(ResponseProjectListDetailDto.class,
...(생략),
fundingDslRepository.completeRate(projectEntity),
likeEntity.count(),
fundingDslRepository.fundingUserCount(projectEntity),
isLikeEntity.count(),
viewEntity.count()
))
.from(projectEntity);
}
service에서 left join할 table을 준다면 구지 builder class에서 join할 table을 관리하고 where, order by절에서 다시 service에게 제공할 필요가 있을까?
결정 : 1번 방법 QEntity는 각각의 service코드에서 관리한다
이유
// builder class 내부
public <T extends EntityPathBase,J extends EntityPathBase> Builder leftJoin(
T joinEntity,
Class<J> entityClass, FunctionInterface<BooleanExpression, J> onFunction,
boolean isFetch
){
J entity = (J) getEntity(entityClass);
if(isFetch)
mainQuery.join(projectEntity.options).fetchJoin();
else
mainQuery.leftJoin(joinEntity).on(onFunction.function(entity));
return this;
}
// service 사용 예시
QProjectTagEntity projectTag = new QProjectTagEntity("project_tag");
SelectProjectList
.builder(userId)
.leftJoin(
projectTag,
QProjectEntity.class, // can null
(project)->projectTag.project.id.eq(project.id),
true
)
.where(()->projectTag.id.in(tagIds))
... (생략)
mysql native query에서 가능하기 때문에 염두조차 하지 못했던 부분이다
select (project.title, count(projectView)) from project left join project.projectView
query dsl에서 group by를 사용하지 않고는 일반 column인 project 정보(title, content, ...)와 집계함수(count(projectView), count(like))를 동시에 사용할 수 없다
때문에 group by는 필수로 사용해야만 한다
select (project.title, count(view)) from project left join view group by project
결과 값을 dto를 통해서 가져오는 방식에서는 fetch join을 사용할 수 없다
fetch 정보를 가져오는 query를 자세히 본적있다면 당연한 부분이였지만 간과하고 있던 부분이었다
1. fetch 정보는 select문 내부에 필요한 column들을 추가 조회하는 방법으로 처리된다
2. query dsl에서 조회 결과를 dto로 받아오기 위해서는 dto 생성자와 select 조회 column이 완전 일치해야한다
1번과 2번을 동시에 만족시킬 방법이 필요하다
방법
1. dto에 필요한 모든 정보에 알맞은 생성자를 만든다
결론 : ProjectEntity로 조회한다
일단 필수 table인 (img, user)2개 table을 필수적으로 fetch join이 필요함으로 별 다른 방법이 없다
dto class constructor (ProjectEntity, Long, Long, ...)
select (project , count(like), count(view), ...) from ...
funding정보를 모두 left join할 경우 총 9개의 table을 left join해야 한다 (projecttag, img, tag, like, view, option, optionfunding, coupon, freefunding)
funding이전 5개 table left join을 했을 때도 속도가 느려서 문제였는데 여기에 추가 4개 table을 left join fetch를 사용한다면 더욱 느려지게 될 것이다
또한 serice에서의 코드 또한 4개의 table을 left join하면서 코드가 길어지게 된다
QOptionEntity optionEntity = new QOptionEntity();
QOptionFundingEntity optionFunding = new QOptionFundingEntity();
QFreeFundingEntity freeFunding = new QFreeFundingEntity();
QCouponEntity couponEntity = new QCouponEntity();
builder()
.leftJoin(
optionEntity ,
QProjectEntity.class , (project) -> project.id.eq(optioEntity.project.id),
true
)
.leftJoin(
optionFunding ,
null, (entity) -> optionEntity.id.eq(optionFunding.optio.id) ,
true
)
.leftJoin(
couponEntity ,
null, (entity) -> couponEntity.optionFunding.id.eq(optioFunding.id),
true
)
.leftJoin(
freeFunding ,
(project) -> project.id.eq(freeFunding.project.id).and(freeFunding.user.id.eq(userId)) ,
true
)
.where() ... 생략
service 코드에서의 코드 작성이 너무 길어지고 9개 table을 left join한 query의 속도가 심이 걱정되었기 때문에 다른 방법이 필요했다
funding 정보를 따로 조회하고 service에서 합친다
project list의 갯수는 10개 이하임으로 funding 정보를 가져오는 query의 속도는 크게 저하되지 않을 것이다
앞서서 project list의 갯수가 10개 이하라는 이유로 funding정보를 service에서 병합하는 방식으로 변경했다
그렇다면 결국 다른 정보들도 모두 해당되는건 아닐까? 처음 부터 project list조회는 조건에 맞는 List<project id>를 먼저 조회하고 필요한 다른 정보들은 추가 조회하고 service에서 병합하는 방법이 올았을지도 모르겠다
열심히 3차 refactoring까지 진행해서 얻은 결론이 결국 List<project id>조회 후 추가 query라는 사실이 씁씁하다
마지막 결론이 많이 다른 방식의 방식으로 결정났지만 처음 refactoring목표였던 from절 service에서 구현과 기존 sub query를 left join방식으로 변경에 성공했다
일단 left join으로 이전 query의 최적화는 성공했음으로 이번 refactoring을 종료한다
이후 할 일
언제나 다른 사람의 의견을 기다리고 있습니다