[Spring] ElasticSearch 과 QueryDSL 를 사용한 검색 성능 비교 테스트

Kyungmin·2024년 4월 25일
0

Spring

목록 보기
15/39

게시물 내용(contents)으로 검색기능을 구현하게 되었는데, 그냥 ES 를 사용하여 검색기능을 구현해야할지 아니면 QueryDSL 을 사용하여 동적으로 원하는 contents 를 가져올지 고민을 했었다.

두 방법의 차이와 성능 차이를 비교하면서 내가 어떤 방법을 택했는지 보자

1. ElasticSearch + QueryDSL

먼저 ElasticSearch + QueryDSL 을 사용하여 구현하였을 때의 흐름은 다음과 같다.
1. 클라이언트가 입력한 contents 에 해당하는 게시물을 es 에서 검색, 이때 es 에서 findByContentsContaining 메서드는 contents 를 포함한 모든 게시물의 정보를 PostSearch 객체 리스트로 반환
2. 검색 결과로 얻은 PostSearch 객체들에서 게시물의 ID(postId) 만 추출, 이 ID 들은 게시물의 실제 DB 에서 사용될 기준
3. 게시물의 ID(postId) 를 사용하여 QueryDSL 을 통해 DB 에서 실제 게시물 데이터를 조회 - 페이징 처리도 같이 처리
4. DTO 형태로 변환하여 반환

1. Query DSL method

@Override
    public Slice<PostDto.ResponseDto> searchAndFilterPosts(List<Long> postIds, Pageable pageable) {
        QPost post = QPost.post;
        QUser user = QUser.user;

        List<Post> posts = queryFactory
                .selectFrom(post)
                .leftJoin(post.user, user).fetchJoin()
                .where(post.id.in(postIds))
                .orderBy(post.createdAt.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize() + 1)
                .fetch();

        boolean hasNext = posts.size() > pageable.getPageSize();
        if (hasNext) {
            posts.remove(posts.size() - 1);
        }

        List<PostDto.ResponseDto> responseDtos = convertPostDto(posts);
        return new SliceImpl<>(responseDtos, pageable, hasNext);
    }

2. 서비스 로직

    public Slice<PostDto.ResponseDto> searchAndFilterPosts(String contents, Pageable pageable) {
        List<PostSearch> searchResults = postSearchRepository.findByContentsContaining(contents);

        List<Long> postIds = searchResults.stream()
                .map(PostSearch::getPostId)
                .collect(Collectors.toList());

        return postRepository.searchAndFilterPosts(postIds, pageable);
    }

2. ElasticSearch 으로만 검색 구현

 public Slice<PostDto.ResponseDto> searchAndFilterPosts(String contents, Pageable pageable) {

        Page<PostSearch> searchResultsPage = postSearchRepository.findByContentsContaining(contents, pageable);

        List<PostDto.ResponseDto> responseDtos = searchResultsPage.getContent().stream()
                .map(this::convertToResponseDtoFromES)
                .collect(Collectors.toList());
        
        return new SliceImpl<>(responseDtos, pageable, searchResultsPage.hasNext());
    }

➡️ 한눈에 딱봐도 ES 를 사용하여 구현하는 것이 덜 복잡하고 성능적으로도 우수할 것 같다는 생각이 들었다. 근데 그렇다면 왜 ElasticSearch + QueryDSL 을 사용하여 굳이 구현을 하는 방법이 있을까?

1. 정밀한 검색과 복잡한 쿼리: ElasticSearch는 전문 검색 엔진으로, 대량의 데이터에서 빠르게 텍스트 기반 검색을 수행할 수 있습니다. 그러나 특정 데이터 모델이나 도메인 특화 로직을 구현할 때는 SQL 기반의 쿼리 언어인 QueryDSL이 더 유연하고 표현력이 뛰어날 수 있습니다. 예를 들어, 관계형 데이터베이스의 복잡한 조인, 하위 쿼리, 그룹화 등을 수행해야 하는 경우 QueryDSL을 사용하는 것이 더 적합할 수 있습니다.

2. 데이터 무결성과 트랜잭션 관리: ElasticSearch는 기본적으로 비관계형 검색 엔진이므로, 복잡한 트랜잭션 처리나 엄격한 데이터 무결성 요구사항을 충족시키기 어려울 수 있습니다. 이러한 경우, ElasticSearch로 빠르게 데이터를 검색하고, QueryDSL(JPA)를 사용하여 데이터를 관리하고 트랜잭션을 처리하는 방식이 효과적일 수 있습니다.

3. 데이터 동기화와 일관성: 데이터가 자주 업데이트되고, 검색 엔진과 관계형 데이터베이스 간의 동기화가 중요한 비즈니스 요구사항인 경우, QueryDSL을 사용하여 데이터베이스의 변경을 관리하고 ElasticSearch는 단순히 이러한 변경을 반영하는 인덱스로 활용될 수 있습니다. 이를 통해 데이터 일관성을 유지하면서도 빠른 검색 기능을 활용할 수 있습니다.

위와 이유가 있다고 하는데, 나의 가장 중요한 목표는 역시나 성능이었다. 때문에 둘의 속도를 비교하는 테스트코드를 작성해서 속도를 비교해보았다.

나는 테스트를 위해 "user" 로 시작하는 데이터를 9개(10개중) 만들어보았다.

✅ ElasticSearch + QueryDSL 을 사용하였을 때

  • ⏰ 298 ms
@Test
    void searchElasticAndDSL() {
        String contents = "user";
        int page = 0;
        int size = 10;
        Pageable pageable = PageRequest.of(page, size);
        
        long startTime1 = System.currentTimeMillis();
        Slice<PostDto.ResponseDto> results = postService.searchAndFilterPosts(contents, pageable);
        for (PostDto.ResponseDto result : results) {
            System.out.println("result.getPostId() : " + result.getPostId());
        }
        long endTime1 = System.currentTimeMillis();

        System.out.println("메소드 실행시간 : " + (endTime1 - startTime1) + " ms");
    }

✅ query

< es + jpa  쿼리 > 
Hibernate: 
    /* select
        post 
    from
        Post post   
    left join
        
    fetch
        post.user as user 
    where
        post.id = ?1 
    order by
        post.createdAt desc */ select
            p1_0.id,
            p1_0.category,
            p1_0.contents,
            p1_0.created_at,
            p1_0.modified_at,
            p1_0.district,
            p1_0.latitude,
            p1_0.longitude,
            p1_0.place_addr,
            p1_0.place_name,
            u1_0.id,
            u1_0.city,
            u1_0.district,
            u1_0.email,
            u1_0.kakao_id,
            u1_0.nickname,
            u1_0.password,
            u1_0.user_rank,
            u1_0.role 
        from
            post p1_0 
        left join
            users u1_0 
                on u1_0.id=p1_0.user_id 
        where
            p1_0.id=? 
        order by
            p1_0.created_at desc 
        limit
            ?, ?
Hibernate: 
    select
        pi1_0.id,
        pi1_0.file_name,
        pi1_0.img_url,
        pi1_0.user_id 
    from
        profile_image pi1_0 
    where
        pi1_0.user_id=?
Hibernate: 
    select
        bm1_0.user_id,
        bm1_0.id,
        bm1_0.created_at,
        bm1_0.post_id,
        bm1_0.status 
    from
        book_mark bm1_0 
    where
        bm1_0.user_id=?
Hibernate: 
    select
        i1_0.post_id,
        i1_0.id,
        i1_0.file_name,
        i1_0.img_url 
    from
        post_image i1_0 
    where
        i1_0.post_id=?
Hibernate: 
    select
        c1_0.post_id,
        c1_0.id,
        c1_0.contents,
        c1_0.created_at,
        c1_0.modified_at,
        c1_0.parent_id,
        c1_0.user_id 
    from
        comment c1_0 
    where
        c1_0.post_id=?

✅ ElasticSearch 만을 사용하였을 때

  • ⏰ 139ms
@Test
    void searchOnlyElastic() {
        String contents = "user";
        int page = 0;
        int size = 10;
        Pageable pageable = PageRequest.of(page, size);

        long startTime1 = System.currentTimeMillis();
        Slice<PostDto.ResponseDto> results = postService.searchAndFilterPosts(contents, pageable);
        for (PostDto.ResponseDto result : results) {
            System.out.println("result.getPostId() : " + result.getPostId());
        }
        long endTime1 = System.currentTimeMillis();

        System.out.println("메소드 실행시간 : " + (endTime1 - startTime1) + " ms");
    }

✅ query

없음 !!

✍️ 정리

  • 아주 적은 양의 데이터였지만 ES 를 사용하는 것이 속도가 빨랐고 무엇보다도 ES를 사용하기 때문에 쿼리문이 발생하지 않았다. 이는 Elasticsearch를 사용할 때 SQL이나 JPQL 쿼리가 실행되지 않는 이유는 Elasticsearch가 전통적인 SQL 데이터베이스 시스템이 아니기 때문이었다. Elasticsearch는 NoSQL 범주에 속하는 분산 검색 엔진으로, 데이터를 JSON 형식의 문서로 저장하고 Lucene 기반의 검색 기능을 제공한다. 데이터 저장, 검색, 분석 작업이 모두 Elasticsearch 자체적인 방식으로 처리되기 때문에, SQL 데이터베이스에서 사용하는 쿼리 언어를 사용하지 않는다.
profile
Backend Developer

0개의 댓글

관련 채용 정보