Spring Boot에서 Elasticsearch 필드명을 Snake Case로 매핑하기

Spring Boot에서 Elasticsearch 필드명을 Snake Case로 매핑하기

Spring Boot에서 Elasticsearch 필드명을 Snake Case로 매핑하기


들어가며

Spring Boot 환경에서 Elasticsearch를 사용할 때, Java 엔티티 클래스의 필드명이 자동으로 Elasticsearch 인덱스에 매핑되는 경우가 있습니다. 이때 Java의 camelCase 명명 규칙(예: modelName, sessionId)이 그대로 Elasticsearch 인덱스의 필드명으로 사용되어, 원하는 snake_case 형식(예: model_name, session_id)으로 매핑되지 않는 문제가 발생할 수 있습니다.

이 글에서는 이러한 문제를 해결하기 위한 다양한 방법과 각 방법의 장단점, 그리고 실제 적용 시 주의해야 할 점들을 상세히 알아보겠습니다.


1. @Field 어노테이션을 사용한 명시적 매핑

가장 직관적이고 명시적인 방법은 Spring Data Elasticsearch에서 제공하는 @Field 어노테이션을 사용하는 것입니다.

java
import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; @Document(indexName = "products") public class Product { @Field(name = "product_id", type = FieldType.Keyword) private String productId; @Field(name = "product_name", type = FieldType.Text) private String productName; @Field(name = "created_at", type = FieldType.Date) private LocalDateTime createdAt; @Field(name = "description", type = FieldType.Text, analyzer = "standard") private String description; // getters and setters }

장점

  • 필드별로 세밀한 제어가 가능
  • 명시적이고 직관적인 매핑
  • 타입과 분석기 등 추가 설정 가능

단점

  • 모든 필드에 어노테이션을 추가해야 하는 번거로움
  • 코드가 다소 장황해질 수 있음

2. Jackson 어노테이션을 사용한 클래스 레벨 매핑

전체 클래스의 필드명에 대해 일괄적으로 snake_case 변환을 적용하고 싶다면, Jackson의 @JsonNaming 어노테이션을 사용할 수 있습니다.

java
import com.fasterxml.jackson.databind.PropertyNaming; import com.fasterxml.jackson.databind.annotation.JsonNaming; @Document(indexName = "products") @JsonNaming(PropertyNaming.SnakeCaseStrategy.class) public class Product { private String productId; // product_id로 매핑 private String productName; // product_name으로 매핑 private LocalDateTime createdAt; // created_at으로 매핑 private String description; // description으로 매핑 // getters and setters }

장점

  • 코드가 간결
  • 클래스 전체에 일관된 네이밍 규칙 적용
  • 추가 설정 없이 JSON 직렬화/역직렬화에도 동일한 규칙 적용

단점

  • 필드별로 세밀한 제어가 어려움
  • 특정 필드만 다른 네이밍 규칙을 적용하기 어려움

3. 전역 설정을 통한 매핑

애플리케이션 전체에 걸쳐 일관된 네이밍 규칙을 적용하고 싶다면, Elasticsearch 설정 클래스를 통해 전역 설정을 할 수 있습니다.

java
@Configuration public class ElasticsearchConfig extends AbstractElasticsearchConfiguration { @Override public RestHighLevelClient elasticsearchClient() { ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("localhost:9200") .build(); return RestClients.create(clientConfiguration).rest(); } @Bean @Override public EntityMapper entityMapper() { ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper( elasticsearchMappingContext(), new DefaultConversionService() ); entityMapper.setObjectMapper(objectMapper()); return entityMapper; } @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); mapper.registerModule(new JavaTimeModule()); return mapper; } }

장점

  • 애플리케이션 전체에 일관된 규칙 적용
  • 설정의 중앙화
  • 코드 중복 최소화

단점

  • 유연성이 떨어짐
  • 특정 도메인/엔티티만 다른 규칙을 적용하기 어려움

실제 적용 시 주의사항

1. 인덱스 재생성

필드명 매핑을 변경할 때는 기존 인덱스를 삭제하고 재생성해야 합니다. 다음은 인덱스를 재생성하는 유틸리티 메서드의 예시입니다.

java
@Service public class IndexManagementService { private final ElasticsearchRestTemplate elasticsearchTemplate; public IndexManagementService(ElasticsearchRestTemplate elasticsearchTemplate) { this.elasticsearchTemplate = elasticsearchTemplate; } public void recreateIndex(Class<?> entityClass) { // 기존 인덱스 삭제 if (elasticsearchTemplate.indexExists(entityClass)) { elasticsearchTemplate.deleteIndex(entityClass); } // 새 인덱스 생성 elasticsearchTemplate.createIndex(entityClass); // 매핑 정보 적용 elasticsearchTemplate.putMapping(entityClass); } public void verifyMapping(Class<?> entityClass) { Map<String, Object> mapping = elasticsearchTemplate.getMapping(entityClass); System.out.println("Current mapping: " + mapping); } }


2. 데이터 마이그레이션

필드명이 변경되는 경우, 기존 데이터를 새로운 매핑에 맞게 마이그레이션해야 할 수 있습니다. 다음은 간단한 마이그레이션 예시입니다.

java
@Service public class DataMigrationService { private final ElasticsearchRestTemplate elasticsearchTemplate; public DataMigrationService(ElasticsearchRestTemplate elasticsearchTemplate) { this.elasticsearchTemplate = elasticsearchTemplate; } public void migrateData(String oldIndex, String newIndex) { // 스크롤 API를 사용하여 대량의 데이터 처리 SearchScrollHits<Map> scrollHits = elasticsearchTemplate.searchScrollStart(1000, new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchAllQuery()) .build(), Map.class, oldIndex ); while (scrollHits.hasSearchHits()) { List<Map> documents = scrollHits.getSearchHits() .stream() .map(SearchHit::getContent) .collect(Collectors.toList()); // 새 인덱스에 데이터 벌크 삽입 elasticsearchTemplate.bulkIndex(documents, newIndex); // 다음 배치 가져오기 scrollHits = elasticsearchTemplate.searchScrollContinue(scrollHits.getScrollId(), 1000, Map.class, oldIndex ); } } }


3. 성능 고려사항

필드명 매핑 변경 시 고려해야 할 성능 관련 사항들:

  • 인덱스 크기가 큰 경우, 재인덱싱에 상당한 시간이 소요될 수 있음
  • 마이그레이션 중 서비스 중단을 최소화하기 위한 전략 필요
  • 별칭(Alias)을 사용하여 무중단 마이그레이션 구현 가능
java
@Service public class ZeroDowntimeMigrationService { private final ElasticsearchRestTemplate elasticsearchTemplate; public void performZeroDowntimeMigration(String indexName, Class<?> entityClass) { // 임시 인덱스 생성 String tempIndex = indexName + "_v2"; // 새 매핑으로 임시 인덱스 생성 elasticsearchTemplate.createIndex(tempIndex); elasticsearchTemplate.putMapping(entityClass); // 데이터 마이그레이션 migrateData(indexName, tempIndex); // 별칭 전환 updateAlias(indexName, tempIndex); } private void updateAlias(String aliasName, String newIndex) { IndicesAliasesRequest request = new IndicesAliasesRequest(); // 기존 별칭 제거 request.addAliasAction( new IndicesAliasesRequest.AliasActions(IndicesAliasesRequest.AliasActions.Type.REMOVE) .index("*") .alias(aliasName) ); // 새 별칭 추가 request.addAliasAction( new IndicesAliasesRequest.AliasActions(IndicesAliasesRequest.AliasActions.Type.ADD) .index(newIndex) .alias(aliasName) ); // 별칭 업데이트 실행 elasticsearchTemplate.getClient() .indices() .updateAliases(request, RequestOptions.DEFAULT); } }

권장 사항

  1. 명시적 매핑 선호
    • 가능한 한 @Field 어노테이션을 사용하여 명시적으로 매핑을 정의
    • 문서화와 유지보수성 측면에서 유리
  2. 일관된 네이밍 규칙
    • 프로젝트 전체에서 일관된 네이밍 규칙을 사용
    • 가능하면 표준 컨벤션(예: snake_case)을 따르기
  3. 테스트 환경에서 검증
    • 매핑 변경 전 테스트 환경에서 충분한 검증
    • 마이그레이션 절차 리허설 수행
  4. 백업 및 롤백 계획
    • 매핑 변경 전 데이터 백업
    • 문제 발생 시 롤백 절차 준비

결론

Spring Boot와 Elasticsearch를 함께 사용할 때 필드명 매핑은 중요한 고려사항입니다. 위에서 설명한 여러 방법들 중에서 프로젝트의 요구사항과 상황에 맞는 최적의 방법을 선택하시기 바랍니다. 특히 프로덕션 환경에서는 데이터 안정성과 서비스 가용성을 최우선으로 고려하여 신중하게 접근해야 합니다.

위 내용들을 잘 활용하시면 Spring Boot와 Elasticsearch 환경에서 원하는 필드명 매핑을 효과적으로 구현하실 수 있을 것입니다. 

Comments