조회 기능을 구현할 때 엔티티를 조회 후 이를 Dto 로 변환하는 과정과, 조회할 때 바로 dto 로 반환하는 것의 성능차이가 궁금했다.
@Transactional(readOnly = true)
@Override
public RestaurantDetailsForm readRestaurant(Long restaurantId) {
Restaurant findRestaurant = restaurantRepository.findByIdAndIsDeletedFalse(restaurantId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 항목입니다."));
RestaurantDetailsForm restaurantDetailsForm = new RestaurantDetailsForm(findRestaurant);
List<Menu> findMenus = menuQueryService.findAllMenus(restaurantId);
List<MenuForm> menuForms = findMenus.stream()
.map(MenuForm::new)
.toList();
restaurantDetailsForm.setMenuForms(menuForms);
return restaurantDetailsForm;
}
최초에는 우선 엔티티를 조회하고 이를 이용하여 dto 로 변환 후 반환하는 로직을 작성하였다.
@Transactional(readOnly = true)
@Override
public RestaurantDetailsForm readRestaurantV2(Long restaurantId) {
RestaurantDetailsForm restaurantDetailsForm = restaurantRepository.findOneRestaurantForm(restaurantId);
List<MenuForm> menuForms = menuQueryService.findAllMenuForms(restaurantId);
restaurantDetailsForm.setMenuForms(menuForms);
return restaurantDetailsForm;
}
두 번째는 바로 dto 로 반환하고, 필요한 정보 매핑 후 클라이언트에게 전달한다.
@Override
public RestaurantDetailsForm findOneRestaurantForm(Long restaurantId) {
return jpaQueryFactory
.select(Projections.constructor(RestaurantDetailsForm.class,
restaurant.name,
restaurant.tel,
restaurant.info,
restaurant.openingTime,
restaurant.closingTime,
restaurant.isOperating,
restaurant.category.name))
.from(restaurant)
.where(restaurant.id.eq(restaurantId)
.and(restaurant.isDeleted.eq(false)))
.fetchOne();
}
@Override
public List<MenuForm> findAllMenuFormsByRId(Long restaurantId) {
return jpaQueryFactory
.select(Projections.constructor(MenuForm.class,
menu.id,
menu.name,
menu.price,
menu.content,
menu.image,
menu.stackQuantity
))
.from(menu)
.where(menu.restaurant.id.eq(restaurantId)
.and(menu.isDeleted.eq(false)))
.fetch();
}
이때 쿼리는 QueryDsl 을 이용하여 바로 Dto 로 반환하게 작성하였다.
Hibernate:
select
r1_0.id,
r1_0.city,
r1_0.street,
r1_0.zipcode,
r1_0.category_id,
r1_0.closing_time,
r1_0.created_at,
r1_0.deleted_at,
r1_0.info,
r1_0.is_deleted,
r1_0.is_operating,
r1_0.minimum_price,
r1_0.modified_at,
r1_0.name,
r1_0.opening_time,
r1_0.point,
r1_0.tel,
r1_0.user_id
from
restaurant r1_0
where
r1_0.id=?
and r1_0.is_deleted=0
Hibernate:
select
c1_0.id,
c1_0.name
from
category c1_0
where
c1_0.id=?
Hibernate:
select
m1_0.id,
m1_0.content,
m1_0.created_at,
m1_0.deleted_at,
m1_0.image,
m1_0.is_deleted,
m1_0.is_menu_status,
m1_0.modified_at,
m1_0.name,
m1_0.price,
m1_0.restaurant_id,
m1_0.stack_quantity
from
menu m1_0
where
m1_0.restaurant_id=?
and m1_0.is_deleted=0
1. Execution time: 53.32 ms
2. Execution time: 23.84 ms
3. Execution time: 24.97 ms
4. Execution time: 33.57 ms
5. Execution time: 31.24 ms
6. Execution time: 33.93 ms
7. Execution time: 27.74 ms
8. Execution time: 25.67 ms
9. Execution time: 29.76 ms
10. Execution time: 28.32 ms
Average execution time : 31.24 ms
Hibernate:
select
r1_0.name,
r1_0.tel,
r1_0.info,
r1_0.opening_time,
r1_0.closing_time,
r1_0.is_operating,
c1_0.name
from
restaurant r1_0
join
category c1_0
on c1_0.id=r1_0.category_id
where
r1_0.id=?
and r1_0.is_deleted=?
Hibernate:
select
m1_0.id,
m1_0.name,
m1_0.price,
m1_0.content,
m1_0.image,
m1_0.stack_quantity
from
menu m1_0
where
m1_0.restaurant_id=?
and m1_0.is_deleted=?
1. Execution time: 55.21 ms
2. Execution time: 20.20 ms
3. Execution time: 16.09 ms
4. Execution time: 12.87 ms
5. Execution time: 13.11 ms
6. Execution time: 14.51 ms
7. Execution time: 13.85 ms
8. Execution time: 12.02 ms
9. Execution time: 16.42 ms
10. Execution time: 13.99 ms
Average execution time : 18.83 ms
수정 전에 비해서 수정 후에는 전체적으로 39.72% 정도 성능이 개선된 것을 확인할 수 있었다.
select 되는 수가 줄어들었고, 이에 불필요한 엔티티 로딩이나 메모리 사용량이 감소됐기 때문에 성능이 개선된 거 같다.
또한 Projections.constructor 를 통해 별도의 엔티티 객체 생성 및 Java 스트림을 사용한 변환 과정 없이, 데이터베이스로부터 읽어온 결과를 직접 DTO 객체로 변환하여 반환하는 것이 성능 개선에 큰 영향을 미쳤다고 생각한다.
데이터베이스 조회 결과를 엔티티 대신 직접 DTO로 변환하는 접근 방식은 성능 면에서 상당히 효율적인 거 같다.
이를 통해 메모리 사용량과 실행 시간을 줄여준 덕에 평균 실행 시간이 39.72% 개선되었다.
다만, 특정 상황에서만 사용되는 쿼리다 보니 재사용성에서는 떨어진다고 느꼈고, 상황을 보며 적절하게 사용해야겠다.