@Transactional(readOnly = true)
public Integer findBySearchJpa(String searchWord,Pageable pageable){
try {
List<Object[]> result = novelTestRepository.findNovelsBy(searchWord, pageable);
return result.size();
} catch (Exception ex) {
throw new RepositoryMethodException("메서드 에러" + ex + ex.getMessage());
}
}
2). Navtive Qeury 코드
Native Query EntityManager를 사용해 SQL을 직접 실행하고, MySQL의 MATCH ... AGAINST 구문으로 Full Text Index를 활용했습니다. - Full Text Index를 사용하는 방식으로 JPA 보다 조회 성능을 향상시켰습니다.
JOIN 조건과 반환되는 레코드는 JPA 코드와 동일하게 유지하였습니다.
@Transactional(readOnly = true)
public Integer findBySearchWordNative(String searchWord, Pageable pageable) {
int pageSize = pageable.getPageSize();// 한 페이지에 표시할 항목 수
int pageNumber = pageable.getPageNumber();//페이지 번호 변수에 할당
int offset = pageNumber * pageSize; // 오프셋 계산, 변수에 할당
//Native 쿼리문 생성, MySQL 문법 사용
String queryStr =
"SELECT n.id as novelId, " +
"n.title as novelTitle," +
"nm.total_views as totalViews, " +
"m.provider_id as providerId " +
"FROM novel n " +
"LEFT JOIN novel_meta_data nm ON n.id = nm.novel_id " +
"JOIN member m ON m.id = n.member_id " +
"WHERE MATCH(n.title) AGAINST(:searchWord IN BOOLEAN MODE) " +//index 사용
"LIMIT :limit OFFSET :offset";//페이지네이션
try {
//네이티브 쿼리 객체 생성
Query query = entityManager.createNativeQuery(queryStr);
//매개변수화된 쿼리, 쿼리문에 파라미터 할당
query.setParameter("searchWord", searchWord);
query.setParameter("offset", offset);
query.setParameter("limit", pageSize);
//쿼리문 실행후 반환된 결과 객체 생성
List<Object[]> resultList = query.getResultList();
//결과 배열 Size 반환
return resultList.size();
} catch (Exception ex) {
throw new RepositoryMethodException("메서드 에러" + ex + ex.getMessage());
}
}
테스트를 위한 전체 class 코드는 다음과 같습니다.
import com.ham.netnovel.common.exception.RepositoryMethodException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Repository
public class NovelSearchTest {
private final EntityManager entityManager;
private final NovelTestRepository novelTestRepository;
public NovelSearchTest(EntityManager entityManager, NovelTestRepository novelTestRepository) {
this.entityManager = entityManager;
this.novelTestRepository = novelTestRepository;
}
@Transactional(readOnly = true)
public Integer findBySearchWordNative(String searchWord, Pageable pageable) {
int pageSize = pageable.getPageSize();// 한 페이지에 표시할 항목 수
int pageNumber = pageable.getPageNumber();//페이지 번호 변수에 할당
int offset = pageNumber * pageSize; // 오프셋 계산, 변수에 할당
//Native 쿼리문 생성, MySQL 문법 사용
String queryStr =
"SELECT n.id as novelId, " +
"n.title as novelTitle," +
"nm.total_views as totalViews, " +
"m.provider_id as providerId " +
"FROM novel n " +
"LEFT JOIN novel_meta_data nm ON n.id = nm.novel_id " +
"JOIN member m ON m.id = n.member_id " +
"WHERE MATCH(n.title) AGAINST(:searchWord IN BOOLEAN MODE) " +//index 사용
"LIMIT :limit OFFSET :offset";//페이지네이션
try {
//네이티브 쿼리 객체 생성
Query query = entityManager.createNativeQuery(queryStr);
//매개변수화된 쿼리, 쿼리문에 파라미터 할당
query.setParameter("searchWord", searchWord);
query.setParameter("offset", offset);
query.setParameter("limit", pageSize);
//쿼리문 실행후 반환된 결과 객체 생성
List<Object[]> resultList = query.getResultList();
//결과 배열 Size 반환
return resultList.size();
} catch (Exception ex) {
throw new RepositoryMethodException("메서드 에러" + ex + ex.getMessage());
}
}
@Transactional(readOnly = true)
public Integer findBySearchJpa(String searchWord,Pageable pageable){
try {
List<Object[]> result = novelTestRepository.findNovelsBy(searchWord, pageable);
return result.size();
} catch (Exception ex) {
throw new RepositoryMethodException("findBySearchWord 메서드 에러" + ex + ex.getMessage());
}
}
}
2. 성능 비교 테스트
1). 테스트 클래스 작성
정확한 비교를 위해, 테스트는 동일한 검색어로 10,000번 반복 검색하여 평균 응답 시간 을 측정하였습니다.
정확한 측정을 위해 나노초(ns) 단위로 측정하였으며, 밀리초(ms)로 변환하여 비교하였습니다.
각 메서드가 반환한 레코드 수가 일치하는지 검증하여 정확성을 확보하였습니다.
Full Text Index에서 가장 많은 레코드에서 포함된(409개) 단어
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
class NovelTest {
private final NovelSearchTest novelSearchTest;
@Autowired
NovelTest(NovelSearchTest novelSearchTest) {
this.novelSearchTest = novelSearchTest;
}
@Test
@Transactional
public void testQueryPerformanceNative() {
Pageable pageRequest = PageRequest.of(0, 1000);//페이지네이션 정보 설정
long totalExecutionTime = 0;//실행시간 초기화
int iterations = 10000;//반복횟수 설정
for (int i = 0; i < iterations; i++) {
long startTime = System.nanoTime();//시작시간, ns단위 설정
Integer result = novelSearchTest.findBySearchWordNative("마음의소리", pageRequest);
long endTime = System.nanoTime();// 완료시간, ns 단위 설정
totalExecutionTime += (endTime - startTime);//실행시간 계산 ns 단위
System.out.println(result);//찾아온 레코드 수 출력
}
System.out.println("Average execution time for Native Query: " + (totalExecutionTime / iterations) + " ns");
}
@Test
@Transactional
public void testQueryPerformanceJpa() {
Pageable pageRequest = PageRequest.of(0, 1000);//페이지네이션 정보 설정
long totalExecutionTime = 0;//실행시간 초기화
int iterations = 10000;//반복횟수 설정
for (int i = 0; i < iterations; i++) {
long startTime = System.nanoTime();//시작시간, ns단위 설정
Integer result = novelSearchTest.findBySearchJpa("마음의소리", pageRequest);
long endTime = System.nanoTime();// 완료시간, ns 단위 설정
totalExecutionTime += (endTime - startTime);//실행시간 계산 ns 단위
System.out.println(result);//찾아온 레코드 수 출력
}
System.out.println("Average execution time for JPA Query: " + (totalExecutionTime / iterations) + " ns");
}
}
2) 검색어 선정
검색어는 다양한 조건과 상황에서 검색 성능을 평가할 수 있도록 여러 기준을 적용하여 선정했습니다. 아래는 각 검색어를 선정한 이유와 해당 단어의 특성입니다.
묘약
정렬 기준: 성능 테스트에서는 검색 결과가 내림차순으로 정렬되므로, 내림차순에서 중간쯤인 'ㅁ'을 기준으로 선택했습니다.(영어 숫자 포함)
단일 결과 레코드: Full Text Index 내에서 해당 단어를 포함한 레코드는 단 1개로, 단일 결과 레코드 를 대상으로 성능을 평가하는 데 적합합니다. 이를 통해 소수의 레코드가 포함된 경우, 인덱스가 성능에 미치는 영향을 테스트할 수 있었습니다.
마음의소리
긴 문자열 테스트: 긴 문자열 검색의 성능을 평가하기 위해 선택했습니다.여러 단어로 구성된 검색어는 더 복잡한 쿼리 조건을 만들어내며, 검색 최적화 여부가 성능에 얼마나 영향을 미치는지 평가할 수 있습니다.
사랑
대량 레코드 검색: Full Text Index에서 가장 많은 레코드(409개) 에서 포함된 단어로, 데이터베이스에서 대량의 결과를 반환하는 조건을 테스트하기에 적합합니다. 대량의 레코드를 대상으로 검색할 때 JPA와 Native Query 간 성능 차이를 비교하기 위한 용도로 사용했습니다.
3) 테스트 결과
테스트 결과, 단일 레코드 검색 시 Native Query는 JPA보다 약 90% 이상 빠른 응답 시간을 기록했습니다.
다수의 레코드 조회에서도 Native Query는 JPA보다 약 60% 더 빠르게 응답했습니다. 이는 MySQL Full Text Index를 통한 검색 최적화가 성능 향상에 큰 기여를 했기 때문입니다.
3. 결론
이번 테스트를 통해, MySQL의 Full Text Index를 사용한 Native Query가 대용량 데이터베이스에서 JPA Query보다 훨씬 효율적임을 확인할 수 있었습니다.
특히, 대량 데이터 조회나 복잡한 검색 조건이 필요한 경우에는 Native Query를 통한 최적화로 드라마틱한 성능 향상을 얻을수 있음을 알게되었씁니다.
하지만 코드의 유지보수성과 Spring Data JPA의 이점을 고려할 때, PK, 숫자등을 이용한 검색, 기본적인 CRUD 작업에는 Spring Data JPA를 적용하는게 유리합니다.