코드개선, 성능개선

검색 성능 개선 - 2

cocodingding 2024. 1. 20. 01:45

지난 번 서비스적인 관점에서 QueryDsl을 적용해 조건검색을 수월하게 하도록 수정을 했다.

그래서 서비스적 관점에선 개선된 부분이 맞으나 검색 속도 측면에선 역시 개선점은 없었다

하지만 쿼리를 보니 필요 없는 쿼리들이 나가는걸 확인했고 DTO 및 엔티티를 다시 살펴보기로 했다.

 

문제점 확인

1. 필요없는 필드들이 많은 DTO를 사용해서 반환

@Getter
@RequiredArgsConstructor
@Builder
public class RestaurantResponseDto {
    private final Long id;
    private final String restaurantName;
    private final String address;
    private final Category category;
    private final String resNumber;
    @JsonIgnore
    private final List<Menu> menu;
    private final String ownerName;
    private final List<Review> reviews;

    public static RestaurantResponseDto fromRestaurantEntity(Restaurant restaurant) {
        return RestaurantResponseDto.builder()
                .id(restaurant.getId())
                .restaurantName(restaurant.getRestaurantName())
                .address(restaurant.getAddress())
                .category(restaurant.getCategory())
                .resNumber(restaurant.getResNumber())
                .menu(restaurant.getMenu())
                .ownerName(restaurant.getMember().getNickName())
                .reviews(restaurant.getReviews())
                .build();
    }

    public static List<RestaurantResponseDto> fromListRestaurantEntity(List<Restaurant> restaurants) {
        return restaurants.stream()
                .map(RestaurantResponseDto::fromRestaurantEntity)
                .collect(Collectors.toList());
    }
}

DTO자체만으로는 쿼리측면에서 크게 개선이 될것같진 않았지만 그래도 의미 없는 필드들이 너무 많은  상태(사실상 이럴거면 엔티티를 그대로 반환하는 것과 크게 다른게 없어보인다)

 

2. FETCH.EAGER

package com.challnege.delivery.domain.restaurant.entity;

import com.challnege.delivery.domain.member.entity.Member;
import com.challnege.delivery.domain.menu.entity.Menu;
import com.challnege.delivery.domain.review.entity.Review;
import com.challnege.delivery.global.audit.Category;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Entity
@Getter
@NoArgsConstructor
public class Restaurant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String restaurantName;

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    Category category;

    @Column(nullable = false)
    private String resNumber;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "restaurant", cascade = CascadeType.ALL)//왜 eager??
    private List<Menu> menu;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "restaurant", cascade = CascadeType.ALL)//왜 eager??
    private List<Review> reviews;


    @Builder
    public Restaurant(long id, String restaurantName, String address, Category category, String resNumber, List<Menu> menu, Member member, List<Review> reviews) {
        this.id = id;
        this.restaurantName = restaurantName;
        this.address = address;
        this.category = category;
        this.resNumber = resNumber;
        this.menu = menu;
        this.member = member;
        this.reviews = reviews;
    }


}

사실 restaurant 도메인은 내가 맡은 도메인이 아니여서 몰랐는데,

EAGER로 했는지는  모든 OneToMany 관계의 테이블에 EAGER가 걸려있었다.

저번에 봤던 의미없는 쿼리들이 나갔던 핵심 이유로 확인된다

다만 해당 도메인을 맡으신 분의 이유가 따로 있을 수도 있으니 지금 수정은 해놨으나 나중에 확인해볼 예정이다.

 

개선 사항

1. 추가 DTO 생성

@Getter
@RequiredArgsConstructor
@Builder
public class RestaurantSearchResponseDto {

    private final Long id;
    private final String restaurantName;
    private final String address;
    private final Category category;
    private final String resNumber;

    public static RestaurantSearchResponseDto fromRestaurantEntity(Restaurant restaurant) {
        return RestaurantSearchResponseDto.builder()
                .id(restaurant.getId())
                .restaurantName(restaurant.getRestaurantName())
                .address(restaurant.getAddress())
                .category(restaurant.getCategory())
                .resNumber(restaurant.getResNumber())
                .build();
    }

    public static List<RestaurantSearchResponseDto> fromListRestaurantEntity(List<Restaurant> restaurants) {
        return restaurants.stream()
                .map(RestaurantSearchResponseDto::fromRestaurantEntity)
                .collect(Collectors.toList());
    }
}

새로운 DTO 클래스를 만들어 보여주지 않아도 되는 정보는 빼고 필요한 필드들만 담아서 작성해 반환하도록 수정했다.

 

2. FETCH.LAZY

 

@Entity
@Getter
@NoArgsConstructor
public class Restaurant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String restaurantName;

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    Category category;

    @Column(nullable = false)
    private String resNumber;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "restaurant", cascade = CascadeType.ALL)//왜 eager??
    private List<Menu> menu;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "restaurant", cascade = CascadeType.ALL)//왜 eager??
    private List<Review> reviews;


    @Builder
    public Restaurant(long id, String restaurantName, String address, Category category, String resNumber, List<Menu> menu, Member member, List<Review> reviews) {
        this.id = id;
        this.restaurantName = restaurantName;
        this.address = address;
        this.category = category;
        this.resNumber = resNumber;
        this.menu = menu;
        this.member = member;
        this.reviews = reviews;
    }


}

 

테이블들에 걸려있던 EAGER를 LAZY로 수정했다.

[Hibernate] 
    select
        distinct r1_0.id,
        r1_0.address,
        r1_0.category,
        r1_0.member_id,
        r1_0.res_number,
        r1_0.restaurant_name 
    from
        restaurant r1_0 
    where
        lower(r1_0.restaurant_name) like ? escape '' 
        and lower(r1_0.address) like ? escape '' 
        and lower(cast(r1_0.category as char)) like ? escape '' 
    order by
        r1_0.id 
    limit
        ?,?
[Hibernate] 
    select
        count(distinct r1_0.id) 
    from
        restaurant r1_0 
    where
        lower(r1_0.restaurant_name) like ? escape '' 
        and lower(r1_0.address) like ? escape '' 
        and lower(cast(r1_0.category as char)) like ? escape ''

 

기대했던대로 의미 없는 쿼리가 더 이상 나가지 않는 모습을 볼 수 있다.

 

Jmeter를 통한 테스트

필요 없는 쿼리가 나가는 것을 막는것 만으로도 이전과 비교해 초당 처리 갯수(49.8 -> 502.8), 평균 처리 속도(17887 -> 1683)의 엄청난 성능의 향상을 보여준다.

 

ElasticSearch 적용해보기

 

Elasticsearch 쿼리문, 조건검색

엘라스틱서치에서 쿼리를 날리는 방법은 여러가지 방법이 있다. 1. 쿼리 메소드 Jpa 레포지토리에서 형식에 맞게 메소드 이름을 작성하면 알아서 쿼리를 만들어서 날려준것처럼 엘라스틱 서치도

aha2246.tistory.com

 

ElasticSearch를 간단하게 만들어본김에 차이가 있을까 테스트를 해보았다.

확실히 풀텍스트서치가 굳이 필요없는 프로젝트이기 때문에 성능차이를 찾아볼 수 없다.

향후 언젠가 풀텍스트 서치가 필요하게 된다면 검색기능 속도의 확실한 향상 혹은, score를 이용한 관련성에 따른 검색결과를 고민해 볼 수 있을 것 같다.