1. 문제 상황
인턴십을 진행하던중 레거시 시스템에서 ElasticSearch 5.x를 사용하고 있었다. 하지만 5.x는 이미 지원이 종료되어 보안 패치나 기능 개선이 불가능했다. 따라서 5.x 버전을 8.x 버전으로 마이그레이션 하는 작업이 있었고 진행하던 과정 중 사용 방식이 바뀌어 공식 문서를 찾아가며 바뀐 부분을 수정하는 작업이 필요했다.
ElasticSearch 5.x 버전에서 사용하던 코드
public void updateData(String updateQuery, String _id) throws IOException {
ElasticsearchClient client = getEsClient();
UpdateRequest updateRequest = new UpdateRequest.Builder<>()
.index(this.log_idx)
.id(_id)
.doc(updateQuery.getBytes())
.build();
UpdateResponse br = client.update(updateRequest, Map.class);
}이 코드는 Elasticsearch에서 지정한 _id를 가진 문서를 찾아, updateQuery에 포함된 필드만 기존 문서에 덮어쓰는 부분 업데이트(partial update) 를 수행하는 코드이다.
해당 코드를 포함한 API를 호출하니 x_content_parse_exception과 같은 오류가 발생하였다. 찾아보니 데이터를 파싱하다가 형식이 맞지 않으면 발생하는 예외였다.
2, 해결 방법
일단 해당 오류가 발생하는 이유를 찾았으니 해당 버전을 사용하고 있는 공식 문서를 찾아보았다. ElasticSearch 5.4.3 공식 문서

5.x 버전에서는 UpdateRequest의 doc필드가 (byte[]) 필드를 지원하긴 하지만 Deprecated라고 나와있었다. Deprecated란 아직은 쓸 수 있지만, 앞으로 버전에서 제거되지만 동작 보장이 안될 수 있다 라는 뜻이다.
실제로 마이그레이션 하려는 버전 ElasticSearch 8.13.4 공식 문서 를 참고해보면

doc(byte[] source, XContentType xContentType) 다음과 같이 byte[] 인자 외에 ContentType을 명시해주도록 메서드 형식이 바뀌었다.
8.3.14 버전과 호환되도록 XContentType.JSON 명시
// (구) 레거시 방식 — JSON 문자열 조립 → getBytes()
String json = "{ \"accident_flag\": true }";
UpdateRequest req = new UpdateRequest("verify_index", verifyUid)
.doc(json.getBytes(StandardCharsets.UTF_8), XContentType.JSON);
client.update(req, Map.class);doc 필드에 XContentType을 명시해주었지만 여전히 byte[]로 보낼 때 업데이트 쿼리를 직접 작성해야 하므로 JSON 구문 오류(쉼표 누락, 따옴표 누락 등)가 있으면 x_content_parse_exception 런타임 에러(실행 중에야 터짐)가 발생하였다.
1차 개선 - Map 기반 부분 업데이트로 전환
// 사용 예시 (Service 코드)
Map<String, Object> updateFields = new HashMap<>();
updateFields.put("accident_flag", true);
es.patchDocFields(verifyUid, updateFields, false);
public void patchDocFields(String id, Map<String, Object> fields, boolean upsert) throws IOException {
ElasticsearchClient client = getEsClient();
var req = new co.elastic.clients.elasticsearch.core.UpdateRequest.Builder<Map<String,Object>, Map<String,Object>>()
.index(this.log_idx)
.id(id)
.doc(fields) // 필요한 필드만 병합
.docAsUpsert(upsert)
.build();
client.update(req, Map.class);
client._transport().close();
}
8.x Java API Client에서는 단순 문자열이나 byte 배열 전달 방식 대신 객체 기반 사용을 권장하였다.
먼저 수동으로 JSON 문자열을 조립하지 않고 Map<String, Object>를 doc(…)에 그대로 넘기는 방식으로 전환하였다. 이렇게 하면 JSON 직렬화는 클라이언트가 자동으로 처리하므로, 구문 오류로 인한 파싱 예외를 원천적으로 방지할 수 있다.
한계: 키는 여전히 문자열이므로 오타를 컴파일 시점에 잡지 못하고, 값 타입 불일치도 런타임에야 매핑 충돌이 발생한다.
예제를 한번 살펴보자
"mappings": {
"properties": {
"age": { "type": "integer" }
}
}다음과 같이 ElasticSearch에서 매핑이 정의되어 있는 경우
Map<String, Object> partial = Map.of(
"age", "twenty" // ← 잘못된 타입
);
client.update(req, Map.class); // 컴파일 OK → 런타임 매핑 충돌key 오타, 값 타입 불일치는 컴파일에서 못 잡음 → 많은 오류가 여전히 런타임이 발생한다.
2차 개선 — POJO 기반 업데이트로 타입 안정성 확보
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/8.19/introduction.html
8.x의 공식 문서를 살펴보면 다음과 같은 문구가 나온다.
NOTESeamless integration of application classes by using an object mapper such as Jackson or any JSON-B implementation.
즉, 8.x Java API Client는 애플리케이션에서 정의한 Java 클래스(POJO/DTO 등)를 직렬화 도구(ObjectMapper 등)를 사용해 JSON으로 변환한다고 명시되어 있다.
POJO 기반 코드 작성
public class VerifyPatch {
private Boolean accidentFlag;
}
es.patchDocFields(verifyUid, new VerifyPatch(true), false);
public void patchDocFields(String id, VerifyPatch doc, boolean upsert) throws IOException {
ElasticsearchClient client = getEsClient();
var req = new co.elastic.clients.elasticsearch.core.UpdateRequest
.Builder<VerifyPatch, VerifyPatch>()
.index(this.log_idx)
.id(id)
.doc(doc)
.docAsUpsert(upsert)
.build();
client.update(req, VerifyPatch.class);
client._transport().close();
}VerifyPatch라는 POJO를 정의하고, UpdateRequest<POJO, POJO> 형태로 업데이트했다. 이렇게 하면 필드명/값 타입을 코드 레벨에서 고정할 수 있어, 많은 실수를 컴파일 타임에 차단할 수 있다.
성과와 느낀점
성과
- 기존 5.x 레거시 클라이언트에서 8.x 최신 클라이언트로 안정적으로 마이그레이션을 완료
- byte[] 기반 업데이트 방식에서 Map/POJO 기반으로 전환하면서 타입 안정성이 향상되었다.
- 값 타입 불일치, 필드명 오타 등 기존에 런타임에서만 발견되던 오류를 컴파일 단계에서 사전에 차단할 수 있게 되었다.
- 코드 가독성과 유지보수성이 개선
느낀점
버전 마이그레이션은 처음 다뤄본 경험이었는데, 이 과정에서 공식 문서의 중요성을 다시금 느낄 수 있었다. 처음에는 다른 자료에서 이유를 찾으려 했지만, 공식 문서를 확인하자 기존 방식이 이미 더 이상 지원되지 않는 메서드임을 바로 파악할 `수 있었다.
또한 공식 문서를 참고하면서 권장하는 POJO 기반 업데이트 방식을 적용하면서 안정성을 보장할 수 있었다. 런타임 오류를 줄이고 최대한 많은 오류를 컴파일 시점에 발견하는 것의 중요성을 깨달았다.(나중에 런타임 오류 시 코드 하나하나 찾아갈 생각하면 끔찍하다..)
블로그나 AI를 활용하면 에러 원인을 빠르게 발견할 때도 있지만, 그렇지 않은 경우 공식 문서가 가장 확실한 근거라는 것을 다시 알게되었다.
