2026년 7월 26일 · 읽는 데 약 7분

Elasticsearch 매핑 없이 데이터를 넣으면 생기는 일

Elasticsearch 매핑 없이 데이터를 넣으면 생기는 일

매핑을 정의하지 않고 Elasticsearch에 문서를 넣으면 어떻게 될까요. 결론부터 적으면, 일단은 아주 잘 들어갑니다. 에러도 경고도 없이요. 각 필드에 처음 도착한 값의 모양을 보고 Elasticsearch가 타입을 추측해 매핑을 만들어 주기 때문입니다. 동적 매핑(dynamic mapping)입니다. 그리고 사고는 바로 그 성공에서 시작됩니다.

추측은 그대로 확정됩니다. 첫 값이 "2023-04-08"이면 그 필드는 날짜가 되고, "미정"이면 문자열이 됩니다. 한번 굳은 타입은 제자리에서 바꿀 수 없고, 그 우연한 결정이 이후 도착하는 모든 문서의 운명을 정합니다. 그러니 매핑 없이 데이터를 넣는 건 스키마를 생략한 게 아니라, 어쩌다 먼저 도착한 값들에게 스키마 설계를 맡긴 겁니다. 그 값들이 좋은 설계자일 확률은 생각보다 낮습니다.

필드는 처음 만난 값으로 타입이 굳습니다 — 동적 매핑의 추측 규칙

처음 보는 필드가 등장하면 Elasticsearch는 값의 모양을 보고 타입을 고릅니다. 글 쓰는 시점 9.x 문서의 기본값(dynamic: true) 기준으로 규칙은 이렇습니다.

첫 문서의 값만들어지는 타입
true / falseboolean
정수 42long
소수 3.14float
날짜 꼴 문자열 "2025-11-02"date
그 외 문자열 "미정", "30"text (+ keyword 서브필드)
null필드를 만들지 않음
배열null 아닌 첫 원소 기준

여기서 두 가지가 자주 허를 찌릅니다. 날짜 감지는 기본으로 켜져 있어서, 기본 날짜 포맷(strict_date_optional_time, 그리고 yyyy/MM/dd 계열)에 맞는 문자열은 date로 굳습니다. 반대로 숫자 감지는 기본으로 꺼져 있어서, 따옴표에 싸인 "30"은 숫자가 아니라 문자열로 굳습니다. 같은 30인데 따옴표 하나로 운명이 갈립니다.

규칙 자체는 합리적입니다. 문제는 추측하는 방식보다, 첫 판단이 이후의 모든 값에 계속 적용된다는 데 있습니다.

사고 1 — 첫 문서의 추측에 뒤 문서들이 튕깁니다

몇 년 전 사이드 프로젝트에서 공연 일정을 크롤링해 넣은 적이 있습니다. 첫 문서의 공연일은 "2023-04-08"이었고, 이 필드는 date로 굳었습니다. 며칠 뒤 수집기 로그에 mapper_parsing_exception이 쌓이기 시작했습니다. 날짜가 확정되지 않은 공연이 "미정"이라는 값을 들고 도착한 겁니다. 굳은 타입에 파싱할 수 없는 값이 오면 Elasticsearch의 기본 동작은 예외를 던지고 문서 전체를 거부하는 것입니다. 그 필드 하나 때문에 공연 제목도, 장소도, 문서에 있던 모든 정보가 함께 색인되지 못했습니다. 원본이 사라진 건 아니지만, 검색에는 그 공연이 통째로 없는 것과 같았습니다.

Bulk API로 넣고 있다면 이 거부는 더 늦게 발견됩니다. 요청 자체는 성공으로 돌아오고, 실패는 응답의 개별 항목 안에만 적히기 때문입니다. 항목 단위로 검사하지 않으면 조용히 흘러갑니다.

거꾸로였다면 더 나빴을 겁니다. 첫 문서가 "미정"이었다면 필드는 text로 굳고, 이후의 모든 날짜가 문자열로 들어갑니다. 에러는 한 건도 없습니다. 대신 “지난 7일” 같은 날짜 연산 필터를 걸 수 없고, Kibana에서 시간축 차트를 그리려는 순간에야 이 필드가 날짜가 아니었다는 걸 알게 됩니다. 에러 없이 조용히 잘못되는 실패는 안드로이드 폰트가 소리 없이 시스템 폰트로 바뀌던 일과 같은 부류입니다. 화면을 보기 전까지는 틀렸다는 사실 자체를 모릅니다.

사고 2 — 값이 조용히 바뀌어 들어갑니다

굳은 타입과 어긋난 값이 항상 튕기는 것도 아닙니다. 숫자 필드에는 강제 변환(coerce)이 기본으로 켜져 있습니다. long으로 굳은 필드에 문자열 "10"이 오면 숫자 10으로 해석해 색인하고, 소수 3.7이 오면 소수부를 잘라 3으로 색인합니다. 이것도 공식 문서에 적힌 기본 동작입니다. 여기의 함정은 원본 JSON은 _source에 그대로 남는다는 점입니다. 문서를 조회하면 3.7이 멀쩡히 보이는데, 집계와 정렬이 계산하는 값은 3입니다. 색인은 성공하고 로그는 조용하고 조회 결과까지 정상인데 평균만 미묘하게 어긋나는 대시보드가 이런 데서 나올 수 있습니다. 겉으로 가장 멀쩡해 보이는 문서가 가장 늦게 발견되는 오류가 되는 셈입니다. 색인이 통과했다고 데이터가 무사한 건 아닙니다 — 통과만 하는 검증은 코드 리뷰 밖에도 있습니다.

앞의 "30"도 여기로 이어집니다. 나이를 "30"처럼 따옴표째 처음 넣었다면 이 필드는 text로 굳고, 그 뒤로는 평균·합계 같은 숫자 집계에 쓸 수 없습니다. text 필드 자체에 정렬이나 집계를 걸면 기본 설정에서는 예외가 나고, 함께 만들어진 .keyword 서브필드로 정렬하면 에러는 피하지만 문자열 사전순입니다. "100""99"보다 앞에 옵니다.

사고 3 — 필드 수가 폭발합니다

동적 매핑은 처음 보는 필드마다 매핑에 항목을 추가합니다. JSON의 키가 사실상 데이터인 구조를 그대로 넣으면 — 사용자 ID나 상품 코드를 키로 쓰는 로그가 전형입니다 — 문서가 올 때마다 필드가 늘어납니다. 인덱스당 기본 한도인 1,000개에 닿는 순간부터, 기본 설정에서는 색인이 “Limit of total fields has been exceeded” 에러로 실패합니다. 고유 키 1,000개를 넣어야 닿는 것도 아닙니다. 동적으로 굳은 문자열 하나가 text와 keyword로 두 자리를 차지하고, 중첩 객체의 경로도 단계마다 셈에 들어갑니다. 보이는 키 수보다 한도가 빨리 찹니다.

이때 한도를 올리면 당장은 뚫리지만, 키가 데이터인 구조가 남아 있는 한 필드는 계속 늘어납니다. 구조를 뒤집는 쪽이 처방입니다. {"user_1234": 3}처럼 키에 들어 있던 값을 {"user": "1234", "count": 3}처럼 값의 자리로 내리면, 필드는 두 개로 끝납니다. 키를 미리 알 수 없어 구조를 뒤집기 어렵다면 flattened 타입이 대안입니다. 임의의 키가 계속 생겨도 필드 수가 늘지 않도록, 객체 전체를 필드 하나처럼 다루는 타입입니다.

처방 — 전부는 말고, 아는 필드만 먼저 적습니다

매핑 없이 시작했다는 걸 눈치챘을 때 제가 밟는 순서입니다.

  1. 지금 뭘로 굳었는지부터 확인합니다. GET 인덱스명/_mapping 한 번이면 동적 매핑의 추측 결과가 전부 나옵니다. 첫 문서를 넣은 직후에 이걸 열어보는 습관만 있어도 위 사고 대부분은 커지기 전에 잡힙니다.

  2. 검색·집계·시각화에 쓸 필드만 명시적 매핑으로 먼저 적습니다. 전부 적을 필요는 없습니다. 날짜 필드, 숫자 집계에 쓸 필드, 정확히 일치해야 하는 코드값 정도만 먼저 못 박고 나머지는 동적 매핑에 맡겨도 됩니다.

  3. 모르는 필드를 어떻게 받을지 dynamic 파라미터로 정합니다. 세 값의 차이는 이렇습니다.

    모르는 필드가 오면어울리는 자리
    true (기본)매핑을 자동 생성 — 지금까지 본 동작탐색·실험 단계
    strict문서를 거부 — 어긋남을 즉시 알게 됨정합성이 중요한 인덱스
    false받아는 주되 색인하지 않음 (_source에는 남음)검색할 일 없는 부속 데이터

    저는 정합성이 중요한 인덱스에는 strict를 겁니다. false는 그 필드로 검색이 안 된다는 뜻이기도 해서, 이 글이 경고한 조용함이 하나 늘어난다는 걸 알고 골라야 합니다. 인덱스 전체가 부담스러우면 특정 object 필드에만 걸 수도 있습니다.

  4. 이미 굳었다면 인덱스를 새로 만듭니다. 굳은 필드의 타입을 제자리에서 바꿀 방법은 없습니다. 올바른 매핑으로 새 인덱스를 먼저 만들고, 기존 데이터를 reindex API로 옮기는 게 정석입니다. 응용 코드가 인덱스 이름을 직접 바라보고 있다면 이때 별칭(alias)도 함께 끼워 두세요. 다음 리인덱스부터는 응용 코드를 건드리지 않고 전환할 수 있습니다.

매핑 없이 넣은 인덱스에도 스키마는 있습니다. 필드마다, 어느 날 맨 먼저 도착한 값이 쓴 스키마입니다. 그 값은 설계된 게 아니라 그저 제일 먼저 도착했을 뿐입니다. 지금 그 인덱스의 _mapping을 열어보면, 우연이 써 놓은 스키마가 그대로 보입니다.


여백 — 본문에 안 들어간 팁 하나. 개발 단계에서 매핑을 실험할 때는 reindex API보다 인덱스를 지우고 새로 만드는 쪽이 빠를 때가 많습니다. 데이터를 버려도 되는 시기에만 쓸 수 있는 방법이라, 저는 매핑 실험을 그 시기 안에 끝내두려고 합니다. 굳은 뒤의 교정보다 굳기 전의 실험이 쌉니다.

참고 — Dynamic field mapping (Elastic 공식 문서), [ELK] 엘라스틱서치 시작하기 (12bme) — 이 글의 출발이 된 옛 메모의 원문

이 글이 울렸다면 눌러 주세요

댓글

  • 불러오는 중…

← 전체 글 목록