검색 색인 파이프라인 개선기: 설정·스냅샷·이벤트 재처리

anlee·2025년 6월 10일

매일매일 블로그

목록 보기
25/49

다양한 서비스가 결합된 대규모 플랫폼에서는 사용자들이 원하는 정보를 빠르고 정확하게 찾을 수 있도록 지원하는 검색 기능이 매우 중요합니다. 저희 팀은 여러 서비스들이 공통으로 사용할 수 있는 검색 플랫폼을 만들고, 다양한 서비스의 검색 기능을 안정적으로 지원하는 역할을 담당하고 있습니다.

이를 위해서는 가장 먼저 각 서비스의 데이터를 검색 엔진으로 가져와 색인(Indexing)하는 과정이 필요한데요, 이 글에서는 기존의 색인 파이프라인(Indexing Pipeline)을 운영하면서 마주했던 문제점들과, 이를 해결하기 위해 어떻게 파이프라인을 개선해 나갔는지 그 여정을 공유하고자 합니다.


1. 기존 색인 파이프라인의 문제점: 우리가 마주한 4가지 도전 과제

저희가 운영하던 색인 파이프라인은 크게 두 가지 방식으로 동작했습니다. 첫째는 데이터 변경 사항을 실시간으로 반영하기 위한 온라인 색인(Online Indexing)이고, 둘째는 데이터 보정이나 사전 데이터 변경 등을 위해 하루에 한 번 전체 데이터를 다시 색인하는 오프라인 색인(Offline Indexing)입니다. 이 두 가지 파이프라인을 운영하면서 다음과 같은 문제점들에 직면하게 되었습니다.

  • 생산성 (Productivity): 데이터를 가져와 검색 엔진에 맞게 가공하고 색인하는 로직의 패턴은 대부분 유사했지만, 새로운 서비스가 추가될 때마다 해당 서비스만을 위한 별도의 색인 로직을 구현해야 했습니다. 이는 서비스가 늘어날수록 관리 포인트가 기하급수적으로 증가하는 결과를 낳았습니다.
  • 의존성 (Dependency): 색인 파이프라인이 각 서비스의 운영 데이터베이스(Online DB)에 직접 연결하여 데이터를 가져오는 구조였습니다. 이로 인해 색인 파이프라인은 각 서비스 DB에 매우 강한 의존성을 가지게 되었고, 서비스 DB의 스키마 변경이나 장애 발생 시 색인 파이프라인도 직접적인 영향을 받는 취약한 구조였습니다.
  • 비용 (Cost): 매일 수행되는 오프라인 풀 색인(Full Indexing) 작업은 각 서비스의 운영 DB 전체를 스캔(Full Scan)해야 했습니다. 이는 운영 DB에 상당한 부하를 주었고, 이 부하를 감당하기 위해 서비스팀에서는 색인 파이프라인만을 위한 별도의 복제 DB(Replica DB)를 만들어 주어야 하는 부담과 비용이 발생했습니다.
  • 가시성 (Visibility): 어떤 데이터 필드가 어떤 로직을 거쳐 어떻게 색인되는지, 특정 데이터가 왜 필터링되었는지 등을 파악하려면 매번 담당 개발자가 작성한 코드를 일일이 열어 분석해야 했습니다. 이는 파이프라인의 동작을 이해하고 디버깅하는 데 많은 시간을 소모하게 만들었습니다.

2. 새로운 색인 파이프라인을 향한 여정: 4가지 개선 목표 설정

위에서 언급한 4가지 문제를 해결하기 위해, 우리는 다음과 같은 명확한 목표를 세웠습니다.

  1. 생산성 향상: 설정 기반(Configuration-driven)의 인터페이스를 제공하고 작업을 자동화하여, 누구나 최소한의 노력으로 새로운 데이터 소스를 연동할 수 있는 생산성 높은 시스템을 만든다.
  2. 의존성 완화: 외부 서비스의 운영 DB에 대한 직접적인 의존성을 낮춰, 상태 변경이나 장애 발생에도 안전하고 독립적인 색인 파이프라인을 만든다.
  3. 비용 절감: 매일 수행되는 풀 색인의 부담을 줄이고, DB 리소스 및 인프라 비용을 최적화한다.
  4. 고가용성 확보: 대량의 이벤트가 쏟아지는 상황에서도 안정적으로 동작하는 고가용성 시스템을 만든다.

3. 문제 해결 과정: 어떻게 파이프라인을 개선했는가?

1) 설정 기반 인터페이스로 생산성 높이기: "코딩 대신 설정으로!"

생산성 높은 시스템을 만들기 위해서는, 반복적인 작업을 자동화하고 개발자가 핵심 비즈니스 로직에만 집중할 수 있도록 해야 합니다. 이를 위해 우리는 다음과 같은 원칙을 세우고, 설정 기반의 인터페이스를 설계했습니다.

  • 원칙:

    • 인터페이스에 명시된 동작만 수행되어야 하며, 숨겨진 마법 같은 로직은 배제한다.
    • 모든 데이터는 명확한 스키마를 기반으로 안전하게 처리되어야 한다.
    • 데이터의 입력(Input)과 출력(Output) 사이에는 커스텀 비즈니스 로직(Transform)을 자유롭게 삽입할 수 있어야 한다.
    • 플랫폼의 핵심 로직은 데이터 처리 외의 부가적인 작업에 관여하지 않아야 한다.
  • YAML 기반 설정 인터페이스:

    • 개발자가 쉽게 이해하고 작성할 수 있도록 YAML 형식으로 설정 인터페이스를 정의했습니다. 이 파일 하나에 데이터 소스 정보, 스키마 위치, 구독할 카프카 토픽, 데이터 변환 로직, 배포 환경 설정 등을 모두 명시할 수 있도록 했습니다.

프로젝트 내부 설정 형식 예시 — 아래 필드는 설명용 내부 스키마이며, Kafka나 Airflow가 그대로 인식하는 표준 설정이 아닙니다. 런타임에서 필드·타입·조건식과 참조하는 변환 함수를 검증해야 합니다.

# 예시: 데이터 소스 및 색인 관련 설정을 정의하는 YAML 파일
version: 1
name: my_service_datastore
storage: datastore
sources:
  - table: my_service_table_v1
    primary_key: id
    schema: dbschema/my_service_table.yaml # 데이터 스키마 파일 경로
    scan:
      type: time
      target_field: updated_at # 증분 색인을 위한 시간 기준 필드
      ttl_interval_ms: 86400000 # 데이터 보존 기간 (예: 1일)
    subscribe_message:
      - topic: event.my_service.data_changed # 실시간 변경 이벤트를 수신할 Kafka 토픽
        transform: my_custom_transform # 해당 이벤트를 처리할 변환 로직 이름
    auto_indexing:
      - option: simple
        active_conditions: # 검색 엔진에 색인될 데이터의 조건
          - deleted_at is null
# ... (배포, 리소스, 변환 파라미터 등 기타 설정)
  • 코드 생성(Code Generation)을 통한 안정성 확보:
    • "모든 데이터는 스키마를 가져야 한다"는 원칙을 지키기 위해, 위 YAML 설정 파일에 정의된 스키마 정보를 바탕으로 데이터 모델(DTO/VO) 코드를 자동으로 생성하는 방식을 채택했습니다.
    • 이를 통해 생성 코드 내부의 타입 일관성을 검사하고, 온라인 이벤트와 오프라인 배치 데이터를 같은 모델로 다룰 수 있었습니다. 외부 JSON의 필수 필드, 타입, 스키마 버전은 런타임에서도 검증해야 합니다.

자동 생성 모델의 구조 예시

// Code generated by indexer/message. DO NOT EDIT.
// source: message/schema/my_service_datastore.yaml

package my_service_datastore

import (
   "time"
)

type MyServiceTableV1 struct {
    ID        int64      `db:"id" json:"id"`
    Title     *string    `db:"title" json:"title"`
    Content   *string    `db:"content" json:"content"`
    UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
    // ...
}
  • 자유로운 데이터 변환 로직(Transform) 삽입:
    • 외부에서 수신한 메시지(예: Kafka 이벤트)를 검색 엔진에 적재하기 전에, 개발자가 자유롭게 비즈니스 로직을 추가하여 데이터를 가공할 수 있도록 Transform 인터페이스를 제공했습니다.
    • 개발자는 이 인터페이스를 구현하여 Kafka 메시지를 파싱하거나, 다른 데이터를 조합하거나, 특정 필드를 추가/제거하는 등 커스텀 변환 로직을 손쉽게 작성할 수 있습니다.

이러한 설정 기반 자동화 덕분에, 새로운 서비스를 검색 플랫폼에 연동하는 작업이 매우 간소화되었고, Airflow와 같은 워크플로우 관리 도구에 오프라인 색인 파이프라인(DAG)이 자동으로 생성되도록 연동하여 생산성을 극대화할 수 있었습니다.

2) 외부 DB 의존성 낮추기: "안전지대, 오프라인 스토리지를 활용하자!"

운영 DB(Online DB)는 실시간으로 상태가 변하고, 직접적인 접근은 장애 포인트를 증가시킵니다. 또한, 매일 풀 스캔을 수행하는 것은 비용과 부하 측면에서 매우 비효율적입니다. 우리는 이 문제를 해결하기 위해 오프라인 스토리지(Offline Storage)를 중간 저장소로 활용하는 아키텍처를 도입했습니다.

  • 아키텍처 변경:
    1. 중간 저장소로 오프라인 스토리지 활용: 운영 DB 대신, 모든 서비스의 데이터가 주기적으로 적재되는 BigQuery 같은 분석 저장소를 배치 색인의 주 데이터 소스로 사용하기로 결정했습니다.
    2. 초기 데이터 적재 (Backfill): 새로운 데이터 소스를 연동할 때, 먼저 오프라인 스토리지의 전체 데이터를 가져와 검색 엔진에 초기 풀 색인(Backfill)을 수행합니다.
    3. 변경 사항 반영 (CDC 및 이벤트 기반): 운영 DB에서 발생하는 데이터 변경 사항은 상태 변경 이벤트(WAL - Write-Ahead Log 기반 또는 애플리케이션 레벨 이벤트) 형태로 Kafka와 같은 메시지 큐로 발행됩니다.
      • 이벤트에 ID를 넣고 최신 상태를 다시 읽으면 여러 변경을 최신 상태로 합칠 수 있습니다. 하지만 ID만 전달한다고 순서나 유실이 해결되지는 않습니다. 변경 이력 보존이 필요한 이벤트에는 적합하지 않을 수 있습니다.
      • 별도의 CDC 스트리밍 애플리케이션이 이 ID를 기준으로 운영 DB에서 최신 데이터를 다시 조회하고, 스키마에 맞춰 변환한 후 오프라인 스토리지에 안정적으로 적재합니다.
    4. 데이터 정합성 확보: 오프라인 배치 색인 시, 오프라인 스토리지의 데이터와 스트리밍으로 수집된 최신 데이터를 비교하여, 원본의 단조 증가 버전이나 로그 위치 등 비교 가능한 기준으로 오래된 데이터가 최신 결과를 덮어쓰지 않도록 합니다. updated_at만으로 판단하면 시계 차이·동일 시각 갱신을 구분하지 못할 수 있습니다.

이러한 구조를 통해 배치 색인기의 운영 DB 전체 조회 의존성을 줄이고, 안정적이고 정제된 오프라인 데이터를 기반으로 색인 작업을 수행함으로써 파이프라인 전체의 안정성을 크게 향상시킬 수 있었습니다.

이 구조에도 최신 상태를 다시 조회하는 수집기는 운영 DB에 의존합니다. 요청을 모아 읽고 조회 동시성을 제한하는 등 부하 관리가 필요합니다. 삭제된 행은 재조회 결과만으로 삭제 시점과 버전을 알 수 없으므로 삭제 이벤트나 tombstone을 보존해야 합니다.

초기 스냅샷과 실시간 변경을 연결할 때는 스냅샷 기준점과 스트림 오프셋을 함께 기록합니다. 스냅샷 이후 변경을 재생하고, 새 인덱스의 건수·표본·삭제 반영을 확인한 뒤 읽기 대상을 전환합니다. 단순히 배치가 끝난 시각부터 이벤트를 읽으면 수집 중 발생한 변경을 놓칠 수 있습니다.

3) 풀 색인 부담 낮추고 비용 절감하기: "파티셔닝으로 똑똑하게 읽자!"

하루에 한 번, 수억 건의 데이터를 오프라인 스토리지에서 가져와 풀 색인하는 과정은 여전히 비용과 시간 측면에서 부담이었습니다. BigQuery의 읽기 비용은 조회 컬럼과 파티션·클러스터 프루닝, 요금 모델 등에 영향을 받습니다. 파티션 컬럼을 활용하지 못하는 필터는 많은 데이터를 읽을 수 있지만, 파일 기반이라는 이유만으로 모든 범위 조회가 전체 스캔을 하는 것은 아닙니다. BigQuery 파티션 조회

  • 해결책: 파티셔닝(Partitioning) 및 분산 처리
    1. 임시 파티션 테이블 생성: 오프라인 색인 작업 시작 전에, 원본 데이터 테이블을 파티셔닝 규칙(예: 해시, 범위 등)에 따라 여러 개의 작은 임시 파티션 테이블로 분할하는 전처리 단계를 추가했습니다.
    2. 파티션 기반 분산 처리: 색인 파이프라인의 각 분산된 프로세서(Worker)는 자신에게 할당된 파티션 테이블의 데이터만 읽어서 처리하도록 했습니다.

작업을 나누면 병렬성은 높일 수 있지만, 데이터를 나누어 복사하는 전처리 자체의 스캔·저장 비용도 계산해야 합니다. BigQuery의 네이티브 시간·정수 범위 파티셔닝과 임시 테이블을 만드는 작업 분할은 구분합니다. 해시 조건으로 여러 번 읽는다면 각 작업이 같은 원본을 반복 스캔하는지도 확인해야 합니다.

초기 기록의 처리 시간 1~2시간과 수만 원 수준 비용은 당시 작업에 대한 설명입니다. 데이터 크기, 가격·요금 모델, 전처리 포함 여부가 정리되지 않았으므로 일반적인 비용 절감 효과로 확장하지 않습니다. 실행 계획과 처리 바이트, 전처리부터 색인 완료까지의 시간을 함께 비교해야 합니다.

4) 이벤트 폭증에도 안전한 고가용성 시스템 만들기: "스트리밍 배치로 부하를 제어하자!"

실시간 색인을 위해 사용하는 Kafka에는 특정 서비스로부터 초당 수천 건 이상의 이벤트가 쏟아져 들어오는 경우가 있습니다. 만약 이러한 이벤트를 하나씩 개별적으로 처리한다면, Kafka Lag가 발생하거나 DB 및 검색 엔진에 급격한 부하가 발생하여 다른 서비스에까지 영향을 미칠 수 있습니다.

  • 해결책: 스트리밍 배치 처리 (Streaming Batch Processing) 도입
    • 시간·크기 기준 배치: 일정 시간이나 최대 건수·바이트에 도달하면 이벤트를 묶어 처리합니다. 이벤트 시간 기준 Tumbling Window를 쓰는 경우에는 워터마크·지연 이벤트 정책 때문에 벽시계 기준 2초마다 결과가 나온다고 보장할 수 없습니다.
    • 상태 저장소 활용: 버퍼를 메모리나 로컬 상태 저장소에 유지한다면 재시작·리밸런싱 시 복구 방식을 함께 정합니다. 로컬 저장만으로 장애 복구가 보장되지는 않습니다.
    • 배치 변환 및 색인: 설정된 시간 윈도우가 닫히면, 모아둔 이벤트 배치를 한 번에 변환하고, 검색 엔진에 벌크(Bulk) API를 사용하여 색인합니다.

다음은 특정 Go 라이브러리의 실행 코드가 아닌 처리 순서를 나타낸 의사 코드입니다.

이벤트를 제한된 크기의 버퍼에 추가
시간 또는 건수·바이트 상한에 도달하면 배치 생성
문서 ID별 버전과 삭제 정보를 확인하며 변환
검색 엔진 Bulk 요청 전송
응답의 각 항목을 검사
  일시 실패: 해당 항목만 제한적으로 재시도
  영구 실패: 실패 저장소에 기록하고 운영 정책에 따라 처리
완료가 확인된 연속 구간까지 파티션별 오프셋 커밋

Bulk 요청의 HTTP 응답이 성공이어도 개별 항목은 실패할 수 있습니다. 항목별 결과를 검사하지 않고 오프셋을 커밋하면 재처리 기회를 잃습니다. 반대로 색인 성공 직후 커밋 전에 장애가 나면 중복 처리될 수 있으므로 문서 ID와 버전 기준으로 멱등성을 확보해야 합니다. Elasticsearch Bulk API

배치 처리는 호출 오버헤드를 줄이지만 고가용성 자체를 보장하지는 않습니다. 버퍼 상한, 소비 지연, 재시도 대기열, 리밸런싱 복구와 검색 엔진 장애를 함께 검증해야 합니다. 과거 기록에 등장한 Pod 수와 처리량 역시 메시지 크기·검색 부하·내구성 조건이 없으면 용량 산정 기준으로 사용할 수 없습니다.


마무리: 지속적인 개선을 향하여

지금까지 검색 플랫폼의 색인 파이프라인이 가진 문제점들을 해결하고, 안정성과 생산성을 높이기 위해 고민하고 개선했던 과정들을 공유했습니다.

  • 설정 기반 자동화를 통해 생산성을 높이고,
  • 오프라인 스토리지 활용으로 외부 의존성을 낮추었으며,
  • 파티셔닝과 스트리밍 배치 처리를 통해 비용과 처리량을 조정할 수 있는 구조를 만들었습니다. 고가용성은 복구·재처리 검증으로 별도 확인해야 합니다.

서비스가 성장할수록 더 많은 데이터를 더 효율적으로 처리할 수 있는 구조는 필수적입니다. 저희는 여기서 멈추지 않고, Transform 로직 자동 생성, 벡터 임베딩 및 LLM을 활용한 시맨틱 검색, 모델 서빙(Inference) 파이프라인 통합, 지속적인 테스트 및 모니터링 강화 등 더욱 편리하고 신뢰할 수 있는 검색 환경을 제공하기 위해 다양한 시도를 계속하고 있습니다.

이 글이 대규모 데이터 처리와 검색 시스템 아키텍처에 대해 고민하는 분들께 작은 영감이나마 드릴 수 있었기를 바랍니다. 긴 글 읽어주셔서 감사합니다.

0개의 댓글