본문 바로가기
개발 언어/Java, Javascript

Spring Boot + Elasticsearch로 구축하는 고성능 검색

by 주호파파 2025. 5. 28.
728x90
반응형

서론

오늘날처럼 데이터가 폭발적으로 증가하는 시대에는 방대한 양의 데이터에서 필요한 정보를 효율적으로 검색하는 것이 핵심적인 요구사항이 되었습니다.

 

Elasticsearch는 분산형, 고가용성, 고성능 등의 특징을 갖춘 강력한 오픈소스 검색 엔진으로, 대규모 데이터의 전문 검색을 빠르게 처리할 수 있습니다.

 

반면 Spring Boot는 간결한 개발 방식과 풍부한 생태계를 통해 개발자가 애플리케이션을 신속하게 구축할 수 있도록 돕습니다.

본문에서는 Spring Boot 프로젝트에 Elasticsearch를 통합하여 전문 인덱싱 및 쿼리 기능을 구현하는 방법을 상세히 소개합니다.


1. Elasticsearch 핵심 메커니즘 분석

1.1. 역색인: 속도의 근원

Elasticsearch는 역색인 구조를 채택하여 Term와 문서 ID의 매핑을 통해 고속 검색을 구현합니다.

문서 1: "Spring Boot Elasticsearch 통합 실전"
문서 2: "고성능 검색 아키텍처 설계"

역색인: Spring -> [1] Boot -> [1] Elasticsearch -> [1] 검색 -> [2] 아키텍처 -> [2]

기존 데이터베이스의 B+ 트리 인덱스와 비교하여 역색인은 퍼지(fuzzy) 검색 성능을 100배 이상 향상시킵니다.

1.2. 분산 아키텍처 설계

  • 샤드(Shard): 인덱스 데이터를 수평으로 분할하여 병렬 처리를 지원합니다 (기본 5개의 주 샤드).
  • 복제본(Replica): 각 샤드는 1개의 복제본을 가지며, 고가용성과 로드 밸런싱을 보장합니다.
  • 준실시간(NRT: Near Real-time): 쓰기 작업 후 1초 이내에 검색 가능하며, _refresh 인터페이스를 통해 강제로 새로 고칠 수 있습니다.

2. Spring Boot 통합 실전

2.1. 환경 준비 및 의존성 설정

권장 버전 조합:

  • Spring Boot 2.7.x
  • Elasticsearch 7.17.x (호환성 좋음)
  • IK Analyzer 7.17.0
XML
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
    <groupId>com.github.yangtu222</groupId>
    <artifactId>ik-analyzer</artifactId>
    <version>7.17.0</version>
</dependency>

application.properties에 Elasticsearch 연결 설정을 추가합니다.

Properties
 
spring.elasticsearch.rest.uris=http://localhost:9200

보안 Elasticsearch 클러스터를 사용하는 경우 사용자 이름과 비밀번호도 설정해야 합니다.

Properties
 
spring.elasticsearch.rest.username=your-username
spring.elasticsearch.rest.password=your-password

2.2. 엔티티 클래스 매핑

Java
 
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "articles")
public class Article {
    @Id
    private String id;
    @Field(type = FieldType.Text)
    private String title;
    @Field(type = FieldType.Text)
    private String content;

    // 생성자, Getter 및 Setter 메서드
    public Article() {}

    public Article(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

위 코드에서 @Document 어노테이션은 이 엔티티 클래스가 articles라는 Elasticsearch 인덱스에 해당함을 지정합니다. @Id 어노테이션은 문서의 고유 식별자를 나타냅니다. @Field 어노테이션은 필드의 타입을 지정하는 데 사용되며, 여기서는 FieldType.Text를 사용하여 해당 필드가 텍스트 타입이며 전문 검색에 적합함을 나타냅니다.

2.3. Repository 인터페이스 생성

Article 엔티티에 대한 CRUD 작업 및 쿼리를 위해 ElasticsearchRepository를 상속하는 인터페이스를 생성합니다.

Java
 
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ArticleRepository extends ElasticsearchRepository<Article, String> {}

2.4. 전문 인덱스 쿼리 구현

Service 레이어에서 데이터를 삽입하는 메서드를 구현합니다.

Java
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ArticleService {
    @Autowired
    private ArticleRepository articleRepository;

    public Article saveArticle(Article article) {
        return articleRepository.save(article);
    }

    public List<Article> saveArticles(List<Article> articles) {
        return articleRepository.saveAll(articles);
    }
}

QueryBuilder를 사용하여 쿼리 조건을 구성하는 전문 검색 메서드를 구현합니다.

Java
 
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class ArticleSearchService {
    @Autowired
    private ElasticsearchOperations elasticsearchOperations;

    public List<Article> searchArticles(String keyword) {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                .build();
        SearchHits<Article> searchHits = elasticsearchOperations.search(searchQuery, Article.class);
        return searchHits.getSearchHits().stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
}

위 코드에서는 MultiMatchQuery를 사용하여 title 및 content 필드에 대한 전문 검색을 수행합니다.

2.5. 컨트롤러 레이어

Service 레이어의 메서드를 RESTful API로 노출하는 컨트롤러를 생성합니다.

Java
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/articles")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
    @Autowired
    private ArticleSearchService articleSearchService;

    @PostMapping
    public Article saveArticle(@RequestBody Article article) {
        return articleService.saveArticle(article);
    }

    @PostMapping("/batch")
    public List<Article> saveArticles(@RequestBody List<Article> articles) {
        return articleService.saveArticles(articles);
    }

    @GetMapping("/search")
    public List<Article> searchArticles(@RequestParam String keyword) {
        return articleSearchService.searchArticles(keyword);
    }
}

3. 테스트

Spring Boot 애플리케이션을 시작하고 Postman 또는 다른 도구를 사용하여 테스트합니다.

3.1. 데이터 삽입

http://localhost:8080/articles로 POST 요청을 보내고, 요청 본문에 JSON 형식의 Article 객체를 포함합니다.

JSON
 
{
    "title": "Spring Boot Elasticsearch 통합",
    "content": "이 문서는 Spring Boot가 Elasticsearch를 통합하여 전문 인덱싱 쿼리를 구현하는 방법을 상세히 설명합니다."
}

3.2. 전문 검색

http://localhost:8080/articles/search?keyword=Elasticsearch로 GET 요청을 보내면 Elasticsearch 키워드를 포함하는 문서 목록을 쿼리할 수 있습니다.


4. 데이터 동기화 방안 비교

4.1. 동기화 전략 선택

방안처리량지연복잡도적용 시나리오
양방향 쓰기 동기화 낮음 낮음 낮음 소규모 단일 앱
메시지 큐 비동기 높음 중간 중간 마이크로서비스
Canal binlog 리스닝 매우 높음 낮음 높음 대규모 분산 시스템
Sheets로 내보내기

4.2. 메시지 큐 비동기 동기화 예시

Java
 
// 상품 서비스에서 메시지 발행
@PostMapping("/product")
public Product createProduct(@RequestBody Product product) {
    productService.save(product);
    rabbitTemplate.convertAndSend("product-exchange",
            "product.create", product.getId());
    return product;
}
Java
 
// 검색 서비스에서 메시지 수신 처리
@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange(name = "product-exchange"),
    value = @Queue(name = "product.queue"),
    key = "product.*"))
public void syncProduct(Long productId) {
    Product product = productService.getById(productId);
    elasticsearchRepository.save(product);
}

5. 고급 검색 기능 구현

5.1. 다중 필드 가중치 검색

Java
 
public Page<Article> search(String keyword, int page, int size) {
    NativeSearchQuery query = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.multiMatchQuery(keyword)
            .field("title", 2.0f) // 제목 가중치 2배
            .field("content")
            .type(MultiMatchQueryBuilder.Type.BEST_FIELDS))
        .withPageable(PageRequest.of(page, size))
        .build();

    return elasticsearchOperations.search(query, Article.class);
}

5.2. 검색 제안 (Completion Suggester)

Java
 
@GetMapping("/suggest")
public List<String> suggest(@RequestParam String prefix) {
    CompletionSuggestionBuilder suggestion = SuggestBuilders
        .completionSuggestion("title.suggest")
        .prefix(prefix)
        .skipDuplicates(true);

    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
        .suggest(new SuggestBuilder().addSuggestion("title-suggest", suggestion));

    SearchResponse response = restHighLevelClient.search(
        new SearchRequest("articles").source(sourceBuilder),
        RequestOptions.DEFAULT);

    return response.getSuggest()
        .getSuggestion("title-suggest")
        .getEntries().stream()
        .flatMap(e -> e.getOptions().stream())
        .map(Suggest.Suggestion.Entry.Option::getText)
        .collect(Collectors.toList());
}

6. 성능 튜닝 방안

6.1. 인덱스 최적화 전략

JSON
 
PUT /articles/_settings
{
  "index": {
    "refresh_interval": "30s", // 새로 고침 빈도 감소
    "translog.durability": "async",
    "number_of_replicas": 1
  }
}

6.2. 쿼리 성능 최적화

  • 캐싱 전략: 요청 캐시 활성화 ?request_cache=true
  • 페이지네이션 최적화: from+size 대신 search_after 사용 (깊은 페이지네이션 시나리오)
  • 필드 필터링: _source를 지정하여 반환 필드 필터링
728x90
반응형