현재 팀원이 refactoring을 거친 상품 상세 페이지의 커밋된 코드를 보다가 생각하게되었다.
추가로 현재 로직에서의 Entity 조회와 Dto조회에 대해서 고민을 하다가 작성을 한다.
(JPA 2편에서는 선 Entity를 추천했지만 우아한 테크에서는 조회시에는 Dto를 선택했다.)
현재 요약
1. 관련 모든 테이블을 Join 시켜서 한번의 쿼리로 Dto로 여러개의 데이터를 리스트형태로 가져온다.(QDto에 Expression으로 list를 못넣기때문)
2. 모든 리스트를 돌면서 UrlList에 들어갈 값들을 (platform, url) Map으로 바꾼다.
3. Tag도 같음
4. 그리고 다시 1번 데이터의 첫번째 값을 가지고와서 위에서 준비한 값들을 넣어서 완성
아이템과 item_url은 일대다 조인
item과 category는 item_category라는 중간 테이블을 두고 일대다,다대일 조인이다.
public class ItemInfoResponseDto {
private Long itemId;
private String name;
private String brand;
private String description;
private int price;
private String imgUrl;
private int views;
private String platform;
private String url;
private String filterTag;
private String categoryTag;
private boolean isLiked;
private Long memberId;
public List<ItemInfoResponseDto> itemResponse(Long itemId, Member member) {
...
.innerJoin(qItemUrl).on(qItem.id.eq(qItemUrl.item.id))
.innerJoin(qItemCategory).on(qItem.id.eq(qItemCategory.item.id))
.innerJoin(qCategory).on(qItemCategory.category.id.eq(qCategory.id))
...
}
memberId 와 itemId를 넣어서 상세 정보를 구한다.
여기서 item과 item_url을 일대다 조인을 하여 중복 데이터가 증가한 상태이다.
또한 카테고리 역시 join되었다.
@Transactional
public ItemInfoResponseDto findItemInfo(Long itemId, Member member) {
increaseViews(itemId);
List<ItemInfoResponseDto> itemResponses = itemRepository.itemResponse(itemId, member);
// platform과 url을 그룹핑하여 반환
Map<String, String> platforms = new HashMap<>();
for (ItemInfoResponseDto itemResponse : itemResponses) {
if (itemResponse.getPlatform() != null && itemResponse.getUrl() != null) {
platforms.put(itemResponse.getPlatform(), itemResponse.getUrl());
}
}
ItemInfoResponseDto itemInfoResponseDto = itemResponses.get(0);
itemInfoResponseDto.setUrl(platforms.toString());
itemInfoResponseDto.setPlatform(platforms.keySet().toString());
List<String> filterTags = itemResponses.stream()
.map(ItemInfoResponseDto::getFilterTag)
.distinct()
.limit(2)
.toList();
itemInfoResponseDto.setFilterTag(filterTags.toString());
return itemInfoResponseDto;
3.2 아이템 조회수 업데이트 Service
@Transactional
public void increaseViews(Long itemId) {
Item item = em.find(Item.class, itemId);
item.increaseViews();
}
select
i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
where
i1_0.id=1003
2023-04-19T15:10:11.196+09:00 INFO 57660 --- [nio-8080-exec-1] p6spy : execution time: 17ms
update
cherishu.item
set
brand='아로마티카',
description='라벤더, 베르가못, 패츌리 등 오일이 블렌딩된 필로우미스트에요. 아늑하고 포근한 아로마향으로 지친 몸과 마음에 안정을 선사해줘요.',
img_url=NULL,
max_age=30,
min_age=20,
modified_date='2023-04-19T00:00:00.000+0900',
name='로즈마리 스칼프 스케일링 샴푸 바 135G',
price=22000,
views=54
where
id=1003
select
i1_0.id,
i1_0.name,
i1_0.brand,
i1_0.description,
i1_0.price,
i1_0.img_url,
i1_0.views,
i2_0.platform,
i2_0.url,
i6_0.name,
c1_0.name,
i8_0.member_id
from
cherishu.item i1_0
join
cherishu.item_url i2_0
on i1_0.id=i2_0.item_id
join
cherishu.item_category i4_0
on i1_0.id=i4_0.item_id
join
cherishu.category c1_0
on i4_0.category_id=c1_0.id
join
cherishu.item_filter i6_0
on i1_0.id=i6_0.item_id
left join
cherishu.item_like i8_0
on i1_0.id=i8_0.item_id
where
i1_0.id=1003
and i6_0.filter_id=5
and substr(cast(i1_0.id as text),1,1)=cast(c1_0.id as text)
Entity조회는 현재 연관 필드가 전부 LAZY LODING 처리가 되어 있으므로 fetch join
혹은 한번에 IN절로 가져오는 BatchSize
, deafult_batch_fetch_size
등을 활용할 수 있다.
(영속성 컨텍스트에 올라감)
Dto 조회는 원하는 값만 뽑아오는 것이기 때문에 innerjoin
등 조인을 이용하여 원하는 값을 뽑아오면 된다. 추가로 원하는 필드만 선택해서 가지고올 수있다.(조회 성능 최적화)
(영속성 컨텍스트에서 관리가 되지 않음)
그렇다면 어느경우에 해당 방법들을 사용해야할까?
추가적으로 Entity의 필드가 40~50개 정도면 차이가 나겠지만
요즘 DB 성능이 좋아서 엔티티를 조회해서 모든 필드값을 가지고 오는 것이 그렇게 크게 차이가 없다고함.(JPA 2편)
개발자가 힘들게 DTO를 만들어서 IN절 던지는 것과 결국 엔티티 조회방식의
BatchSize
와 동일함
엔티티 조회에서 성능이 안나오면 많은 데이터를 조회하는 상황이므로 캐시(Redis) 등을 사용하는 것을 고려해야함.
추가로 한방 쿼리로 보내면 네트워크 부하가 덜 올수는 있지만 중복되는 데이터를 많이 전송하게 되어
결국은 네트워크 부하가 쿼리를 한방 더쏘는 것과 비슷하다.
최적화란, 말 그대로 가장 알맞은 상황으로 맞춘다는 말이다.
(나무위키)
최적화를 시키기 위해 다양한 Dto코드 혹은 리팩토링도 중요하다.
하지만 코드의 인식성과 간결성, 생산성을 생각하면 무작정 최적화를 하는 것도 안좋은 것 같다.
물론 대량의 데이터를 사용하기 위해 N+1을 막으려고 최적화를 할 수도 있다.
하지만 1개의 데이터를 조회하고 연관 필드가 1개밖에 없다면 BatchSize
로 In절을 조절하거나 Dto로 원하는 값만 가져오는 최적화가 크게 빛을 못하고 가독성만 떨어뜨릴수 있다.
그래서 개인적으로 이번 쿼리를 보면서 최적화란알맞은 상황에 맞게 상황을 고려야하여 개발하는 것 같다.
어떤것이 더 좋을진 이후 테스트를 해볼 예정이다.
진짜 조회만 해야한다.
=> Dto를 쓰되 상황에 맞게 쪼개서 쿼리를 보낼 것 같다.
=> 중복 데이터를 피하고 역할과 반환값에 집중할 것 같다.
=> 데이터가 많아지면 1차 캐시 등 다른 최적화를 찾는다.
실시간으로 변경이 있다.
=> **엔티티 조회 방식을 사용할 것 같다.
=> fetch join
과 BatchSize
@OneToMany를 지양한다.
애매한 상황이다.
=> 엔티티 조회 방식으로 구성할 것 같다.
일단, 3번에서 보면 현재 Item 정보를 조회하는 쿼리가 중복으로 2번 호출이 된다.
(상세 정보, 영속화 엔티티)
그래서 Dto + 벌크성 수정 혹은 Item 엔티티를 조회해서 Dto로 반환 방식을 사용할 듯 하다.
하지만 나는 후자가 더 나은 듯하다.
두번째로, 현재 많은 데이터를 Dto 스펙에 맞춰서 가져오다보니
일부 필드만 달라지는데 다른 정보들까지 중복해서 가져오고 있다.
(join해서 가져왔기에)
(현재 그룹핑을 따로 진행하여 첫줄만 다시 들고온 후 덮어쓰기를 하는 방식이 사용되고 있다.)
해당 쿼리를 개인적으로
// Repository
@Override
public List<ItemInfo> itemReponse(Long itemId, Member member) {
}
@Override
public List<ItemUrlInfo> itemUrl(Long itemId) {
}
@Override
public List<ItemUrlInfo> itemTag(Long itemId) {
}
로 수정해서 Dto로 가져온뒤 수정을 할 것 같다.
해당 Item Entity 필드값을 거의 다 사용하고 있으니
Entity와 연관된 일대다에 IN절을 날려서 한번에 들고와서 인메모리에서 그룹핑을 해서 itemResponseDto
로 줄 듯하다.
여기도 후자가 좋은듯하다.(item view update 시에도 영속성 컨텍스트에서 찾아오니 쿼리가 나가지 않는다.)
필드 수도 현재 최대 10개로 적은 편에 속한다.
기본적으로 현재 방식도 원하는 값이 나오고 있다.
단지 더 좋은 방법, Dto조회 방식과 Entity 조회방식에서 차이점을 보기 위해 해당 글을 쓰고 리뷰를 하는 중이지 결과가 잘못된 것은 절대 아니다.
물론 json
형식에 수정이 조금 있어야하겠지만 원하는 내용의 값은 제대로 들어가고 있다.
현재 방식에서 Entity 조회를 사용했을 경우 성능을 비교하여 적어보려고 한다. (DB에서의 차이도 한번 보려고해서p6spy
도 사용했다.)
사실 필드 갯수가 그렇게 많지 않아서 크게 성능 차이가 나올까 싶다.
이렇게
p6spy : execution time: 22ms
일단 현재 코드로 나온 실행시간이다.
1.INFO 65410 --- [nio-8080-exec-2] p6spy : execution time: 22ms
2.INFO 65410 --- [nio-8080-exec-2] p6spy : execution time: 25ms
3.INFO 65410 --- [nio-8080-exec-2] p6spy: execution time: 25ms
total: findItemInformation 실행 시간: 359ms (첫 실행기준)
total .findItemInformation 실행 시간: 182ms (3번)
(첫 실행 기준)
1.2023-04-20T17:51:34.796+09:00 INFO 66910 --- [nio-8080-exec-1] p6spy : execution time: 18ms
select
distinct i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i2_0.item_id,
i2_0.id,
i2_0.platform,
i2_0.url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
join
cherishu.item_url i2_0
on i1_0.id=i2_0.item_id
where
i1_0.id=1003
2.2023-04-20T17:51:34.839+09:00 INFO 66910 --- [nio-8080-exec-1] p6spy : execution time: 13ms
select
c1_0.name
from
cherishu.item_category i1_0
join
cherishu.category c1_0
on c1_0.id=i1_0.category_id
where
i1_0.item_id=1003
3. 2023-04-20T17:51:34.858+09:00 INFO 66910 --- [nio-8080-exec-1] p6spy : execution time: 13ms
update
cherishu.item
set
brand='아로마티카',
description='라벤더, 베르가못, 패츌리 등 오일이 블렌딩된 필로우미스트에요. 아늑하고 포근한 아로마향으로 지친 몸과 마음에 안정을 선사해줘요.',
img_url=NULL,
max_age=30,
min_age=20,
modified_date='2023-04-20T00:00:00.000+0900',
name='로즈마리 스칼프 스케일링 샴푸 바 135G',
price=22000,
views=65
where
id=1003
total .findItemInformation 실행 시간: 182ms (3번)
(첫 실행 기준)
total.findItemInformation 실행 시간: 236ms (쿼리 5방)
1. 2023-04-20T18:17:39.471+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 15ms
select
i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
where
i1_0.id=1003 fetch first 1 rows only
2. 2023-04-20T18:17:39.493+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 9ms
select
i1_0.item_id,
i1_0.id,
i1_0.platform,
i1_0.url
from
cherishu.item_url i1_0
where
i1_0.item_id=1003
3.2023-04-20T18:17:39.511+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 9ms
select
i1_0.item_id,
i1_0.id,
i1_0.category_id
from
cherishu.item_category i1_0
where
i1_0.item_id=1003
4. 2023-04-20T18:17:39.526+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 9ms
select
c1_0.id,
c1_0.created_date,
c1_0.modified_date,
c1_0.name,
c1_0.parent_id
from
cherishu.category c1_0
where
c1_0.id in(53,107,54,55,108,1)
5.2023-04-20T18:17:39.575+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 11ms
update
cherishu.item
set
brand='아로마티카',
description='라벤더, 베르가못, 패츌리 등 오일이 블렌딩된 필로우미스트에요. 아늑하고 포근한 아로마향으로 지친 몸과 마음에 안정을 선사해줘요.',
img_url=NULL,
max_age=30,
min_age=20,
modified_date='2023-04-20T00:00:00.000+0900',
name='로즈마리 스칼프 스케일링 샴푸 바 135G',
price=22000,
views=80
where
id=1003
2023-04-20T18:17:39.586+09:00 INFO 67329 --- [nio-8080-exec-2] p6spy : execution time: 9ms
commit
2023-04-20T18:17:39.587+09:00 INFO 67329 --- [nio-8080-exec-2] c.b.i.controller.PublicItemController :
total.findItemInformation 실행 시간: 236ms (쿼리 5방)
해당 방법은 조회할 데이터가 많을 때 좋을 것 같다.
하나의 상세페이지를 조회하기엔 IN절의 효과를 극대화하지 못하는 것 같다.
1. 2023-04-20T19:01:25.951+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 15ms
select
i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
where
i1_0.id=1003 fetch first 1 rows only
2. 2023-04-20T19:01:25.978+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 11ms
select
i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
where
i1_0.id=1004 fetch first 1 rows only
3. 2023-04-20T19:01:25.993+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 11ms
select
i1_0.item_id,
i1_0.id,
i1_0.platform,
i1_0.url
from
cherishu.item_url i1_0
where
i1_0.item_id in(1003,1004)
4.2023-04-20T19:01:26.011+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 12ms
select
i1_0.item_id,
i1_0.id,
i1_0.category_id
from
cherishu.item_category i1_0
where
i1_0.item_id in(1003,1004)
5. 2023-04-20T19:01:26.030+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 12ms
select
c1_0.id,
c1_0.created_date,
c1_0.modified_date,
c1_0.name,
c1_0.parent_id
from
cherishu.category c1_0
where
c1_0.id in(53,107,54,55,108,60,57,59,109,110,58,1)
6.2023-04-20T19:01:26.109+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 12ms
update
cherishu.item
set
brand='아로마티카',
description='라벤더, 베르가못, 패츌리 등 오일이 블렌딩된 필로우미스트에요. 아늑하고 포근한 아로마향으로 지친 몸과 마음에 안정을 선사해줘요.',
img_url=NULL,
max_age=30,
min_age=20,
modified_date='2023-04-20T00:00:00.000+0900',
name='로즈마리 스칼프 스케일링 샴푸 바 135G',
price=22000,
views=84
where
id=1003
2023-04-20T19:01:26.122+09:00 INFO 68002 --- [nio-8080-exec-2] p6spy : execution time: 11ms
commit
total. findItemInformation 실행 시간: 271ms (6번)
2개 조회를 해야하니 2번 엔티티 조회를 위한 추가 쿼리 1개 이외에 변동 사항없고 실행 시간도 IN로 인한 최적화
(4번은 각각 Dto로 조회해와서 합칠려고했으나 이후 수업 때문에 엔티티로만)
total. findItemInformation 실행 시간: 360ms (4번)
1. 2023-04-20T18:27:12.487+09:00 INFO 67484 --- [nio-8080-exec-1] p6spy : execution time: 19ms
select
i1_0.id,
i1_0.brand,
i1_0.created_date,
i1_0.description,
i1_0.img_url,
i1_0.max_age,
i1_0.min_age,
i1_0.modified_date,
i1_0.name,
i1_0.price,
i1_0.views
from
cherishu.item i1_0
where
i1_0.id=1003 fetch first 1 rows only
2. 2023-04-20T18:27:12.554+09:00 INFO 67484 --- [nio-8080-exec-1] p6spy : execution time: 13ms
select
i1_0.url
from
cherishu.item_url i1_0
where
i1_0.item_id=1003
3. 2023-04-20T18:27:12.574+09:00 INFO 67484 --- [nio-8080-exec-1] p6spy : execution time: 13ms
select
c1_0.name
from
cherishu.item_category i1_0
join
cherishu.category c1_0
on c1_0.id=i1_0.category_id
where
i1_0.item_id=1003
4. 2023-04-20T18:27:12.606+09:00 INFO 67484 --- [nio-8080-exec-1] p6spy : execution time: 14ms
update
cherishu.item
set
brand='아로마티카',
description='라벤더, 베르가못, 패츌리 등 오일이 블렌딩된 필로우미스트에요. 아늑하고 포근한 아로마향으로 지친 몸과 마음에 안정을 선사해줘요.',
img_url=NULL,
max_age=30,
min_age=20,
modified_date='2023-04-20T00:00:00.000+0900',
name='로즈마리 스칼프 스케일링 샴푸 바 135G',
price=22000,
views=81
where
id=1003
total. findItemInformation 실행 시간: 360ms (4번)
fetchJoin
과 batch_fetch_size
활용현재는 엔티티를 조회해서 페이징 여부가 없는 페이지인 상황으로 진행.