[ T I L ] 2024.03.21

오세창·2024년 3월 21일

TIL

목록 보기
14/18

문제

조회 기능을 구현할 때 엔티티를 조회 후 이를 Dto 로 변환하는 과정과, 조회할 때 바로 dto 로 반환하는 것의 성능차이가 궁금했다.

시도 ( 1 )

    @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 로 변환 후 반환하는 로직을 작성하였다.

시도 ( 2 )

    @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 로 반환하게 작성하였다.

결과

수정 전

Query

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

수정 후

Query

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% 개선되었다.

다만, 특정 상황에서만 사용되는 쿼리다 보니 재사용성에서는 떨어진다고 느꼈고, 상황을 보며 적절하게 사용해야겠다.

0개의 댓글