두 방법의 차이와 성능 차이를 비교하면서 내가 어떤 방법을 택했는지 보자
먼저 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);
}
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개중) 만들어보았다.
@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=?
@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
없음 !!