이번 시간에는 페이징 기능을 구현하며 상품 검색에 필요한 조회 성능을 개선한 과정을 정리하고자 합니다.
기존 검색 구현 방식에서는 상품과 연관된 데이터를 개별적으로 모두 조회한 후, Stream을 사용해 조건에 맞는 데이터를 필터링하고 매핑했습니다.
그러나 이 과정에서 상품 조회와 관련된 쿼리 코드에 중복이 많았고, 실행되는 쿼리 수도 많았습니다.
이 문제를 해결하기 위해 개선 전 코드와 개선한 코드를 비교하고, 성능 테스트 코드를 통해 개선 효과를 확인해 보겠습니다.
해당 트러블 슈팅에 대해서 Github에도 정리했으니 참고해주세요.
리팩토링 전
리팩토링 전 코드는 아래와 같습니다.
데이터 조회 및 처리 로직이 길고 복잡했습니다.
각 태그(userDefinedTag, recommendedTag) 정보를 별도의 쿼리로 각각 조회했으며,
이후 데이터를 Map으로 변환하여 처리하는 과정에서도 불필요하게 많은 테이블과 컬럼을 참조했습니다.
성능 테스트
테스트를 위해 35개의 상품 데이터를 등록했으며, 성능 측정을 위해 StopWatch를 사용해 쿼리 조회 시간을 확인했습니다.
테스트 코드는 아래와 같습니다.
Hibernate:
select
i1_0.item_id,
i1_0.price,
i1_0.description,
i1_0.name_kor,
i1_0.create_at,
i1_0.like_count,
i1_0.sanrio_characters,
i1_0.main_category,
i1_0.sub_category,
ii1_0.img_url
from
item i1_0
left join
item_img ii1_0
on ii1_0.item_id=i1_0.item_id
and ii1_0.is_main_img=?
where
i1_0.sanrio_characters=?
limit
?, ?
Hibernate:
select
udt1_0.item_id,
udt1_0.name
from
user_defined_tag udt1_0
Hibernate:
select
rt1_0.item_id,
rt1_0.tag_option
from
recommended_tag rt1_0
Hibernate:
select
wi1_0.item_id,
u1_1.email
from
wish_item wi1_0
join
wish_list wl1_0
on wl1_0.wish_list_id=wi1_0.wish_list_id
join
(users u1_0
join
user_base u1_1
on u1_0.user_id=u1_1.user_id)
on wl1_0.wish_list_id=u1_0.wish_list_id
Hibernate:
select
count(i1_0.item_id)
from
item i1_0
left join
item_img ii1_0
on ii1_0.item_id=i1_0.item_id
and ii1_0.is_main_img=?
where
i1_0.sanrio_characters=?
2024-12-10T16:47:44.275+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : StopWatch '': 0.4037543 seconds
----------------------------------------
Seconds % Task name
----------------------------------------
0.4037543 100%
2024-12-10T16:47:44.276+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : 코드 실행 시간 (s): 0.4037543
2024-12-10T16:47:44.277+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : Total Items: 7
2024-12-10T16:47:44.277+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : Page Size: 8
총 실행 시간은 0.4115583 초.
각 테이블에 대해 개별적으로 쿼리가 실행되었습니다.
쿼리를 보면, item 테이블을 조회한 후, 추가로 user_defined_tag, recommended_tag 테이블에 대해 쿼리가 실행된 것을 확인할 수 있습니다.
리팩토링 후
이를 개선하기 위해 컬렉션 조인으로 연관된 태그 정보를 함께 조회하도록 변경했으며, 중복 행 반환을 방지하기 위해 distinct() 키워드를 추가했습니다.
Stream을 사용하여 조건에 맞는 데이터를 필터링하고 매핑하는 과정을 convertToThumbnailItemDto() 메서드로 분리했습니다.
상품에 대해 찜등록한 유저 정보는 아래와 같이 getLikers() 메서드로 분리하여 사용했습니다.
성능 테스트
Hibernate:
select
i1_0.item_id,
i1_0.create_at,
i1_0.description,
i1_0.like_count,
i1_0.main_category,
i1_0.name_kor,
i1_0.price,
i1_0.sanrio_characters,
i1_0.sub_category,
i1_0.uploader_id
from
item i1_0
where
i1_0.sanrio_characters=?
limit
?, ?
Hibernate:
select
count(i1_0.item_id)
from
item i1_0
where
i1_0.sanrio_characters=?
Hibernate:
select
wi1_0.item_id,
u1_1.email
from
wish_item wi1_0
join
wish_list wl1_0
on wl1_0.wish_list_id=wi1_0.wish_list_id
join
(users u1_0
join
user_base u1_1
on u1_0.user_id=u1_1.user_id)
on wl1_0.wish_list_id=u1_0.wish_list_id
Hibernate:
select
iil1_0.item_id,
iil1_0.item_img_id,
iil1_0.img_url,
iil1_0.is_main_img
from
item_img iil1_0
where
iil1_0.item_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
udtl1_0.item_id,
udtl1_0.item_tag_id,
udtl1_0.name,
udtl1_0.tag_frequency
from
user_defined_tag udtl1_0
where
udtl1_0.item_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
rtl1_0.item_id,
rtl1_0.tag_id,
rtl1_0.tag_frequency,
rtl1_0.tag_option
from
recommended_tag rtl1_0
where
rtl1_0.item_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2024-12-10T16:47:44.443+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : StopWatch '': 0.1356374 seconds
----------------------------------------
Seconds % Task name
----------------------------------------
0.1356374 100%
2024-12-10T16:47:44.443+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : 코드 실행 시간 (s): 0.1356374
2024-12-10T16:47:44.443+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : Total Items: 7
2024-12-10T16:47:44.443+09:00 INFO 12556 --- [demoshop] [ main] c.e.d.r.item.ItemRepositoryTest : Page Size: 8
찜한 회원 정보를 가져오는 로직은 getLikers() 메서드를 재사용하여 중복된 코드를 줄였습니다.
또한, 상품 조회 쿼리에서 distinct와 countDistinct를 사용하여 중복 데이터를 제거하고 성능을 개선했습니다.
코드 가독성을 높이기 위해 DTO 변환 로직을 convertToThumbnailItemDto() 메서드로 분리하여 코드의 간결성과 유지보수성을 향상시켰습니다.
총 실행 시간은 0.1081724초.
단일 쿼리에서 효율적으로 실행되어 실행 시간이 단축되었습니다.
그리고 연관 해시태그 정보는 위와 같이 in 쿼리로 가져올 수 있습니다.
두 개 테스트를 함께 실행하면 다음과 같습니다.
후기
검색 구현에 대한 리팩토링 작업을 하면서 중복된 코드를 줄이고 성능 개선을 위해 고민하는 시간을 가질 수 있었습니다.
extract method를 적절히 사용하여 메서드 재사용성을 높였고, 중복 데이터를 제거하기 위해 distinct와 countDistinct를 활용했습니다.
성능을 확인하기 위해 실행 시간을 측정하는 성능 테스트를 진행했으나, 아쉽게도 상품 데이터가 100개 이상의 대량 데이터가 아니였습니다.
다음에는 대량 데이터를 추가하여 성능 테스트를 다시 진행할 생각입니다.
읽어주셔서 감사합니다:)
'프로젝트 > 개인 프로젝트 V3' 카테고리의 다른 글
Full Text Search를 이용한 DB 성능 개선 (0) | 2025.03.06 |
---|---|
거래요청에 대한 동시성 테스트 (2) | 2024.12.06 |
다중 토큰: Refresh 토큰과 생명 주기 (0) | 2024.12.04 |
1:N 조인에서 발생한 중복 데이터 및 누락 문제 해결 (0) | 2024.08.10 |
댓글