이번 시간에는 페이징 기능을 구현하며 상품 검색에 필요한 조회 성능을 개선한 과정을 정리하고자 합니다.
기존 검색 구현 방식에서는 상품과 연관된 데이터를 개별적으로 모두 조회한 후, Stream을 사용해 조건에 맞는 데이터를 필터링하고 매핑했습니다.
그러나 이 과정에서 상품 조회와 관련된 쿼리 코드에 중복이 많았고, 실행되는 쿼리 수도 많았습니다.
이 문제를 해결하기 위해 개선 전 코드와 개선한 코드를 비교하고, 성능 테스트 코드를 통해 개선 효과를 확인해 보겠습니다.
해당 트러블 슈팅에 대해서 Github에도 정리했으니 참고해주세요.
리팩토링 전
리팩토링 전 코드는 아래와 같습니다.
데이터 조회 및 처리 로직이 길고 복잡했습니다.
각 태그(userDefinedTag, recommendedTag)와 찜하기(wishItem) 정보를 별도의 쿼리로 각각 조회했으며,
이후 데이터를 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 테이블을 조회한 후, 추가로 item_img, user_defined_tag, recommended_tag, wish_item 테이블에 대해 다중 조회 쿼리가 실행된 것을 확인할 수 있습니다.
리팩토링 후
코드를 수정하면서 개선하려는 주요 사항은 쿼리의 성능 최적화입니다. 여러 개의 개별 쿼리를 실행하던 방식에서, in 절을 활용하여 여러 아이템에 대한 정보를 한 번에 조회하도록 변경하였습니다.
이를 통해 불필요한 조인 및 반복적인 쿼리 호출을 줄이고, 필요한 데이터만 효율적으로 조회할 수 있도록 했습니다.
또한, 태그 검색과 이름 검색을 분리하여 각 검색 조건에 맞는 쿼리만 수행하도록 함으로써, 쿼리 성능을 더욱 개선하고자 했습니다.
리팩토링 후 코드는 다음과 같습니다.
성능 테스트
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' 카테고리의 다른 글
거래요청에 대한 동시성 테스트 (2) | 2024.12.06 |
---|---|
다중 토큰: Refresh 토큰과 생명 주기 (0) | 2024.12.04 |
API 페이징 오류와 데이터 중복 문제 해결 (0) | 2024.08.10 |
댓글