Elasticsearch 입문 32편 Spring Data ES. @Document·Template·Repository·Reactive·Criteria·Auditing.
이 글은 Elasticsearch 입문에서 운영까지 시리즈 38편 중 32편이에요. 26~31편까지 cluster·shard·snapshot·security·monitoring·performance 운영 6편을 끝내고, 이제 통합 파트의 첫 글로 자바 백엔드에서 Elasticsearch 를 어떻게 깔끔하게 쓰는지 살핍니다.
이 글은 Spring Data Elasticsearch 6.0 공식 docs 와 Elasticsearch 8.x 공식 client docs 를 참고해 한국어 학습 노트로 풀어쓴 자료예요.
자바 백엔드 입문 44~50편에서 다룬 JPA 와 같은 *Repository · Template · POJO 매핑* 패턴이라 한 호흡에 따라옵니다. Spring Boot 3.5+ 환경에서 따라 치면 본문이 머리에 잘 박혀요.
왜 32편이 Spring Data Elasticsearch 인가
자바 백엔드 입문 44~50편에서 다룬 JPA 가 "RDBMS 를 자바 객체로 다룬다" 였다면, 이 글은 "Elasticsearch 를 자바 객체로 다룬다" 자리예요. Spring Data 프로젝트 의 같은 패밀리라서 — Repository · Template · POJO 매핑 · Auditing — 네 가지 추상화가 거의 똑같이 따라와요. JPA 를 한 번 써본 사람은 형태 익숙 단계에서 30%는 그냥 잡혀요.
7편(Search API) 부터 22편(Vector Search) 까지 우리는 cURL 로 JSON DSL 던지기 만 해왔어요. 운영 코드에서 그걸 그대로 쓰면 문자열 조립·타입 안 맞음·NPE 가 줄줄이 터져요. Spring Data Elasticsearch 가 이 자리를 어노테이션 + 메서드 호출 로 깔끔하게 잡아 줍니다.
이 글이 끝나면 — @Document 로 POJO 매핑 짜고, Repository 로 메서드 이름만으로 쿼리 만들고, ElasticsearchTemplate 으로 복잡한 집계도 한 메서드로 호출하고, Reactive 환경까지 함께 잡힙니다.
의존성 — Spring Data Elasticsearch 6.0.0
작성 시점(2026-05-19) 기준 최신 안정 버전은 Spring Data Elasticsearch 6.0.0 이고, Spring Boot 3.5+ 와 함께 쓰는 게 표준 조합이에요. Boot 의 dependency management 가 버전을 잡아 주니까 사용자는 starter 한 줄만 박으면 됩니다.
Gradle (Kotlin DSL) 기준:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
// Reactive 환경이면 추가
implementation("org.springframework.boot:spring-boot-starter-webflux")
}
Maven 이면 똑같이 spring-boot-starter-data-elasticsearch 한 줄. 이 starter 가 내부적으로 Elasticsearch Java Client (공식 client, 8.x 부터 low-level + high-level 통합) 와 Spring Data Commons 를 끌고 와요.
연결은 application.yml 에 호스트만 박으면 끝.
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: ${ELASTIC_PASSWORD}
socket-timeout: 5s
connection-timeout: 1s
Spring Boot 가 알아서 ElasticsearchTemplate · RestClient · Repository 자동 설정 을 다 끼워 줘요. 별도 @Configuration 클래스 없이 호스트 한 줄 만으로 동작 시작.
운영 환경에선 TLS·CA 인증서·API Key 를 함께 박아요. AWS OpenSearch Service 면 SigV4 서명 이 추가로 들어가는데, 35편(AWS OpenSearch) 에서 깊이.
| 환경 | 권장 client | 인증 | 연결 코드 |
|---|---|---|---|
| 로컬 Docker | RestClient (기본) | 없음 또는 elastic/비번 | URI 한 줄 |
| 자체 운영 (TLS) | RestClient + SSLContext | API Key | @Configuration 으로 SSLBundle |
| Elastic Cloud | RestClient | API Key | cloud-id + api-key |
| AWS OpenSearch | OpenSearch Java Client | SigV4 | AWS SDK 의존성 |
여기서부터는 로컬 ES 8.x 단일 노드 기준으로 따라가요.
@Document·@Field·@Id — POJO 어노테이션 매핑
JPA 의 @Entity · @Column · @Id 와 같은 자리에 ES 는 @Document · @Field · @Id 가 있어요. 자바 클래스 한 개가 ES 인덱스 한 개 에 대응되고, 필드는 ES 필드 로 매핑.
가장 단순한 Product 예시:
@Document(indexName = "products", createIndex = true)
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "nori_analyzer")
private String name;
@Field(type = FieldType.Keyword)
private String brand;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Date, format = DateFormat.date_optional_time)
private Instant createdAt;
@Field(type = FieldType.Nested)
private List<Tag> tags;
// getters/setters
}
@Document 는 클래스 → 인덱스 매핑이에요. 핵심 속성을 정리하면:
| 속성 | 의미 | 기본값·예시 |
|---|---|---|
indexName |
대상 인덱스 이름 | 필수 — "products" 또는 "products-#{T(java.time.LocalDate).now()}" SpEL |
createIndex |
앱 시작 시 인덱스 자동 생성 | true (단, 권장 X — 운영은 명시적으로) |
versionType |
낙관적 락 타입 | EXTERNAL |
writeTypeHint |
_class 자동 박기 |
6.0 부터 기본 false (구버전 호환 시 켜기) |
shards |
Primary Shard 수 | 1 (운영은 명시적으로) |
replicas |
Replica 수 | 1 |
@Field 는 필드 → ES 필드 매핑이에요. type 으로 ES 필드 타입을 지정하고, 분석기·multi-field·null 처리 까지 한 어노테이션 안에서 다 잡혀요. 9편(Field Types) 에서 다룬 모든 ES 타입이 FieldType enum 에 그대로 들어 있어요.
@Id 는 ES 의 _id 자리예요. JPA 와 달리 @GeneratedValue 는 없고, 값을 안 박으면 ES 가 자동으로 UUID 생성. 직접 박는 경우엔 외부 시스템의 PK (예: PG 의 product_id) 를 그대로 넣는 게 일반.
@MultiField 는 한 필드를 여러 매핑으로 색인할 때 써요. 한국어 풀텍스트 검색과 정확 매칭을 둘 다 잡고 싶을 때 자주 등장.
@MultiField(
mainField = @Field(type = FieldType.Text, analyzer = "nori_analyzer"),
otherFields = {
@InnerField(suffix = "keyword", type = FieldType.Keyword),
@InnerField(suffix = "english", type = FieldType.Text, analyzer = "english")
}
)
private String name;
→ 색인되는 필드는 name · name.keyword · name.english 셋. 검색에선 name 으로 풀텍스트, name.keyword 로 정확 매칭, name.english 로 영어 분석을 동시 호출 가능.
@Setting · @Mapping 은 인덱스 settings·mapping 을 JSON 파일로 박을 때 써요.
@Document(indexName = "products")
@Setting(settingPath = "settings/products-settings.json")
@Mapping(mappingPath = "mappings/products-mapping.json")
public class Product { ... }
→ 어노테이션만으로 못 잡는 복잡한 custom analyzer · synonyms · normalizer 는 JSON 파일에 박고 클래스패스에서 읽어요. 11편(Korean Analyzer) 의 Nori 사용자 사전이 이 패턴.
ElasticsearchOperations·ElasticsearchTemplate — sync API
JPA 의 EntityManager · JdbcTemplate 와 같은 자리예요. ElasticsearchOperations 가 인터페이스고, ElasticsearchTemplate 이 표준 구현이에요. 의존성 주입은 인터페이스로 받는 게 표준.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ElasticsearchOperations operations;
public Product save(Product product) {
return operations.save(product);
}
public Product findById(String id) {
return operations.get(id, Product.class);
}
public SearchHits<Product> searchByName(String keyword) {
Query query = NativeQuery.builder()
.withQuery(q -> q.match(m -> m.field("name").query(keyword)))
.withPageable(PageRequest.of(0, 20))
.build();
return operations.search(query, Product.class);
}
public void delete(String id) {
operations.delete(id, Product.class);
}
}
핵심 메서드를 정리하면:
| 메서드 | 자리 | 비고 |
|---|---|---|
save(T) / save(Iterable<T>) |
upsert (있으면 update, 없으면 insert) | 내부 bulk 사용 |
get(id, Class<T>) |
id 로 단건 조회 | null 가능 |
search(Query, Class<T>) |
풀텍스트·집계 검색 | SearchHits<T> 반환 |
searchForStream(...) |
큰 결과셋 스트림 | scroll 자동 |
delete(id, Class<T>) / delete(Query, Class<T>) |
단건·조건 삭제 | by query 도 가능 |
update(UpdateQuery, IndexCoordinates) |
partial update | doc / script 둘 다 |
bulk(...) |
다건 묶음 처리 | 23편(Bulk API) 와 동등 |
indexOps(Class<T>) |
인덱스 관리 핸들 | mapping·alias·delete |
반환 타입 SearchHits
SearchHits<Product> hits = operations.search(query, Product.class);
long total = hits.getTotalHits();
float maxScore = hits.getMaxScore();
for (SearchHit<Product> hit : hits) {
Product product = hit.getContent();
Map<String, List<String>> highlight = hit.getHighlightFields();
// ...
}
집계 결과는 hits.getAggregations() 로 받아요. 15~17편의 metric · bucket · pipeline 집계가 그대로 자바 코드로 호출됩니다.
ElasticsearchRepository — 메서드 이름 기반 쿼리
JPA Repository 와 완전히 같은 패턴 이에요. 인터페이스만 만들고, 메서드 이름을 find · By · And · Or · Between · OrderBy 같은 키워드로 짜면 런타임에 쿼리 자동 생성.
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByBrand(String brand);
Page<Product> findByNameContaining(String keyword, Pageable pageable);
List<Product> findByPriceBetween(Double min, Double max);
Slice<Product> findByBrandAndPriceLessThan(String brand, Double max, Pageable pageable);
long countByBrand(String brand);
void deleteByCreatedAtBefore(Instant cutoff);
}
→ 위 코드에서 XML · JSON · DSL 한 줄 없이 ES 가 호출돼요. 메서드 이름이 그대로 match·range·term 쿼리 로 자동 변환됩니다.
JPA Repository 키워드와 거의 같지만, 몇 가지 ES 고유 차이 가 있어요.
| 키워드 | JPA | Spring Data ES |
|---|---|---|
findBy<X> |
WHERE x = ? |
term 쿼리 |
findBy<X>Containing |
LIKE %?% |
match 쿼리 (분석기 적용) |
findBy<X>StartingWith |
LIKE ?% |
prefix 쿼리 |
findBy<X>Between |
BETWEEN ? AND ? |
range 쿼리 |
findBy<X>In |
IN (?, ?, ?) |
terms 쿼리 |
OrderBy<X>Desc |
ORDER BY x DESC |
sort by x desc |
복잡한 쿼리는 @Query 어노테이션으로 JSON DSL 직접 박기 도 가능.
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
@Query("""
{
"bool": {
"must": [
{ "match": { "name": "?0" } }
],
"filter": [
{ "range": { "price": { "lte": ?1 } } }
]
}
}
""")
List<Product> findByNameAndMaxPrice(String name, Double maxPrice);
}
→ JPA 의 @Query("SELECT p FROM ...") 와 완전히 같은 형태 예요. ES DSL JSON 안에서 ?0 · ?1 같은 위치 파라미터를 그대로 쓰면 됩니다.
페이지네이션은 Pageable 그대로 — PageRequest.of(0, 20, Sort.by("price").descending()) 같은 표준 Spring Data 패턴.
ReactiveElasticsearchTemplate — 반응형 API
Spring WebFlux 환경에선 sync API 대신 ReactiveElasticsearchOperations (인터페이스) / ReactiveElasticsearchTemplate (구현) 을 써요. 모든 메서드가 Mono · Flux 를 반환하는 것 빼면 이름·시그니처가 sync 와 거의 같아요.
@Service
@RequiredArgsConstructor
public class ReactiveProductService {
private final ReactiveElasticsearchOperations operations;
public Mono<Product> save(Product product) {
return operations.save(product);
}
public Mono<Product> findById(String id) {
return operations.get(id, Product.class);
}
public Flux<SearchHit<Product>> searchByName(String keyword) {
Query query = NativeQuery.builder()
.withQuery(q -> q.match(m -> m.field("name").query(keyword)))
.build();
return operations.search(query, Product.class);
}
}
Reactive Repository 도 그대로 제공돼요 — ReactiveElasticsearchRepository<T, ID> 를 상속하면 메서드 이름 기반 쿼리가 Mono · Flux 로 반환됩니다.
public interface ReactiveProductRepository extends ReactiveElasticsearchRepository<Product, String> {
Flux<Product> findByBrand(String brand);
Mono<Long> countByBrand(String brand);
}
운영에서 헷갈리는 자리는 언제 Reactive 를 쓸 것이냐 인데 — 전 구간 비동기 (Netty · Reactor · R2DBC) 가 이미 구축된 곳에선 sync ES 호출이 blocking 이라 worst case 가 됩니다. 그런 환경에서만 Reactive ES 가 의미 있고, 보통의 Tomcat + JPA 환경에선 sync 가 단순·디버깅 모두 압도적으로 편해요.
작성 시점(2026-05-19) 기준 Reactive ES 의 가장 큰 함정은 context propagation — @Auditing · SecurityContext · MDC 같은 ThreadLocal 기반 컨텍스트가 Reactor chain 을 따라가지 않아요. 해결은 Reactor Context · @ContextPropagation 어노테이션 또는 micrometer-context-propagation 라이브러리. 함정 섹션에서 다시.
Criteria API & NativeQuery — 복잡한 쿼리 빌더
메서드 이름이나 @Query 로 못 잡는 동적 쿼리 는 두 가지 빌더가 있어요.
Criteria API — 도메인-친화 빌더
Criteria criteria = new Criteria("name").contains("나이키")
.and(new Criteria("brand").is("Nike"))
.and(new Criteria("price").lessThanEqual(100_000));
CriteriaQuery query = new CriteriaQuery(criteria, PageRequest.of(0, 20));
SearchHits<Product> hits = operations.search(query, Product.class);
JPA Criteria API 와 같은 체이닝 형태예요. and · or · not · is · contains · between · greaterThan 같은 메서드로 bool 쿼리 가 자동 조립됩니다. 동적 필터 (사용자가 체크한 옵션만 OR 로 묶기) 자리에 가장 어울려요.
NativeQuery — ES DSL 그대로 빌더
import co.elastic.clients.elasticsearch._types.query_dsl.*;
NativeQuery query = NativeQuery.builder()
.withQuery(q -> q
.bool(b -> b
.must(m -> m.match(mm -> mm.field("name").query("나이키")))
.filter(f -> f.range(r -> r.field("price").lte(JsonData.of(100_000))))
)
)
.withAggregation("by_brand", a -> a
.terms(t -> t.field("brand").size(10))
)
.withSort(s -> s.field(f -> f.field("price").order(SortOrder.Desc)))
.withPageable(PageRequest.of(0, 20))
.build();
SearchHits<Product> hits = operations.search(query, Product.class);
ES Java Client 의 DSL 그대로 자바 람다 형태로 호출하는 빌더예요. Criteria API 로는 표현 불가한 function_score · nested · has_child · pipeline aggregation 모두 여기서 가능. 작성 시점(2026-05-19) 기준 NativeQuery 가 권장 표준 이에요. (구버전 NativeSearchQuery 는 deprecated.)
| 빌더 | 어울리는 자리 | 가독성 | 표현력 |
|---|---|---|---|
| Method name | 단순 1~2 조건 | ★★★★★ | ★★ |
| @Query (JSON) | 정적 복잡 쿼리 | ★★★ | ★★★★★ |
| Criteria API | 동적 필터 조합 | ★★★★ | ★★★ |
| NativeQuery | 모든 쿼리·집계 | ★★★ | ★★★★★ |
Index 관리 — IndexOperations 와 Auditing
운영 코드에선 인덱스 자체 를 다루는 자리도 자주 등장해요. operations.indexOps(Class<T>) 또는 operations.indexOps(IndexCoordinates.of("products")) 로 핸들을 받아요.
IndexOperations indexOps = operations.indexOps(Product.class);
// 인덱스 생성
indexOps.createWithMapping();
// 매핑 확인
Map<String, Object> mapping = indexOps.getMapping();
// alias 추가 — 5편(Index 관리) 의 alias 운영 패턴
AliasActions aliasActions = new AliasActions(
new AliasAction.Add(AliasActionParameters.builder()
.withIndices("products-2026-05")
.withAliases("products")
.build())
);
indexOps.alias(aliasActions);
// refresh — 색인 직후 강제 검색 가능 상태로
indexOps.refresh();
// 삭제
indexOps.delete();
운영 인덱스는 보통 @Document(createIndex = false) 로 잠그고, Flyway-같은 별도 마이그레이션 도구 로 인덱스를 명시적으로 만들어 둡니다. 앱이 자동 생성하면 PR 미통과 매핑 변경 이 운영에 들어가는 사고가 잦아요.
Auditing 은 JPA Auditing 과 같은 패턴이에요. @CreatedDate · @LastModifiedDate · @CreatedBy · @LastModifiedBy 가 그대로 사용 가능.
@Configuration
@EnableElasticsearchAuditing(auditorAwareRef = "auditorAware")
public class ElasticsearchConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.of(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getName);
}
}
@Document(indexName = "products")
public class Product {
@Id
private String id;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
→ save() 호출 시 ES Template 이 현재 시간 · AuditorAware 의 사용자 를 자동으로 박아 줘요. 운영 추적·감사 로그 자리에 표준.
함정은 @EnableElasticsearchAuditing 누락 — @EnableJpaAuditing 만 켜고 ES 쪽은 안 켜면 조용히 무시 됩니다. 함정 섹션에서 다시.
자주 만나는 사고 6가지
사고 1 — Client version mismatch
원인 — Spring Data Elasticsearch 6.0.0 은 Elasticsearch 8.x 서버 에 맞춰져 있어요. 서버가 7.x 면 client 가 no compatible API 오류를 뱉으며 연결 자체가 거부됩니다.
해결 — Spring Data ES 버전과 ES 서버 버전을 공식 호환 표 로 맞춰요. 6.0.x ↔ 8.13+, 5.3.x ↔ 8.11+, 5.2.x ↔ 8.7+. ES 9.x 미리보기는 작성 시점(2026-05-19) 기준 Spring Data ES 6.1.x 미리보기와만 호환.
사고 2 — Dynamic Mapping 으로 필드 타입 자동 추론 망함
원인 — @Field(type = ...) 를 안 박으면 ES 가 첫 색인되는 값 으로 타입을 자동 추론해요. "123" 같은 숫자 문자열을 text 로 잡았다가 나중에 range 쿼리가 안 도는 사고가 자주 등장.
해결 — 모든 필드에 @Field(type = ...) 명시. 인덱스도 dynamic: strict 로 잠그는 게 표준. 8편(Mapping Deep) 의 Mapping Explosion 사고와 같은 자리.
사고 3 — Date format mismatch
원인 — 자바 Instant 또는 LocalDateTime 을 그대로 박으면 직렬화 형식이 ES 매핑의 format 과 안 맞아 색인 거부 또는 문자열로 색인 되는 사고.
해결 — @Field(type = FieldType.Date, format = DateFormat.date_optional_time) 를 명시하고, Jackson 의 JSR-310 모듈 (jackson-datatype-jsr310) 이 의존성에 들어가 있는지 확인. Spring Boot 3.x 면 기본 포함이지만, 커스텀 ObjectMapper 를 쓰면 빠지기 쉬워요.
사고 4 — Reactive context propagation 끊김
원인 — Reactive ES Template 을 쓰는 환경에서 SecurityContext · MDC · @CreatedBy 가 Reactor chain 의 다른 스레드 로 가면 사라져요. AuditorAware 가 항상 null 을 반환하는 증상으로 드러나요.
해결 — Hooks.enableAutomaticContextPropagation() 호출 또는 micrometer-context-propagation 의존성 추가. Reactor Context 로 명시적으로 값을 흘려보내는 게 가장 안전.
사고 5 — @EnableElasticsearchAuditing 누락
원인 — JPA Auditing 만 켜고 ES Auditing 을 안 켜는 실수가 가장 흔해요. @CreatedDate · @LastModifiedDate 가 조용히 동작 안 함 — 예외도 안 뜨고 그냥 null 이 들어가요.
해결 — @EnableElasticsearchAuditing 을 @SpringBootApplication 클래스나 별도 @Configuration 에 명시. JPA 와 ES 가 같은 앱에 있으면 둘 다 켜야 합니다.
사고 6 — Repository 의 save() 가 매번 refresh
원인 — Spring Data ES 의 save() 가 기본적으로 RefreshPolicy.NONE 인데, 테스트 환경에서 IMMEDIATE 로 잡아 둔 채 운영으로 그대로 가면 — 매 색인마다 refresh 가 도는 사고. 처리량이 1/10 로 떨어져요.
해결 — 운영에선 RefreshPolicy.NONE 또는 WAIT_UNTIL 만 사용. IMMEDIATE 는 통합 테스트 전용 으로 명확히 분리. application.yml 의 spring.data.elasticsearch.client.reactive.refresh-policy 로 전역 설정 가능.
사고 7 — writeTypeHint 의 _class 필드가 매핑을 깨먹음
원인 — Spring Data ES 5.x 까지 기본값이 WriteTypeHint.TRUE 라 모든 문서에 _class 필드 가 자동 박혔어요. 6.0 부터 기본 FALSE 로 바뀌었는데, 5.x 인덱스를 6.0 코드로 읽으면 역직렬화 실패 사고가 등장.
해결 — 기존 5.x 인덱스를 6.0 으로 읽을 땐 @Document(writeTypeHint = WriteTypeHint.TRUE) 로 명시. 신규 인덱스는 기본 FALSE 유지.
운영 권장 패턴 5가지
운영 인덱스는 항상 alias 로 노출 합니다. @Document(indexName = "products") 에서 "products" 는 alias 고, 실제 인덱스는 "products-2026-05" 같이 날짜 suffix 가 붙은 별도 인덱스예요. 5편(Index 관리) 의 alias 운영 패턴과 그대로 연결.
@Document(createIndex = false) 로 앱 자동 생성 차단 하고, 인덱스·매핑·alias 는 별도 마이그레이션 으로 명시적으로 만들어요. Flyway-같은 도구 또는 CI/CD 스크립트 에 두는 게 표준. 37편(IaC) 에서 깊이.
Repository 와 ElasticsearchTemplate 을 역할로 분리 합니다. 단순 CRUD · 메서드 이름 쿼리 는 Repository 로, 복잡한 동적 검색 · 집계 · 함수 점수 는 Template + NativeQuery 로 — 섞어 쓰면 디버깅이 어려워요.
RefreshPolicy 는 운영 코드에서 NONE 또는 WAIT_UNTIL 만. 통합 테스트 전용으로 IMMEDIATE 를 박을 땐 @TestConfiguration 로 분리해서 프로덕션 빌드에 절대 안 새는 구조로.
Mapping 변경은 코드 배포 전에 ES 에 먼저 적용 합니다. Rolling deploy 중간에 매핑 불일치 인스턴스가 한 번이라도 트래픽을 받으면 색인 거부 + 사용자 응답 오류 가 됩니다. 매핑 → 새 인덱스 → reindex → alias 갈아끼우기 → 코드 배포 순서가 표준.
시험 직전 한 번 더 — 압축 노트
- Spring Data Elasticsearch 6.0.0 = Spring Boot 3.5+ 와 함께. JPA 와 같은 Repository · Template · POJO 패턴.
- @Document(indexName, createIndex, writeTypeHint) = 클래스 → 인덱스 매핑.
- @Field(type, analyzer, format) = 필드 → ES 필드 매핑. @MultiField 로 한 필드 → 여러 매핑.
- @Id = ES
_id. 값 안 박으면 ES 가 UUID 자동 생성. - @Setting · @Mapping = JSON 파일로 settings · mapping 박기.
- ElasticsearchOperations = 인터페이스, ElasticsearchTemplate = 구현. save · get · search · delete · update · bulk · indexOps.
- SearchHits
= 결과 + total · maxScore · aggregations · highlight 한 묶음. - ElasticsearchRepository
= 메서드 이름 기반 자동 쿼리 (JPA Repository 와 같은 패턴). - @Query("""json""") = JSON DSL 직접 박기.
?0 · ?1위치 파라미터. - ReactiveElasticsearchOperations = WebFlux 환경. Mono · Flux 반환. context propagation 함정 주의.
- Criteria API = 도메인 친화 빌더 (동적 필터). NativeQuery = ES DSL 그대로 빌더 (권장 표준).
- IndexOperations = 인덱스 자체 관리 (create · mapping · alias · refresh · delete).
- @EnableElasticsearchAuditing + AuditorAware = @CreatedDate · @LastModifiedDate · @CreatedBy · @LastModifiedBy.
- 7대 사고: client/server 버전 mismatch · 동적 매핑 타입 망함 · Date format mismatch · Reactive context 끊김 · @EnableElasticsearchAuditing 누락 · RefreshPolicy IMMEDIATE 누수 · writeTypeHint 의 _class 매핑 깨짐.
- 권장 패턴: alias 노출 · createIndex=false · Repository/Template 역할 분리 · RefreshPolicy 운영 NONE · 매핑 선배포 후 코드 배포.
시리즈 다른 편
- 이전 글 = 31편 Performance Tuning — JVM·OS·인덱스·쿼리 최적화
- 다음 글 = 33편 Kibana·ELK 스택 — Discover·Dashboard·Lens·Logstash·Beats
- 1편 = Elasticsearch 입문 종합 — 검색·로그·AI 한 통에 다 들어가는 분산 엔진
- 5편 = Index 관리 — Create·Settings·Alias·Reindex
- 8편 = Mapping Deep — Static·Dynamic·Multi-field·Runtime
- 11편 = Korean Analyzer — Nori·mecab-ko·사용자 사전
- 19편 = Search Features — search_after·scroll·highlight·rescore
- 23편 = Bulk API — 처리량 10배 늘리는 패턴
- 34편 = Observability — APM·Tracing·Profiling
- 35편 = AWS OpenSearch Service — Domain·Serverless·Provisioned
- 37편 = IaC — Terraform·CloudFormation·Helm
한 줄 정리 — Spring Data Elasticsearch = @Document POJO 매핑 · ElasticsearchTemplate sync API · Repository 메서드 쿼리 · Reactive 변형 · NativeQuery DSL 빌더 · Auditing 까지 JPA 와 같은 패턴 으로 ES 를 자바에서 깔끔하게 쓰는 표준. 6.0.0 + Spring Boot 3.5+ 이 작성 시점 표준 조합.