들어가며: 스크립트 파이프라인의 함정

데이터 파이프라인은 보통 작은 스크립트 하나에서 시작합니다. CSV를 읽고, 날짜 형식을 바꾸고, 특정 컬럼을 매핑하는 단순한 작업이죠. 그런데 시간이 지나면서 비슷한 변환 로직이 여러 파일에 중복되고, 새로운 데이터셋을 추가할 때마다 기존 스크립트를 복붙해서 수정하게 됩니다.

이런 방식의 가장 큰 문제는 워크플로의 의도(Intent)가 코드 속에 묻혀버린다는 점입니다. 나중에 "이 파이프라인이 정확히 어떤 일을 하나요?"라는 질문에 답하려면 코드를 처음부터 분석해야 합니다. 특히 금융, 제약(임상시험)처럼 규제가 엄격한 환경에서는 이게 치명적입니다. 감사(Audit) 준비에 몇 주씩 걸리고, 새로운 데이터셋을 추가할 때마다 전체 배포 사이클을 기다려야 하니까요.

이 글에서는 명세 기반 구성(Specification-Driven Composition) 패턴을 소개합니다. 워크플로가 '무엇을' 해야 하는지와 '어떻게' 처리할지를 분리해서, 더 유연하고 관리 가능한 데이터 파이프라인을 만드는 방법입니다. AWS Lambda, Step Functions, S3, OpenSearch Service를 활용한 서버리스 구현 예제도 함께 다룹니다.

Developer reviewing a JSON specification document for data pipeline configuration Programming Illustration

핵심 아이디어: 의도와 실행의 분리

명세 기반 구성은 세 가지 계층으로 나뉩니다.

  1. 의도 계층 (Intent Layer) – 워크플로가 무엇을 해야 하는지 JSON/YAML 스펙으로 정의
  2. 구성 계층 (Composition Layer) – 스펙을 검증하고 실행 가능한 파이프라인으로 조립
  3. 처리 계층 (Processing Layer) – 실제 데이터 변환을 수행하는 재사용 가능한 함수들

실제 스펙 예시 (JSON)

{
  "workflow": "monthly_finance_report",
  "version": "1.0",
  "source": {
    "dataset": "raw_orders",
    "format": "csv",
    "location": "s3://data-lake/raw/orders/"
  },
  "target": {
    "dataset": "standard_orders",
    "format": "parquet",
    "location": "s3://data-lake/standard/orders/"
  },
  "preprocessing": [
    {
      "capability": "standardize_columns",
      "version": "2.1",
      "params": {
        "rename_map": {
          "order_id": "order_identifier",
          "order_total": "amount"
        }
      }
    },
    {
      "capability": "cast_numeric",
      "version": "1.0",
      "params": {
        "columns": ["amount", "tax", "shipping_cost"]
      }
    }
  ],
  "mappings": [
    {
      "source_field": "order_date",
      "target_field": "transaction_date",
      "capability": "format_date",
      "version": "1.2",
      "params": {
        "input_format": "%Y-%m-%d",
        "output_format": "%Y%m%d"
      }
    },
    {
      "source_field": "amount",
      "target_field": "amount_usd",
      "capability": "normalize_currency",
      "version": "3.0",
      "params": {
        "source_currency": "KRW",
        "target_currency": "USD"
      }
    }
  ]
}

이 스펙을 보면 "어떤 데이터를 가져와서, 어떤 전처리를 하고, 어떤 필드를 어떻게 변환해서 저장할지"가 코드 없이 명확하게 드러납니다. 이것이 바로 의도의 가시화입니다.

AWS architecture diagram showing Step Functions, Lambda, S3, and OpenSearch integration Algorithm Concept Visual

AWS 서버리스 구현: Composer + Registry + Step Functions

아키텍처 흐름

  1. 사용자가 S3 버킷에 JSON 스펙을 업로드
  2. S3 이벤트가 Lambda Composer 함수를 트리거
  3. Composer가 스펙을 파싱하고, OpenSearch Service에 등록된 Capability 레지스트리에서 각 변환 함수의 메타데이터(ARN, 입출력 스키마, 권한)를 조회
  4. Composer가 검증을 마치면 Step Functions 상태 머신(ASL)을 동적으로 생성하고 실행
  5. Step Functions가 각 Capability Lambda를 순차적으로 호출
  6. 모든 로그는 CloudWatch Logs로 전송

Capability 레지스트리 설계 시 주의할 점

레지스트리는 단순한 조회 테이블이 아니라 거버넌스 대상입니다. Capability 정의는 Git으로 관리하고, CI/CD 파이프라인에서 메타데이터 검증과 테스트를 거친 후에만 새 버전을 등록해야 합니다.

# 예: 레지스트리 조회 함수 (Python + boto3)
import boto3
from opensearchpy import OpenSearch, RequestsHttpConnection

def get_capability_metadata(capability_id, version):
    """
    OpenSearch에서 Capability 메타데이터를 조회합니다.
    """
    client = OpenSearch(
        hosts=[{'host': OPENSEARCH_ENDPOINT, 'port': 443}],
        http_auth=('master_user', 'master_password'),  # 실제로는 Secrets Manager 사용
        use_ssl=True,
        verify_certs=True,
        connection_class=RequestsHttpConnection
    )
    
    query = {
        "query": {
            "bool": {
                "must": [
                    {"term": {"capability_id": capability_id}},
                    {"term": {"version": version}}
                ]
            }
        }
    }
    
    response = client.search(index="capability-registry", body=query)
    hits = response['hits']['hits']
    
    if not hits:
        raise ValueError(f"Capability {capability_id} v{version} not found")
    
    return hits[0]['_source']

보안 고려사항

  • S3 버킷은 SSE-KMS(고객 관리 키)로 암호화
  • OpenSearch 도메인은 노드 간 암호화 및 HTTPS(TLS) 적용
  • 각 Capability Lambda는 필요한 필드만 수신하도록 IAM 정책 최소화
  • 민감 필드 태깅: 스펙에 "sensitivity": "PHI"를 명시하고, Capability가 이 태그를 어떻게 처리할지(유지/제거/추가) 레지스트리에 선언 → Composer가 자동으로 마스킹 정책 생성

Data transformation pipeline with reusable capability modules and declarative specs Development Concept Image

실무 적용 시 주의사항 및 한계

이 패턴은 모든 상황에 적합하지 않습니다. 다음과 같은 경우에는 오히려 복잡도만 증가시킵니다.

  • 단순 1회성 변환: 스펙을 작성하고 Composer를 설정하는 비용이 스크립트 하나 짜는 것보다 큼
  • 파이프라인이 3~5개 미만: 중복 제거 효과를 보기 어려움
  • 매우 빠른 프로토타이핑이 필요한 경우: 초기 설정에 시간이 걸림

또한, 스펙 스키마를 처음 설계할 때 신중해야 합니다. 너무 유연하게 만들면 검증 로직이 복잡해지고, 너무 제한적이면 다양한 워크플로를 표현하기 어렵습니다. 실제로 도입할 때는 기존 파이프라인 하나를 골라서 스펙으로 표현해보고, 부족한 부분을 발견하면서 점진적으로 개선하는 걸 추천합니다.

다음 단계 학습 방향

  1. AWS Lambda 이벤트 기반 아키텍처 가이드 – S3 업로드를 Composer에 연결하는 기본 패턴 학습
  2. Step Functions와 Lambda 통합 가이드 – 동적 상태 머신 생성 방법
  3. CloudWatch 사용자 지정 메트릭 – 파이프라인 성능 모니터링

한국 개발 생태계에서의 적용 맥락 국내에서는 금융권, 제약사, 대기업 데이터 플랫폼 팀에서 이 패턴을 도입하는 사례가 늘고 있습니다. 특히 규제 준수(예: 개인정보보호법, GxP)가 중요한 환경에서 '무엇을 했는지'를 코드 리뷰 없이도 감사할 수 있다는 점이 큰 강점입니다. 다만, SI 프로젝트처럼 짧은 기간에 많은 파이프라인을 만들어야 하는 상황에서는 초기 러닝커브를 고려해야 합니다.

함께 보면 좋은 글:

근거자료: AWS Architecture Blog - Specification-driven composition for flexible data workflows

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.