이번 프로젝트에서는 QueryDsl을 사용하고 있다. 전 프로젝트까지만 해도 JPQL, QueryDsl을 왜 굳이 써야 하는지 감이 잡히지 않아서 Jpa method만 사용하였다. 자세한 내용은 추후 따로 글을 작성할 예정이다.
문제상황
웬만한 조회에서 QuerDsl을 사용하고 있었다. 하지만 문제가 발생했는데 보통은 key - value 형태로만 조회를 했다.

그런데 key - list 형태로 조회를 해와야하는 상황이 생겨버렸다. 즉 부모에 자식이 리스트 형태로 들어가야 하는 것이다.

한 이틀동안 뇌피셜을 기반으로 여러 시도를 하다가 실패했다. 다음 작업을 해야 할 날짜가 다가와서 일단 JPA 메서드로 범벅을 해놓고 넘어갔다. 어느 정도 시간이 지나 여유가 생겨서 다시 수정을 해보려고 마음먹었다. JPA 메서드를 사용하면 안되겠다고 생각한 큰 이유가 조회할 때 쿼리가 너무 많이 발생한다.. Nana라는 카테고리의 게시물인데 Nana, NanaTitle, NanaContent 의 구조로 되어 있고 게시물 관련 쿼리 말고도 회원의 Langauge, 회원이 좋아요 누른 여부 등등등 여러 쿼리가 합쳐져서 게시물 하나 조회하는데 28개의 쿼리가 발생하고 있었다. 이건 너무 심한 것 같아서 얼른 개선 방법을 찾아보았다.
해결방법 & 설명
내가 최종적으로 만들어야 하는 형태는 아래와 같다. data안에 list1을 넣어야 하고 그안에 또 list2를 넣어야 한다.. 결과 먼저 말하자면 list2까지는 실패했다. 거의 일주일 동안 여러 시도를 해보았지만 결국 list 1까지만 해결했다.
// 원래의 목표
"data":{
"key1" : "value"
"key2" : "value"
"list1":[
"key3" : "value"
"key4" : "value"
"list2" : [
"key5" : "value"
"key6" : "value"
]
]
}
// 성공 형태 (이 방법을 기준으로 설명)
"data":{
"key1" : "value"
"key2" : "value"
"list1":[
"key3" : "value"
"key4" : "value"
]
}
결론 먼저 말하자면 transfrom이라는 것을 이용해서 부모 안에 자식 리스트를 넣을 수 있었다. 사용 방법을 우선 설명하고 코드랑 비교해보겠다.
1. 반환형을 Map<id자료형, 반환받을 형태> resultMap 으로 지정한다. (resultMap은 그냥 변수명 자유롭게 가능 )
2. from, join, where, orderBy 등 을 상황에 맞게 적절히 작성한다.
3. .transform(groupBy(구분 값)).as(new Q객체(key1, key2, list(new Qlist1(key3, key4))))
4. 구분 값은 구분할 수 있는 unique 값-> 보통 entity의 id, Q객체의 인자들에는원래 QueryDsl 작성하는 것처럼 작성하고 자식 리스트 부분을 list()로 감싸서 하나의 Q객체를 추가적으로 만든다. -> list(new Qlist(ke3, key4))
5. 1의 반환받을 형태가 1개의 객체라면 return resultMap.get(구분값) ,
List라면 resultMap.keySet().stream().map(resultMap::get).collect(toList())
이렇게 보면 어렵지만 코드와 함께 보면 쉽게 이해할 수 있을 것이다. 나는 nana라는 객체를 상세 조회하는 쿼리이기에 transform(groupBy(구분 값)) 의 구분 값은 nana.id이다. 아래는 내가 실제 작성한 코드이다.
public NanaCompositeDto findNanaDetailById(Long nanaId, Locale locale) {
// Long은 구분 값 nana.id의 자료형, NanaCompositeDto는 반환 타입 -> 메서드 return 형
Map<Long, NanaCompositeDto> resultMap = queryFactory
// from, join, where, orderBy 작성
.from(nana)
.join(nanaTitle).on(nanaTitle.nana.id.eq(nanaId))
.join(nanaContent).on(nanaContent.nanaTitle.id.eq(nanaTitle.id))
.join(imageFile).on(imageFile.id.eq(nanaContent.imageFile.id))
.where(nanaTitle.language.locale.eq(locale))
.orderBy(nanaContent.number.asc())
//gropBy()에 구분 값 nana.id 작성
// QNanaResponse_NanaCompositeDto에 List-> 자식리스트 사용.
.transform(groupBy(nana.id).as(new QNanaResponse_NanaCompositeDto(
nana.version,
nanaTitle.id,
nanaTitle.language.locale,
nanaTitle.imageFile.originUrl,
nanaTitle.imageFile.thumbnailUrl,
nanaTitle.subHeading,
nanaTitle.heading,
nanaTitle.notice,
list(new QNanaResponse_NanaCompositeDto_NanaContentDto(
nanaContent.id,
imageFile.originUrl,
nanaContent.number,
nanaContent.subTitle,
nanaContent.title,
nanaContent.content)
)
)));
// resultMap에서 id에 해당하는 NanaDetailDto 반환
// 단일 값
return resultMap.get(nanaId);
전체적으로 요약해 보자면 조회해야 할 값들을 transfrom(groupBy(nana.id).as(!여기!) as 에 넣어서 원래의 querydsl처럼 사용한다. 이때 자식 리스트가 필요하면 list(new Q객체(값 1, 값 2, 값 3,,))을 사용한다. 여기서 groupBy에 넣는 값이 이해가 되지 않을 수 있는데 메서드 전체 반환 형을 List로 했을 때의 값을 생각해 보면 쉽다. 묶을 값의 기준을 설정하는 것이다.
public List<NanaCompositeDto> findNanaDetailById(Long nanaId, Locale locale) {
// 반환형 List로 변경, Map안에 반환형도 List로 변경
Map<Long, List<NanaCompositeDto>> resultMap = queryFactory
// from, join, where, orderBy 작성
.from(nana)
// 전의 코드와 동일해서 생략
.transform(groupBy(nana.id).as(new QNanaResponse_NanaCompositeDto(
nana.version,
// 전의 코드와 동일해서 생략
// List형태로 NanaDetailDto 반환
return resultMap.keySet().stream()
.map(resultMap:;get)
.collect(toList());
/////////////////////////////////
// 결과 값
"data":[
{ // nana id 1의 값들
"key1" : "value"
"key2" : "value"
"list1":[
"key3" : "value"
"key4" : "value"
]
}, // nana id 1의 값들
{ // nana id 2의 값들
"key1" : "value"
"key2" : "value"
"list1":[
"key3" : "value"
"key4" : "value"
]
}, // nana id 2의 값들
]
그래서 해결된 건가..?
위의 방법을 사용해서 두 번에 걸쳐 쿼리 수를 줄였다. 28개 -> 25개 -> 21개. (쿼리 수는 intellij 터미널에서 hibernates를 검색한 기준이다.)

28개
Hibernate:
/* select
member1,
member1.language
from
Member member1
left join
member1.language
where
member1.id = ?1
and member1.status = ?2 */ select
m1_0.id,
m1_0.birth_date,
m1_0.created_at,
m1_0.description,
m1_0.email,
m1_0.gender,
m1_0.language_id,
m1_0.level,
m1_0.modified_at,
m1_0.nickname,
m1_0.image_file_id,
m1_0.provider,
m1_0.provider_id,
m1_0.status,
m1_0.type,
l1_0.id,
l1_0.created_at,
l1_0.date_format,
l1_0.locale,
l1_0.modified_at
from
member m1_0
left join
language l1_0
on l1_0.id=m1_0.language_id
where
(
m1_0.status = 'ACTIVE'
)
and m1_0.id=?
and m1_0.status=?
Hibernate:
/* <criteria> */ select
n1_0.id,
n1_0.created_at,
n1_0.modified_at,
n1_0.version
from
nana n1_0
where
n1_0.id=?
Hibernate:
/* <criteria> */ select
nt1_0.id,
nt1_0.created_at,
nt1_0.heading,
nt1_0.image_file_id,
nt1_0.language_id,
nt1_0.modified_at,
nt1_0.nana_id,
nt1_0.notice,
nt1_0.sub_heading
from
nana_title nt1_0
where
nt1_0.nana_id=?
and nt1_0.language_id=?
Hibernate:
/* <criteria> */ select
nc1_0.id,
nc1_0.content,
nc1_0.created_at,
nc1_0.image_file_id,
nc1_0.modified_at,
nc1_0.nana_title_id,
nc1_0.number,
nc1_0.sub_title,
nc1_0.title
from
nana_content nc1_0
where
nc1_0.nana_title_id=?
order by
nc1_0.number
Hibernate:
/* <criteria> */ select
c1_0.id,
c1_0.content,
c1_0.created_at,
c1_0.modified_at
from
category c1_0
where
c1_0.content=?
Hibernate:
/* <criteria> */ select
f1_0.id,
f1_0.category_id,
f1_0.created_at,
f1_0.member_id,
f1_0.modified_at,
f1_0.post_id
from
favorite f1_0
where
f1_0.member_id=?
and f1_0.category_id=?
and f1_0.post_id=?
Hibernate:
/* <criteria> */ select
c1_0.id,
c1_0.content,
c1_0.created_at,
c1_0.modified_at
from
category c1_0
where
c1_0.content=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
25개
Hibernate:
/* select
member1,
member1.language
from
Member member1
left join
member1.language
where
member1.id = ?1
and member1.status = ?2 */ select
m1_0.id,
m1_0.birth_date,
m1_0.created_at,
m1_0.description,
m1_0.email,
m1_0.gender,
m1_0.language_id,
m1_0.level,
m1_0.modified_at,
m1_0.nickname,
m1_0.image_file_id,
m1_0.provider,
m1_0.provider_id,
m1_0.status,
m1_0.type,
l1_0.id,
l1_0.created_at,
l1_0.date_format,
l1_0.locale,
l1_0.modified_at
from
member m1_0
left join
language l1_0
on l1_0.id=m1_0.language_id
where
(
m1_0.status = 'ACTIVE'
)
and m1_0.id=?
and m1_0.status=?
Hibernate:
/* select
nana.id,
nana.version,
nanaTitle.id,
nanaTitle.language.locale,
nanaTitle.imageFile.originUrl,
nanaTitle.imageFile.thumbnailUrl,
nanaTitle.subHeading,
nanaTitle.heading,
nanaTitle.notice,
nanaContent
from
Nana nana
inner join
NanaTitle nanaTitle
on nanaTitle.nana.id = ?1
inner join
NanaContent nanaContent
on nanaContent.nanaTitle.id = nanaTitle.id
inner join
ImageFile imageFile
on imageFile.id = nanaTitle.imageFile.id
where
nanaTitle.language.locale = ?2
order by
nanaContent.number asc */ select
n1_0.id,
n1_0.version,
nt1_0.id,
l1_0.locale,
if2_0.origin_url,
if2_0.thumbnail_url,
nt1_0.sub_heading,
nt1_0.heading,
nt1_0.notice,
nc1_0.id,
nc1_0.content,
nc1_0.created_at,
nc1_0.image_file_id,
nc1_0.modified_at,
nc1_0.nana_title_id,
nc1_0.number,
nc1_0.sub_title,
nc1_0.title
from
nana n1_0
join
(nana_title nt1_0
join
image_file if2_0
on if2_0.id=nt1_0.image_file_id)
on nt1_0.nana_id=?
join
nana_content nc1_0
on nc1_0.nana_title_id=nt1_0.id
join
image_file if1_0
on if1_0.id=nt1_0.image_file_id
join
language l1_0
on l1_0.id=nt1_0.language_id
where
l1_0.locale=?
order by
nc1_0.number
Hibernate:
/* <criteria> */ select
c1_0.id,
c1_0.content,
c1_0.created_at,
c1_0.modified_at
from
category c1_0
where
c1_0.content=?
Hibernate:
/* <criteria> */ select
f1_0.id,
f1_0.category_id,
f1_0.created_at,
f1_0.member_id,
f1_0.modified_at,
f1_0.post_id
from
favorite f1_0
where
f1_0.member_id=?
and f1_0.category_id=?
and f1_0.post_id=?
Hibernate:
/* <criteria> */ select
c1_0.id,
c1_0.content,
c1_0.created_at,
c1_0.modified_at
from
category c1_0
where
c1_0.content=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
Hibernate:
/* <criteria> */ select
h1_0.id,
h1_0.category_id,
h1_0.created_at,
h1_0.keyword_id,
h1_0.language_id,
h1_0.modified_at,
h1_0.post_id
from
hashtag h1_0
where
h1_0.language_id=?
and h1_0.category_id=?
and h1_0.post_id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
k1_0.id,
k1_0.content,
k1_0.created_at,
k1_0.modified_at
from
keyword k1_0
where
k1_0.id=?
Hibernate:
select
if1_0.id,
if1_0.origin_url,
if1_0.thumbnail_url
from
image_file if1_0
where
if1_0.id=?
Hibernate:
select
il1_0.nana_content_id,
il1_1.id,
il1_1.created_at,
il1_1.description,
il1_1.info_type,
il1_1.modified_at
from
nana_info_type il1_0
join
nana_additional_info il1_1
on il1_1.id=il1_0.nana_additional_info_id
where
il1_0.nana_content_id=?
21개
Hibernate:
/* select
member1,
member1.language
from
Member member1
left join
member1.language
where
member1.id = ?1
and member1.status = ?2 */ select
m1_0.id,
m1_0.birth_date,
m1_0.created_at,
m1_0.description,
m1_0.email,
m1_0.gender,
m1_0.language_id,
m1_0.level,
m1_0.modified_at,
m1_0.nickname,
m1_0.image_file_id,
m1_0.provider,
m1_0.provider_id,
m1_0.status,
m1_0.type,
l1_0.id,
l1_0.created_at,
l1_0.date_format,
l1_0.locale,
l1_0.modified_at
from
member m1_0
left join
language l1_0
on l1_0.id=m1_0.language_id
where
(
m1_0.status = 'ACTIVE'
)
and m1_0.id=?
and m1_0.status=?
Hibernate:
/* select
nana.id,
nana.version,
nanaTitle.id,
결과는.....



테스트 코드를 돌렸을 때 빠른 순으로 정리하자면 28개 -> 21개 -> 25개 이다... 테스트는 돌릴 때마다 속도가 달라지지만 빠른 순서는 바뀌지 않았다.
(모든 코드를 보고 싶다면 아래의 링크로...)
https://abalone-athlete-adb.notion.site/QueryDsl-019fd12aee5c4636b0c285d9a8879eec?pvs=4)
결론
작지만 쿼리의 수가 줄어드는 것을 보고 두근두근 했지만 테스트를 돌려보고 조금은 절망적이었다 ㅋㅋㅋㅋㅋㅋ 하지만 깨달은 큰 결론 쿼리의 수가 줄어들면 무조건 속도가 빨라진다? 가 절대 아니다. 찾아보니 쿼리 속도 개선에는 쿼리의 수가 중요한 것이 아니라 상황에 따라 여러 규칙을 지켜야 했다.(좌변은 연산하지 않기, OR 대신 UNIION 사용 등등,,,) 그래서 일단 원상태로(JPA 메서드 사용) 돌려놓았다. 전에는 "쿼리 왜 알아야 하지 jpa가 다해주는데", "DML? 몰라도 될 것 같은데" 싶었지만 정말 어리석고 가벼운 생각이었다 허허.... 쿼리 성능 개선에 관심이 생겼고 SQLD 자격증에 갑자기 관심이 생겼다. 과연 이후의 결과는????? (sqld 자격증 접수해야겠다.)
'My project > Nanaland in Jeju' 카테고리의 다른 글
| 프로젝트 진행 시 엑셀에 작성한 데이터, DB에 저장하는 방법 (0) | 2024.10.17 |
|---|---|
| 스프링 검색어 자동완성 비동기 처리 (Feat. 1만번의 부하테스트 결론은 Over Engineering 이었다고 한다..) (5) | 2024.08.29 |
| 검색어 자동완성 구현하기 with Redis (Feat. Elasticsearch) (3) | 2024.08.01 |
| Stream API 사용 중 .toList()에서 UnsupportedOperationException 발생한 썰 (1) | 2024.07.26 |
| 맥에서 MySql, MaraiDB 두개 다 사용하기 (Feat. Docker 이래서 쓰는구나, 맥 mariaDB 설치하기) (2) | 2024.03.27 |