밤이 지나고, 나는 바보라는 알림이 깜빡깜빡했다. 왜냐, 리스트가 필수가 아닌 내 선택이라는 걸 아침에 다시 깨달았기 때문이다.
새벽 2시까지 공부하고 9시에 머어엉..하니 일어나서...
바보! 바보! 바보!
어제 새벽에 한 무의미한... 건 아닌 씨름은 서브쿼리 마스터가 되는 좋은 지름길이라 생각하기로 했다.
List<BookmarkCustomAgeResponseDto> bookmarksByCustomAgeList = jpaQueryFactory
.select(
Projections.constructor(
BookmarkCustomAgeResponseDto.class,
bookmark.id,
jobOpening.company,
jobOpening.hiringStartAt,
jobOpening.hiringEndAt,
jobOpening.position,
Expressions.stringTemplate(
"GROUP_CONCAT({0})",
jobOpeningKeyword.keyword.name
),
ExpressionUtils.as(
select(bookmark.count())
.from(bookmark)
.join(bookmark.user, user)
.where(user.age.between(requestDto.minAge(), requestDto.maxAge())),
"bookmarkCount"
)
))
.from(bookmark)
.join(bookmark.jobOpening, jobOpening)
.join(bookmark.user, user)
.leftJoin(jobOpening.jobOpeningKeywordList, jobOpeningKeyword)
.where(user.age.between(requestDto.minAge(), requestDto.maxAge()))
.groupBy(bookmark.id, jobOpening.company, jobOpening.hiringStartAt, jobOpening.hiringEndAt, jobOpening.position)
.orderBy(Expressions.numberPath(Long.class, "bookmarkCount").desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long totalCount = Optional.ofNullable(jpaQueryFactory
.select(Wildcard.count)
.from(bookmark)
.join(bookmark.user, user) //연령대 조회를 위해 이 쪽에서도 조인을 걸어야 함.
.where(user.age.between(requestDto.minAge(), requestDto.maxAge()))
.fetchOne()).orElse(0L);
return new PageImpl<>(bookmarksByCustomAgeList, pageable, totalCount);
}
이런식으로 코드를 작성했는데... 200 OK는 나오지만 여전히 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
조회수 결과가 이상하다. 뭔가 잘못된 게 확실했다.
다른 팀원이 짠 DTO를 참고한 게 나의 큰 패착 원인이었다. 나도 똑같이 List를 써야지! 라고 고집한 게 문제였다. 분명 처음에는 그 DTO를 같이 쓰려던 거였는데... 새벽이 되어 졸음이 쏟아지니 이상하게도 List를 집착하는 형태로 결론이 나버렸다.
결국 새벽 2시까지 잠들지 못하고 나는 헛고생만 하다가 날이 밝아 머리가 맑아지니 아 맞다. 리스트일 필요가 없구나... 하고 깨달아버렸다.
결론 > 나는 새벽 감성에 젖어 슬픔을 토로했지만 그 슬픔조차 나의 실수였다.
이제는 진짜 후다닥 빨리 리팩토링 할 자신이 있다. 솔직히 이리 쓰고 있는 시간도 아까워서 얼른 고치러 가야겠다. 사실 지금 멍청한 실수이긴 한데 드디어 해결책을 찾은거라서 기분이 좀 좋다.
근데 1차, 결과가 안 나온다! 왜지???
열심히 굴려굴려 검색하고 GPT 때리고 해봐서 결국 결과가 나왔는데 count 값이 이상하게 집계되어서 다시 고쳤다. 근데 이번에는 또.... 왜 모비스 천국이지...?
위에 내려보면 10개 다 모비스 QA 모집이다 ㅋㅋㅋㅋㅋㅋㅋㅋ 아무래도 집계할 때 즐겨찾기 한 유저 기준으로 집계되어서 유저끼리 겹치는 같은 공고 중에 제일 count 높은게 집계된 거 같다. 한 마디로 비둘기가 고개를 꺾으며 날고있다. 으아아악
그리고 결론, 어제 그 성공한 코드를 하나하나 분석하며 재시도하니 드디어 성공했다.
아무래도 어제의 코드를 더 공부하는 시간이 필요할 거 같다.
@Override
public Page<BookmarkCustomAgeResponseDto> readTop10BookmarksByAgeGroup(
ReadBookmarkAgeRequestDto requestDto, Pageable pageable) {
List<BookmarkCustomAgeResponseDto> bookmarksByCustomAgeList = jpaQueryFactory
.select(new QBookmarkCustomAgeResponseDto(
jobOpening.id,
jobOpening.title,
jobOpening.company,
jobOpening.educationLevel,
jobOpening.employmentType,
jobOpening.hiringStartAt,
jobOpening.hiringEndAt,
jobOpening.position,
jobOpening.salary,
jobOpening.minExperienceYears,
jobOpening.maxExperienceYears,
ExpressionUtils.as(
select(bookmark.id.count())
.from(bookmark)
.join(bookmark.user, user)
.join(bookmark.jobOpening)
.where(user.age.between(requestDto.minAge(), requestDto.maxAge())
.and(bookmark.jobOpening.id.eq(jobOpening.id))),
"bookmarkCount"
)
))
.from(bookmark)
.join(bookmark.jobOpening,jobOpening)
.join(bookmark.user,user)
.where(user.age.between(requestDto.minAge(),requestDto.maxAge()))
.groupBy(
jobOpening.id,
jobOpening.title,
jobOpening.company,
jobOpening.educationLevel,
jobOpening.employmentType,
jobOpening.hiringStartAt,
jobOpening.hiringEndAt,
jobOpening.position,
jobOpening.salary,
jobOpening.minExperienceYears,
jobOpening.maxExperienceYears
)
.orderBy(Expressions.numberPath(Long.class, "bookmarkCount").desc())
.offset(pageable.getOffset())
.limit(10)
.fetch();
Long totalCount = Optional.ofNullable(jpaQueryFactory
.select(Wildcard.count)
.from(bookmark)
.join(bookmark.user, user) //연령대 조회를 위해 이 쪽에서도 조인을 걸어야 함.
.where(user.age.between(requestDto.minAge(), requestDto.maxAge()))
.fetchOne()).orElse(0L);
return new PageImpl<>(bookmarksByCustomAgeList, pageable, totalCount);
}
햐햐햐... 드디어 성공코드다....
눈물이 앞을 가린다.... 처음에는 뭣도 모르고 얼레벌레... 여러가지를 누락했었다. 아마 어제 성공한 코드랑 유사한대도 자꾸만 실패한 이유는 jobOpening에서 표현해야하는 필드가 너무 많고, 나는 사람이라 그걸 제대로 캐치 못하는 경우가 빈번했기 때문인 것 같다.
앞으로도 DB를 많이 쓸 거고 QueryDSL도 매우 능숙하게 써야한다고 알고 있는데 그저 한숨만 나온다. 어제 오늘 하면서 그나마 배워서 도움 될 거는 ExpressionUtils
로 as, 즉 별칭을 사용할 수 있다는 점이고, 서브쿼리에서 별칭을 사용한 후에 그걸 기준으로 정렬까지 가능한 Expressions.numberPath
를 배웠다는 점인 것 같다.
그리고 쿼리문을 작성할 때 연관관계가 있다고 하더라도 조인을 사용하지 않으면 조회가 안된다는거..? 솔직히 DB로 사용할 땐 join 써야하는 걸 알고 있었는데도 ㅋㅋㅋㅋ 여기서는 놓친 게 아무래도 공부 부족인 거 같다는 생각이 많이 든다.
아 그리고, Qdto 형태는 dto 단에서 @QueryProjection
어노테이션을 써주어야 쓸 수 있는데 일반 프로젝션을 쓸 때는 바로 캐치할 수 없는 필드에 대한 체크를 해준다고 해야하나? 생성자에서 매개변수로 있는 값(dto 필드값) 중에 누락된 값이 있으면 코드 내에서 알 수 있게 해준다.
진작쓸걸
사실 어제도 그걸 검색해서 알고는 있었는데 짧은 코드라서 어노테이션 붙이기만 하고 안썼다.
확실히 dto 필드가 늘어나면서 멘붕이 오기 때문에 앞을로도 감사합니다. 하고 자주 쓸 것 같다. 그리고 어제오늘 이유없이 List에 집착해서 그걸 QueryDSL로 꼭 적용하려 하는 건... 앞으로 조심해야 할 문제인 거 같다. 굳이 그게 아니고 대체제가 있다면 굳이 어려운 길로 갈 필요는 없지 않은가? 심지어 보이는 화면이 크게 다른 것도 아닌데 말이다.
무언가 하나의 키워드에 몰두해서 프로젝트의 본질을 잊지 않도록 노력해야겠다. 이 부분은 항상 어렵게 느껴지지만 그래도 노력하면 조금씩 나아질거라 생각한다.
<출처>
https://jojoldu.tistory.com/401
https://ssow93.tistory.com/34
https://velog.io/@wndid2008/TIL-최종-프로젝트-7-연령대-즐겨찾기-커스텀-조회-1-실패