← 기술 블로그

Claude Code 에이전트 릴레이 패턴 — 스케줄 에이전트 간 파일 기반 통신

Phase: 8 — 운영 최적화 시리즈: 운영 최적화

이 글을 읽으면 알 수 있는 것


문제: 스케줄 에이전트는 서로를 모른다

Claude Code의 스케줄 에이전트(claude schedule)는 cron처럼 지정된 시간에 독립적으로 실행됩니다. 각 에이전트는 자기 프롬프트만 받고, 자기 작업만 하고, 종료합니다.

문제는 이겁니다. ETL 파이프라인에서 여러 단계의 분석이 필요한데, 에이전트 A가 에이전트 B를 호출하는 API가 없습니다.

에이전트 A (데이터 검증)
    ↓ ??? 어떻게 전달?
에이전트 B (비즈니스 분석)
    ↓ ??? 어떻게 전달?
Telegram 알림 (파운더에게 도착)

서로 직접 호출이 불가능한 에이전트들을 어떻게 릴레이처럼 협업시킬 것인가. 이것이 이 글의 핵심 질문입니다.


해법: 파일이 메시지 버스다

에이전트 간 통신에 별도 메시지 큐나 데이터베이스가 필요하지 않았습니다. 파일 시스템이 곧 메시지 버스입니다.

ETL (cron)
    ↓  .etl-results/latest.json 작성
validate_data_integrity (Django command)
    ↓  .etl-results/integrity-check.json 작성
data-validator 에이전트 (스케줄)
    ↓  .etl-results/dv-analysis.txt 작성
etl_daily_report (Django command)
    ↓  Telegram 메시지 발송

각 단계의 출력이 다음 단계의 입력이 됩니다. latest.json은 ETL 실행 결과, integrity-check.json은 무결성 검증 결과, dv-analysis.txt는 전문가 수준의 위험 분석입니다.

핵심 원칙은 단순합니다.

  1. 각 단계는 정해진 경로에 파일을 쓴다
  2. 다음 단계는 그 파일을 읽는다
  3. 파일이 없으면 해당 분석을 건너뛴다 (graceful degradation)

구현 1 — validate_data_integrity: 기계가 읽을 수 있는 검증 결과

기존 validate_data_integrity 커맨드는 사람이 읽는 텍스트만 출력했습니다. 에이전트가 이 결과를 해석하려면 구조화된 데이터가 필요합니다.

—format=json 옵션 추가

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            '--format', choices=['text', 'json'], default='text',
            help='Output format'
        )
        parser.add_argument(
            '--output', type=str, default=None,
            help='Write results to file instead of stdout'
        )

CHECK별 구조화된 결과

각 무결성 검증 항목을 일관된 구조로 출력합니다.

{
  "timestamp": "2026-03-31T04:15:00+09:00",
  "checks": [
    {
      "check_id": "CHK-001",
      "name": "product_count_consistency",
      "label": "Product count matches across sources",
      "passed": true,
      "message": "1,604,283 products in DB, 1,604,283 in ETL log"
    },
    {
      "check_id": "CHK-007",
      "name": "orphan_ingredients",
      "label": "No orphan ingredient references",
      "passed": false,
      "message": "Found 23 ingredients referencing non-existent substances"
    }
  ],
  "summary": {
    "total": 12,
    "passed": 11,
    "failed": 1
  }
}

check_id는 사람이 참조할 때 쓰고, name은 코드에서 필터링할 때 씁니다. label은 에이전트가 리포트에 인용할 때 자연어 설명으로 사용합니다. 이렇게 세 종류의 식별자를 두는 게 과잉 설계처럼 보이지만, 실제로 사용처가 다릅니다.


구현 2 — data-validator 에이전트: 이중 모드 설계

data-validator.md는 Claude Code의 에이전트 프롬프트 파일입니다. 이 에이전트에는 두 가지 실행 모드를 설계했습니다.

Mode 1: 스케줄 모드

## Mode 1 — Scheduled (autonomous)
트리거: etl_daily.sh에서 validate_data_integrity --format=json 실행 후 자동 호출
입력: .etl-results/integrity-check.json
출력: .etl-results/dv-analysis.txt

### 분석 절차
1. integrity-check.json을 읽는다
2. 각 CHECK의 passed/failed를 확인한다
3. failed 항목에 대해 위험 등급을 평가한다
4. 종합 소견을 dv-analysis.txt에 작성한다

Mode 2: 대화형 모드

## Mode 2 — Interactive (on-demand)
트리거: 메인 세션에서 "data-validator에게 물어봐" 형태로 호출
입력: 사용자가 지정한 파일 또는 직접 질문
출력: 대화 내에서 직접 응답

같은 에이전트가 새벽에는 자율 분석, 낮에는 문답 형태로 동작합니다. 프롬프트 하나로 두 모드를 지원하는 이유는, 도메인 지식(검증 기준, 위험 판단 로직)을 한 곳에서 관리하기 위함입니다.

위험 등급 분류 기준

### Risk Assessment
- **GREEN**: 모든 CHECK 통과. 정상 운영.
- **YELLOW**: 1~2개 CHECK 실패, 서비스 영향 없음.
  예) 마이너 카운트 불일치, 비활성 레코드 참조
- **RED**: 핵심 CHECK 실패, 서비스 영향 가능.
  예) product count 불일치, 활성 제품의 ingredient 누락

GREEN/YELLOW/RED 분류는 단순하지만, 뒤에 나올 etl_daily_report에서 후속 조치 권장을 자동 생성하는 데 핵심 입력이 됩니다.


구현 3 — ceo-briefing 검토와 기각

Polsia(AI가 운영하는 1인 회사)에서 영감을 받아 CEO 브리핑 에이전트도 설계했습니다. 이 에이전트의 역할은 “DV 분석을 흡수해서 비즈니스 관점으로 레이어링”하는 것이었습니다.

설계했던 분석 방법론

## 분석 원칙: Trust but verify
1. data-validator의 dv-analysis.txt를 읽는다
2. 동료의 분석을 신뢰하되, 원본 데이터(integrity-check.json)와 교차 검증한다
3. 기술적 소견을 비즈니스 임팩트로 변환한다
   - "orphan ingredients 23건" → "사용자 검색 시 23개 제품 성분 정보 누락 가능"
4. 권장 조치를 우선순위와 함께 제시한다

“Trust but verify”는 흥미로운 패턴이었습니다. 에이전트 B가 에이전트 A의 분석을 그대로 복사하는 게 아니라, 원본 데이터로 돌아가서 한 번 더 확인합니다. 에이전트 간 교차검증이 자동으로 발생하는 구조입니다.

기각 이유

그러나 현재 규모에서 CEO 에이전트는 기각했습니다. 이유는 단순합니다.

  1. Claude Code 메인 세션이 이미 오케스트레이터 역할을 하고 있다. 파운더가 직접 대화하는 메인 세션이 곧 CEO의 판단 레이어다.
  2. Telegram 알림의 수신자가 파운더 1명이다. 경영진에게 요약 보고하는 상황이 아니라, 기술을 이해하는 사람이 직접 받아보는 상황이다.
  3. 레이어가 하나 더 추가되면 latency와 토큰 비용이 증가한다. 파일 릴레이 단계가 늘어날수록 전체 파이프라인 실행 시간이 길어진다.

대신, CEO 에이전트가 하려던 “비즈니스 해석” 역할은 etl_daily_report의 룰 기반 권장 시스템으로 흡수했습니다. AI 에이전트보다 if-else가 적합한 영역이었습니다.


구현 4 — etl_daily_report: 모든 것을 모으는 최종 조립기

etl_daily_report 커맨드는 파이프라인의 마지막 단계입니다. 앞 단계들의 출력을 모두 흡수해서 Telegram 메시지로 조립합니다.

—integrity-json 옵션

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            '--integrity-json', type=str, default=None,
            help='Path to integrity check JSON from validate_data_integrity'
        )

파일 경로를 명시적으로 받는 이유는, 하드코딩된 경로 대신 etl_daily.sh가 경로를 주입하도록 설계했기 때문입니다. 파이프라인 오케스트레이터(쉘 스크립트)가 파일 경로를 관리하고, 각 단계는 자기가 받은 경로만 봅니다.

_build_agent_recommendations(): 룰 기반 권장

def _build_agent_recommendations(self, integrity_data, dv_analysis):
    """무결성 결과 + DV 분석을 기반으로 후속 조치를 권장한다."""
    recommendations = []

    if integrity_data:
        failed = [c for c in integrity_data['checks'] if not c['passed']]

        for check in failed:
            if check['check_id'] in ('CHK-001', 'CHK-002'):
                recommendations.append({
                    'priority': 'HIGH',
                    'action': f"[{check['check_id']}] 핵심 카운트 불일치 — 즉시 확인 필요",
                    'detail': check['message']
                })
            else:
                recommendations.append({
                    'priority': 'LOW',
                    'action': f"[{check['check_id']}] {check['label']} — 모니터링 권장",
                    'detail': check['message']
                })

    if dv_analysis:
        # DV 에이전트의 위험 등급을 파싱
        if 'RED' in dv_analysis:
            recommendations.insert(0, {
                'priority': 'CRITICAL',
                'action': 'DV 에이전트 RED 판정 — 파이프라인 점검 필요',
                'detail': dv_analysis[:200]
            })

    return recommendations

이 함수가 CEO 에이전트를 대체합니다. AI가 매번 토큰을 소비하며 “이 수치는 비즈니스적으로…”를 생성하는 대신, 룰 기반으로 즉시 분류합니다. CHK-001(제품 카운트)과 CHK-002(성분 매핑)는 핵심 지표이므로 HIGH, 나머지는 LOW. DV 에이전트가 RED를 내렸으면 CRITICAL.


대안 비교: 왜 “스케줄 체인 + 파일 릴레이”인가

세 가지 방식을 검토했습니다.

A안: EC2에서 Claude CLI 직접 호출

# etl_daily.sh에서 직접
claude --agent data-validator --input .etl-results/integrity-check.json
claude --agent ceo-briefing --input .etl-results/dv-analysis.txt

기각 이유: EC2 인스턴스에 Claude CLI를 설치하고 인증을 유지해야 합니다. t3.micro의 1GB RAM에서 Claude CLI + Docker 앱을 동시에 돌리면 OOM 위험이 높습니다. 또한 CLI 세션 관리(인증 토큰 갱신 등)가 추가 부담입니다.

B안: 스케줄 체인 + 파일 릴레이 (선택)

cron → etl_daily.sh → validate_data_integrity → (파일) → DV 에이전트 → (파일) → etl_daily_report → Telegram

선택 이유: 각 단계가 독립적이고, 실패해도 다음 단계가 graceful하게 동작합니다. 파일이 없으면 해당 섹션을 건너뛸 뿐, 전체가 죽지 않습니다. EC2에는 쉘 스크립트만 돌면 되고, 에이전트는 Claude Code 스케줄로 별도 실행됩니다.

C안: 수동 운영

매일 아침 → 직접 로그 확인 → 직접 판단 → 직접 알림

기각 이유: 솔로 파운더가 매일 아침 로그를 수동 확인하는 건 지속 가능하지 않습니다. 하루 빠뜨리면 무결성 이슈를 놓칩니다.

기준A. EC2 CLIB. 스케줄 체인C. 수동
EC2 리소스 부담높음 (CLI + Docker)낮음 (쉘만)없음
실패 격리낮음 (CLI 크래시 시 전체 중단)높음 (단계별 독립)-
유지보수CLI 인증 관리파일 경로 관리시간 비용
확장성에이전트 추가 용이에이전트 추가 용이한계 있음

인프라: etl_daily.sh 9단계 파이프라인

B안의 실체는 etl_daily.sh 쉘 스크립트입니다. 기존 ETL 스크립트를 9단계 파이프라인으로 확장했습니다.

#!/bin/bash
# /home/ec2-user/scripts/etl_daily.sh

RESULTS_DIR="/home/ec2-user/.etl-results"
LOG_DIR="/home/ec2-user/logs"
DATE=$(date +%Y%m%d)
LOG="$LOG_DIR/etl_$DATE.log"

mkdir -p "$RESULTS_DIR" "$LOG_DIR"

# --- Stage 1~4: ETL 실행 ---
echo "[$(date)] Stage 1: ETL i1250 start" >> "$LOG"
docker exec checkeat-app \
  python manage.py import_i1250 --resume >> "$LOG" 2>&1

echo "[$(date)] Stage 2: ETL c002 start" >> "$LOG"
docker exec checkeat-app \
  python manage.py import_c002 --resume >> "$LOG" 2>&1

# --- Stage 5: ETL 결과 수집 ---
echo "[$(date)] Stage 5: Collect ETL results" >> "$LOG"
docker exec checkeat-app \
  python manage.py collect_etl_results \
  --output /app/.etl-results/latest.json >> "$LOG" 2>&1

# --- Stage 6: 무결성 검증 (기계) ---
echo "[$(date)] Stage 6: Data integrity check" >> "$LOG"
docker exec checkeat-app \
  python manage.py validate_data_integrity \
  --format=json \
  --output /app/.etl-results/integrity-check.json >> "$LOG" 2>&1

# --- Stage 7: DV 에이전트 분석 대기 ---
# DV 에이전트는 Claude Code 스케줄로 별도 실행.
# 이 단계에서는 DV 분석 파일 존재 여부만 확인.
echo "[$(date)] Stage 7: Check DV analysis" >> "$LOG"
DV_FILE="$RESULTS_DIR/dv-analysis.txt"
if [ -f "$DV_FILE" ]; then
  echo "  DV analysis found" >> "$LOG"
  DV_FLAG="--dv-analysis $DV_FILE"
else
  echo "  DV analysis not found, skipping" >> "$LOG"
  DV_FLAG=""
fi

# --- Stage 8: 리포트 생성 + Telegram 발송 ---
echo "[$(date)] Stage 8: Generate report" >> "$LOG"
docker exec checkeat-app \
  python manage.py etl_daily_report \
  --integrity-json /app/.etl-results/integrity-check.json \
  $DV_FLAG >> "$LOG" 2>&1

# --- Stage 9: 정리 ---
echo "[$(date)] Stage 9: Cleanup" >> "$LOG"
# 어제 분석 파일 삭제 (오늘 것만 유지)
find "$RESULTS_DIR" -name "*.txt" -mtime +1 -delete
find "$RESULTS_DIR" -name "*.json" -mtime +1 -delete

echo "[$(date)] Pipeline complete" >> "$LOG"

docker-compose 볼륨 공유

.etl-results 디렉토리가 호스트와 컨테이너 간에 공유되어야 합니다.

services:
  app:
    image: checkeat-app
    volumes:
      - etl-results:/app/.etl-results
      # ...

volumes:
  etl-results:
    driver: local
    driver_opts:
      type: none
      device: /home/ec2-user/.etl-results
      o: bind

호스트의 /home/ec2-user/.etl-results와 컨테이너의 /app/.etl-results가 같은 디렉토리를 가리킵니다. Django 커맨드가 컨테이너 안에서 쓴 JSON을 호스트의 쉘 스크립트가 읽을 수 있고, Claude Code 스케줄 에이전트가 쓴 분석 파일을 다시 컨테이너 안의 Django 커맨드가 읽을 수 있습니다.


에이전트 팀 구성

파이프라인 외에도, 개발 과정 전반에서 활용하는 에이전트 팀이 있습니다.

에이전트역할주요 사용
planner기획→기술 명세 번역기능 설계 시 요구사항을 기술 태스크로 분해
qa-detective버그 탐정코드 리뷰 시 엣지케이스 공격
data-validator데이터 무결성 검증ETL 파이프라인 (스케줄) + 수동 검증 (대화형)

교차검증 패턴이 핵심입니다. planner가 명세를 쓰면 qa-detective가 공격하고, data-validator가 데이터 관점에서 검증합니다. 메인 세션이 이들의 의견을 종합해서 최종 판단합니다.

CEO 에이전트를 별도로 두지 않은 이유를 다시 정리하면: 메인 세션이 이미 오케스트레이터입니다. 에이전트는 전문 분야의 깊은 분석을 담당하고, 종합 판단은 도메인을 가장 잘 아는 파운더 + 메인 세션이 합니다. 현재 규모(1인 운영, 에이전트 3~4개)에서 오케스트레이터를 에이전트로 분리하면 레이어만 하나 늘어나는 오버엔지니어링입니다.


전체 아키텍처 요약

새벽 3:00  cron → etl_daily.sh 시작
           ├── Stage 1~4: ETL 실행 (import_i1250, import_c002, ...)
           ├── Stage 5: collect_etl_results → latest.json
           ├── Stage 6: validate_data_integrity → integrity-check.json

새벽 4:00  Claude Code Schedule → data-validator 에이전트
           ├── integrity-check.json 읽기
           ├── 위험 평가 (GREEN/YELLOW/RED)
           └── dv-analysis.txt 쓰기

새벽 5:00  cron → etl_daily.sh (Stage 7~9)
           ├── Stage 7: dv-analysis.txt 존재 확인
           ├── Stage 8: etl_daily_report (모든 파일 흡수 → Telegram)
           └── Stage 9: 정리

아침 7:00  파운더 기상 → Telegram에서 리포트 확인

시간 간격이 통신 프로토콜입니다. Stage 6 이후 DV 에이전트에게 1시간의 작업 시간을 줍니다. DV 에이전트가 늦거나 실패해도, Stage 7에서 파일 부재를 감지하고 해당 섹션 없이 리포트를 생성합니다.


한계와 솔직한 상태

이 시스템은 설계하고 구현했지만, 아직 실전 운영에서 검증되지 않았습니다. 솔직하게 알려진 한계를 정리합니다.

에이전트 간 실시간 피드백 불가

DV 에이전트가 RED를 판정해도, etl_daily_report가 실행되기 전까지 아무 일도 일어나지 않습니다. 긴급 상황에 대한 즉시 알림이 없습니다. 현재는 시간 간격(1시간)에 의존하는데, 이것이 실제 운영에서 충분한지는 돌려봐야 압니다.

파일 기반 통신의 취약점

아직 답을 모르는 질문들

이 질문들은 실제로 파이프라인을 돌리기 시작하면 답이 나올 것입니다.


교훈