수백만 건 공공데이터 변경분을 감지하는 해시 기반 델타(Delta) 동기화 파이프라인 설계

수백만 건 공공데이터 변경분을 감지하는 해시 기반 델타(Delta) 동기화 파이프라인 설계
수백만 건 공공데이터 변경분을 감지하는 해시 기반 델타(Delta) 동기화 파이프라인 설계

국토교통부 건축물대장, 행정안전부 지방행정 인허가 데이터, 도로명주소 등 국가 공공데이터포털(data.go.kr)에서 개방하는 주요 데이터셋은 작게는 수십만 건에서 많게는 1,000만 건 이상의 방대한 규모를 가집니다.

이러한 원천 데이터를 주기적으로 내부 RDBMS나 검색 엔진(Elasticsearch)에 동기화할 때 가장 큰 난관은 신뢰할 수 있는 변경 일자(updated_at) 컬럼의 부재입니다. 많은 공공데이터 API나 CSV 덤프 파일은 최종 수정 시각을 제공하지 않거나, 시스템 이관 일자로 통째로 갱신되어 있어 타임스탬프 기반의 증분 추출이 원천적으로 불가능합니다.

이로 인해 매 배치 주기마다 수백만 건을 무차별 덮어쓰기(Full Upsert)하게 되면, 불필요한 디스크 I/O와 인덱스 재정렬, Redo/Undo 로그 폭증으로 인해 데이터베이스 성능이 급격히 저하됩니다. 이번 글에서는 Martin Kleppmann의 데이터 지향 애플리케이션 설계(DDIA) CDC 개념과 Debezium 아키텍처를 바탕으로, 타임스탬프 없이도 속성 해시 지문(Row Hash Fingerprint)을 통해 변경분을 완벽히 식별하고 쓰기 부하를 95% 이상 감축하는 실무 델타 파이프라인을 정리합니다.

1. 단순 전체 덮어쓰기(Full Upsert)가 초래하는 인프라 비용

수백만 건의 데이터를 매일 통째로 INSERT ... ON DUPLICATE KEY UPDATE로 밀어 넣을 경우 다음과 같은 심각한 오버헤드가 발생합니다:

  • 실제 변경 없는 레코드에 대한 불필요한 Write I/O: 100만 건 중 실제 데이터가 변경된 것은 하루 평균 0.1%~0.5%(1,000~5,000건)에 불과함에도, 99.5%의 동일한 행에 대해 락을 획득하고 인덱스 탐색 및 B-Tree 리프 노드 검증을 수행합니다.
  • WAL 및 바이너리 로그(Binlog) 폭증: 실제 값의 변동이 없더라도 행 단위 쓰기 락과 트랜잭션 로그가 생성되어 복제 슬레이브 지연(Replication Lag)을 유발합니다.
  • 다운스트림 이벤트 브로커 과부하: DB 변경 이벤트를 감지하여 Kafka나 검색 인덱스로 전파하는 파이프라인이 있다면, 의미 없는 수정 이벤트가 대량 방출되어 컨슈머 시스템 전체가 지연됩니다.

2. 해시 기반 델타(Hash-based CDC) 아키텍처 핵심 원리

원천 시스템이 변경 로그(Binlog/WAL)를 직접 제공하지 못하는 파일/API 환경에서는 결정적 해시 지문(Deterministic Hash Fingerprint)을 활용하여 애플리케이션 레벨에서 변경 데이터를 캡처(CDC)해야 합니다.

  1. 카노니컬 문자열(Canonical String) 표준화: 변경 감지 대상이 되는 비즈니스 속성 컬럼들을 알파벳 순으로 정렬하고, Null 값 및 앞뒤 공백을 정규화한 구분자 기반 단일 바이트열로 변환합니다.
  2. 단방향 해시 생성: 정규화된 바이트열을 SHA-256(또는 빠른 연산이 필요한 경우 MD5)으로 해싱하여 32~64자의 고유 체크섬을 생성합니다. SHA-256의 충돌 확률은 2의 256제곱분의 1로, 수천억 건의 레코드에서도 수학적으로 충돌이 불가능합니다.
  3. 3자 비교 집합 연산(Set Difference): 원천 데이터의 (PK, Hash) 튜플과 타깃 DB에 저장된 최신 (PK, Hash) 튜플을 집합 연산하여 Insert(신규), Update(해시 불일치), Delete(원천에서 소멸)의 3개 변경 집합으로 완벽히 분리합니다.

3. 파이썬 기반 해시 델타 체커 실무 구현

다음은 메모리 효율성을 고려하여 원천 데이터와 기존 DB의 해시 테이블을 비교 분석하는 파이썬 델타 추출 모듈 예시입니다.

import hashlib
import json
import logging
from typing import Dict, Any, Tuple, Set

logger = logging.getLogger(__name__)

def generate_row_hash(record: Dict[str, Any], tracking_columns: list) -> str:
    # 1. 컬럼 순서 및 공백 표준화 (Canonical String)
    canonical_data = [
        str(record.get(col, '')).strip() for col in sorted(tracking_columns)
    ]
    raw_payload = "|".join(canonical_data).encode("utf-8")
    return hashlib.sha256(raw_payload).hexdigest()

def identify_deltas(
    incoming_records: Dict[str, Dict[str, Any]], 
    existing_hashes: Dict[str, str],
    tracking_columns: list
) -> Tuple[list, list, list]:
    to_insert = []
    to_update = []
    
    incoming_keys = set(incoming_records.keys())
    existing_keys = set(existing_hashes.keys())
    
    # 1. 신규 레코드 (Insert)
    new_keys = incoming_keys - existing_keys
    for k in new_keys:
        row = incoming_records[k]
        row["row_hash"] = generate_row_hash(row, tracking_columns)
        to_insert.append(row)
        
    # 2. 변경 레코드 (Update: PK는 동일하나 속성 해시가 불일치)
    common_keys = incoming_keys & existing_keys
    for k in common_keys:
        row = incoming_records[k]
        new_hash = generate_row_hash(row, tracking_columns)
        if new_hash != existing_hashes[k]:
            row["row_hash"] = new_hash
            to_update.append(row)
            
    # 3. 삭제 레코드 (Delete: 소스 데이터에서 소멸)
    to_delete = list(existing_keys - incoming_keys)
    
    logger.info(f"델타 분석 완료: 신규 {len(to_insert)}건, 수정 {len(to_update)}건, 삭제 {len(to_delete)}건, 미변경 {len(common_keys) - len(to_update)}건")
    return to_insert, to_update, to_delete

4. 대용량 동기화 운영 모범 체크리스트

  • 해시 전용 인덱스 및 컬럼 격리: 테이블에 row_hash CHAR(64)(SHA-256) 컬럼을 추가하고 보조 인덱스를 생성하십시오. 비교 쿼리 실행 시 SELECT id, row_hash FROM target_table만 메모리에 로드(커버링 인덱스)하면 100만 건이라도 수십 MB 메모리 내에서 0.5초 만에 해시 맵을 구축할 수 있습니다.
  • 부동소수점 및 날짜 포맷팅 정밀도 고정: 위도·경도 좌표나 금액, 일자 필드는 문자열 변환 시 37.566500과 37.5665처럼 포맷 차이로 인해 거짓 변경(False Positive Update)이 발생하기 쉽습니다. 포맷터 함수를 단일화하십시오.
  • 배치 감사 메트릭 로깅: 매 실행 주기마다 (총 처리 건수, Insert, Update, Delete, Skip 건수, 소요 시간)을 감사 테이블과 모니터링 대시보드에 기록하여, 원천 데이터의 비정상적인 대량 삭제나 포맷 변경 사고를 조기에 감지하십시오.

출처 및 공식 기술 문서

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다