Blog

[Spring][258] 프로젝트에서 3 Layered Architecture와 Elasticsearch 연결 테스트

Category
Author
Tags
PinOnMain
1 more property

1. 접속 환경 설정

build.gradle 의존성 추가
// Elasticsearch implementation 'org.springframework.data:spring-data-elasticsearch:5.1.3'
Java
복사
application.properties 연결 주소 입력
# Elasticsearch URL 설정 spring.elastic.url=localhost:9200
Java
복사
ElasticsearchConfig.java를 통한 Elasticsearch 실제 커넥션
application.properties에서 직접 연결 할 수도 있지만 모듈화시켜서 구조를 살펴보고자 했습니다.
@Configuration public class ElasticsearchConfig extends ElasticsearchConfiguration { @Value("${spring.elastic.url}") private String elasticUrl; @Override public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() .connectedTo(elasticUrl) .build(); } }
Java
복사

2. 3 Layered 아키텍처와 Elasticsearch 연결

ElasticsearchBook.java JPA엔티티와의 충돌을 막기 위한 전용 엔티티 생성
기존 Book.java 엔티티에 @Document 어노테이션을 사용하면 JPAElasticsearch가 동시에 해당 엔티티에 접근하게 되면서 충돌 문제가 발생했었습니다. 여러 참고자료를 찾아본 결과 ~Application.java 메인 이닛 클래스에서 @Enable.. 어노테이션을 통해서 각각 스캔 대상을 분리 시킬 수도 있지만, 우선 간략한 테스트 과정을 위해서 우선 별도의 엔티티를 생성하는 것으로 대체했습니다.
@Document어노테이션으로 인덱스명(RDBMS의 테이블명과 비슷)을 입력하면 의 인덱스와 맵핑됩니다.
@Field어노테이션으로 각 필드(RDBMS의 컬럼과 비슷) 타입, 이름을 지정하게 됩니다.
Elasticsearch는 소문자로만 구성하라는 주의사항이 있었고 이름에 스네이크 컨벤션을 유지했습니다.
@Document(indexName = "book") public class ElasticsearchBook { @Id @Field(type = FieldType.Long, name = "book_id") private Long bookId; @Field(type = FieldType.Text, name = "book_author") private String bookAuthor; @Field(type = FieldType.Keyword, name = "book_status") private String bookStatus; @Field(type = FieldType.Keyword, name = "book_publish") private String bookPublish; @Field(type = FieldType.Keyword, name = "book_name") private String bookName; // 나머지 필드들에 대한 매핑 추가 }
Java
복사
Controller
기본적인 테스트를 위한 컨트롤러 작성입니다. Elasticsearch용 엔티티인 ElasticsearchBook.java 를 사용해서 어떤 결과가 나타나는지 확인하고자 했습니다.
@RequiredArgsConstructor @RequestMapping @Controller public class ElasticBookSearchController { private final ElasticBookService elasticBookService; // Elasticsearch + 무한스크롤 기능 구현 초기 페이지 진입 @GetMapping("/search/el1") public String elasticSearchResults( @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "page", defaultValue = "0", required = false) Integer page, @RequestParam(value = "size", defaultValue = "20", required = false) Integer size, Model model) { long startTime = System.currentTimeMillis(); // 실행시간 측정 // 페이지 요청 시, 현재까지의 모든 페이지를 가져오도록 수정 List<ElasticBookResponseDto> elasticBookResponseDtos = elasticBookService.searchByBookName(keyword, page, size); model.addAttribute("currentPage", page); model.addAttribute("books", elasticBookResponseDtos); model.addAttribute("hasNext", !elasticBookResponseDtos.isEmpty()); long endTime = System.currentTimeMillis(); long durationTimeSec = endTime - startTime; System.out.println(durationTimeSec + "m/s"); // 실행시간 측정 return "users/searchEL1"; } // Elasticsearch 무한 스크롤 데이터 요청 API @GetMapping("search/elastic") @ResponseBody public ResponseEntity<List<ElasticBookResponseDto>> search( @RequestParam("keyword") String book_name, @RequestParam(value = "page", defaultValue = "0", required = false) Integer page, @RequestParam(value = "size", defaultValue = "20", required = false) Integer size) { List<ElasticBookResponseDto> elasticBookResponses = elasticBookService.searchByBookName(book_name, page, size) .stream() .map(ElasticBookResponseDto::from) .collect(Collectors.toList()); return ResponseEntity.ok(elasticBookResponses); } }
Java
복사
Service
기존 JPA로 MySQL과 연결된 Repository들과 차이점을 보기위해 별도로 ElasticBookSearchRepository 로 부터 접근하도록 했습니다.
@Slf4j @RequiredArgsConstructor @Transactional(readOnly = true) @Service public class ElasticBookService { private final BookRepository bookRepository; private final ElasticBookSearchRepository elasticBookSearchRepository; public List<ElasticBookResponseDto> searchByBookName(String keyword, int page, int size) { if (keyword == null) { // 검색 키워드가 null인 경우에 대한 처리 return Collections.emptyList(); } Sort sort = Sort.by(Sort.Direction.ASC, "_id"); Pageable pageable = PageRequest.of(page, size, sort); return elasticBookSearchRepository.findByBookNameContains(keyword, pageable) .stream() .map(ElasticBookResponseDto::from) .collect(Collectors.toList()); } }
Java
복사
Repository
ElasticsearchRepository를 상속받는 인터페이스를 구성합니다. 이는 마치 이전 JPA를 사용하기 위해서 JpaRepository를 상속받았던 것과 비슷하다고 볼 수 있습니다. ElasticsearchRepository의 구현체를 정의한다고 볼 수 있습니다.
/** * 엘라스틱 서치를 사용해서 도서 검색을 위한 기본 레포지토리입니다. * */ public interface ElasticBookSearchRepository extends ElasticsearchRepository<ElasticsearchBook, Long>, ElasticCustomBookSearchRepository { List<ElasticsearchBook> findByBookNameContains(String keyword); }
Java
복사
public interface ElasticCustomBookSearchRepository { List<ElasticsearchBook> findByBookNameContains(String keyword, Pageable pageable); }
Java
복사
ElasticCustomBookSearchRepositoryImpl 을 통해서 쿼리문을 튜닝 할 수 있도록 합니다.
@Repository @RequiredArgsConstructor public class ElasticCustomBookSearchRepositoryImpl implements ElasticCustomBookSearchRepository { private final ElasticsearchOperations elasticsearchOperations; @Override public List<ElasticsearchBook> findByBookNameContains(String keyword, Pageable pageable){ Criteria criteria = Criteria.where("bookName").contains(keyword); Query query = new CriteriaQuery(criteria).setPageable(pageable); SearchHits<ElasticsearchBook> search = elasticsearchOperations.search(query, ElasticsearchBook.class); return search.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); } }
Java
복사

3. 테스트

Elasticsearch는 기본적으로 데이터를 캐싱하여 검색 성능을 최적화하는데, 이는 반복되는 동일한 검색 요청에 대해 더 빠른 응답을 제공할 수 있습니다. 이러한 캐싱은 검색 결과, 필터링, 집계 등에 적용될 수 있습니다. 기본적으로 키워드 검색에 대하여 높은 성능을 가지고 있음과 동시에 동일한 검색 요청이 누적됨에 따라 계속하여 요청 속도가 증가하는 것을 살펴 볼 수 있습니다.