로컬 경로를 사용하여 에이전트 간에 파일을 전달하는 방식 중단하기
요약
로컬 개발 환경과 달리 운영(Production) 환경의 에이전트는 일시적이고 격리된 파일 시스템을 사용하므로 로컬 경로 기반의 파일 전달 방식은 부적합합니다. 에이전트가 생성한 결과물을 안정적으로 관리하기 위해서는 단순한 파일 저장이 아닌, 컨텍스트가 포함된 '아티팩트(Artifact)' 개념과 오브젝트 스토리지를 활용한 설계가 필요합니다.
핵심 포인트
- 로컬의 파일 시스템 의존성은 운영 환경의 서버리스/컨테이너 환경에서 작동하지 않음
- 에이전트 출력물은 단순한 파일이 아닌 컨텍스트를 포함한 '아티팩트'로 취급해야 함
- 운영 환경에서는 내구성이 있는 오브젝트 스토리지를 사용하여 결과물을 저장해야 함
- 사용자에게는 로컬 경로가 아닌 접근 가능한 링크를 제공하는 설계가 필수적임
저는 에이전트를 로컬에서 실행하는 것에 매우 익숙해져 있었습니다.
워크플로우는 간단했습니다. 에이전트를 실행하고, 에이전트가 내 파일 시스템에 출력을 쓰도록 둔 다음, ./outputs 폴더에서 모든 것을 검사하는 것이었습니다.
마크다운 (Markdown) 보고서, JSON 파일, 스크린샷, 차트 — 에이전트가 생성하는 것이 무엇이든 그곳에 바로 있었습니다.
그러다 저는 그것을 배포했습니다.
에이전트의 어떤 것도 변하지 않았습니다.
환경의 모든 것이 변했습니다.
파일(보고서, 데이터셋, 이미지, JSON 덤프)을 생성하는 에이전트를 구축한다면, 아마도 여러분은 이 격차를 경험했을 것입니다.
로컬 개발은 이를 숨겨줍니다.
운영 (Production) 환경은 첫날부터 이를 직면하게 합니다.
로컬 버전
사용자의 기기에서 에이전트 출력을 유지하는 것은 사소한 일입니다:
from pathlib import Path
def save_report(content: bytes, run_id: str) -> Path:
...
그게 전부입니다. 바이트를 쓰고, 경로를 얻고, 다음으로 넘어갑니다.
디렉토리를 나열하거나, 파일을 cat 하거나, PDF를 열거나, 파이프라인의 다음 스크립트에 경로를 전달할 수 있습니다.
한 명의 사용자가 한 대의 노트북에서 하나의 에이전트를 실행하는 경우라면, 이것은 완벽하게 괜찮습니다.
문제는 로컬 개발이 아닙니다.
문제는 “내 노트북에서 작동한다”를 “나는 스토리지 계층 (storage layer)을 가지고 있다”로 착각하는 것입니다.
운영 버전
운영 환경의 에이전트는 신뢰할 수 있는 ./outputs/ 폴더를 가질 수 없습니다.
에이전트들은 파일 시스템이 일시적이거나, 격리되어 있거나, 혹은 둘 다인 환경에서 실행됩니다.
서버리스 (Serverless) 함수는 /tmp를 제공할 수도 있지만, 이는 실행 환경에 국한되며 종종 크기가 제한됩니다. 컨테이너 (Containers)는 재시작될 때 로컬 상태를 잃습니다. 백그라운드 워커 (Background workers), 큐 (queues), 그리고 오케스트레이터 (orchestrators)는 각 태스크를 서로 다른 머신에서 실행할 수 있습니다.
그리고 재시도 (retries)는 예외적인 상황이 아닙니다. 그것은 시스템의 일부입니다.
여러분의 오케스트레이터는 결국 실패한 단계를 다시 실행할 것이며, 이제 동일한 논리적 출력이 두 번 생성됩니다.
그리고 루프 내의 인간 (human in the loop)이 있습니다.
에이전트는 사람들이 실제로 읽어야 하는 것들을 생성합니다: 컴플라이언스 (compliance) PDF, 분석 요약, 생성된 슬라이드, CSV 내보내기, 차트, 스크린샷, 디버그 번들.
그 사람들은 여러분의 워커 노드 (worker node)에 대한 SSH 접근 권한이 없습니다.
그들에게 필요한 것은 링크이지, 결코 보지 못할 머신의 파일 경로가 아닙니다.
따라서 프로덕션 체크리스트는 로컬 개발 환경과는 매우 다르게 보이기 시작합니다:
| 로컬 (Local) | 프로덕션 (Production) |
|---|---|
path.write_bytes() | 내구성이 있는 오브젝트 스토리지 (durable object storage)에 업로드 |
| ... | ... |
저는 동일한 벽에 부딪힌 몇몇 팀들과 이야기를 나누었습니다.
에이전트 로직은 완성되었습니다.
이제 아티팩트 배관 작업 (artifact plumbing)이 시작됩니다.
파일 (Files) vs 아티팩트 (artifacts)
이것이 제가 이 문제를 생각하는 방식을 바꾼 차이점입니다:
파일 (File)은 경로에 있는 바이트 (bytes)입니다.
아티팩트 (Artifact)는 파일에 컨텍스트 (context)가 더해진 것입니다.
그 컨텍스트가 바로 에이전트 작업이 끝난 후 결과물을 사용할 수 있게 만드는 핵심입니다.
예를 들어:
- 어떤 실행 (run)이 이를 생성했는가: session_id, 파이프라인 실행 (pipeline run), 배치 작업 (batch job)
- 어떤 에이전트가 이를 생성했는가: agent_id, 단계 (stage), 모델 버전 (model version)
- 그것의 실제 정체는 무엇인가: 콘텐츠 타입 (content type), 크기 (size), 사용자 정의 메타데이터 (custom metadata)
- 언제 만료되어야 하는가: TTL 또는 생명주기 규칙 (lifecycle rules)
- 바이트가 어디에 있는지 모르는 상태에서 나중에 어떻게 검색할 것인가: 안정적인 ID (stable ID)
- 인프라 외부의 누군가와 어떻게 공유할 것인가: 임시 다운로드 링크 (temporary download link)
디스크에 놓여 있는 PDF는 파일입니다.
하지만 session_id=pipeline_run_42, agent_id=report-writer, model=claude-sonnet-4라는 태그가 붙어 있고, art_2xk9f7v3m1p0로 검색 가능하며, 30일 후에 만료되도록 설정된 PDF는 어떨까요?
그것은 아티팩트입니다.
여러분의 에이전트는 여전히 파일을 생성할 수도 있습니다.
하지만 다운스트림 에이전트 (downstream agents), 디버그 도구 (debug tools), 프로덕션 워크플로우 (production workflows), 그리고 Slack에서 기다리고 있는 사람들은 모두 아티팩트를 필요로 합니다.
결국 구축하게 되는 메타데이터 계층 (metadata layer)
대부분의 팀은 아티팩트 저장소 (artifact store)를 구축하는 것부터 시작하지 않습니다. 그들은 S3 (또는 R2, 또는 GCS)로 시작하며, 오브젝트 키 (object keys)만으로는 충분하지 않다는 느낌을 서서히 갖게 됩니다.
저희의 사용자 조사 (user research)를 포함하여 제가 계속 목격하는 패턴은 다음과 같습니다.
먼저, 오브젝트 스토리지에 바이트를 저장합니다:
import hashlib
import boto3
...
그러다 오브젝트 키만으로는 충분하지 않다는 것을 깨닫게 됩니다.
어떤 실행이 파일을 생성했는지, 어떤 에이전트가 이를 만들었는지, 어떤 종류의 출력물인지, 언제 만료되어야 하는지, 그리고 나중에 어떻게 찾을 수 있는지를 알아야 합니다.
그래서 메타데이터 테이블 (metadata table)을 추가합니다:
CREATE TABLE artifacts (
id text PRIMARY KEY,
tenant_id uuid NOT NULL,
...
그다음 이를 API로 래핑(wrap)합니다:
def create_artifact(file_path, session_id, agent_id, metadata=None):
key = upload_file(file_path, tenant_id=current_tenant())
artifact_id = f"art_{generate_id()}"
...
축하합니다, 이제 아티팩트 저장소(artifact store)를 구축하는 단계에 들어섰습니다.
그다음 나머지 80%의 작업이 나타납니다:
- 재시도에 안전한 업로드를 위한 멱등성 키 (Idempotency keys)
- 재시도로 인해 스토리지 비용이 두 배로 들지 않도록 하는 콘텐츠 해시 중복 제거 (Content-hash deduplication)
- 올바른 만료 의미론 (expiry semantics)을 가진 서명된 업로드/다운로드 URL (Presigned upload/download URLs)
- 좀비 하위 에이전트가 "종료된" 실행에 내용을 추가할 수 없도록 하는 세션 봉인 (Session sealing)
- 자체 만료 모델을 가진 인간용 공개 다운로드 링크
- 소프트 삭제 (Soft delete) 및 가비지 컬렉션 (Garbage collection)
- 사용량 측정 (Usage metering) 및 할당량 강제 (Quota enforcement)
- 실제로 쿼리가 가능한 메타데이터 필터링 (Metadata filtering)
저는 엔지니어들이 이러한 유형의 래퍼(wrapper)를 구축하는 데 시간을 소비하면서도 중복 제거(dedup), TTL, 또는 세션 의미론을 제대로 구현하지 못하는 것을 지켜봐 왔습니다.
이는 해당 팀들을 비난하려는 것이 아닙니다. 이는 필수적인 배관 작업(plumbing)입니다. 하지만 필수적인 배관 작업은 여전히 배관 작업일 뿐입니다. 대부분의 팀은 에이전트 인프라를 재구축하는 것이 아니라, 자신의 제품에 그 시간을 쏟아야 합니다.
아티팩트 계층(artifact layer)이 처리해야 할 사항
이것을 직접 구축할지 아니면 목적에 맞게 설계된 계층을 사용할지 결정하고 있다면, 제가 사용할 기본적인 체크리스트는 다음과 같습니다.
실행 / 세션 그룹화 (Run / session grouping)
여러분은 한 가지 질문에 빠르게 답할 수 있어야 합니다:
이 파이프라인 실행(pipeline run)이 무엇을 생성했는가?
로그를 grep으로 검색하는 것이 아닙니다.
S3 접두사(prefix)를 나열하고 명명 규칙이 유지되었기를 바라는 것도 아닙니다.
단 한 번의 쿼리로 해결해야 합니다:
artifacta ls --session pipeline_run_42
세션은 여러분의 오케스트레이터(orchestrator)가 이미 사용 중인 것이 무엇이든 될 수 있어야 합니다: pipeline_run_42, daily_batch_20260313, customer_report_8841 등.
출력물을 그룹화하기 위해 별도의 "세션 생성" 단계가 필요해서는 안 됩니다.
에이전트 출처 (Agent provenance)
3주 후에 보고서가 잘못된 것처럼 보일 때, 무엇이 그것을 생성했는지 알아야 합니다.
어떤 에이전트인가?
어떤 모델인가?
워크플로의 어느 단계인가?
이는 agent_id와 메타데이터가 업로드 시점에 캡처되어야 함을 의미하며, 여전히 존재하기를 바라는 로그 속에 묻혀 있어서는 안 된다는 것을 의미합니다.
client.push(
"analysis.json",
session_id="pipeline_run_42",
...
메타데이터 (Metadata)
객체 스토리지 (Object storage)의 메타데이터만으로는 충분하지 않습니다.
헤더 (Headers)는 제한적이고, 쿼리하기 까다로우며, 파이프라인 (Pipeline) 전반에 걸쳐 일관성을 유지하기 어렵습니다.
아티팩트 (Artifact) 레코드와 함께 저장되고, 아티팩트 목록을 조회할 때 필터링할 수 있는 구조화된 메타데이터 (Structured metadata)가 필요합니다.
중복 제거 (Deduplication)
에이전트 시스템 (Agent systems)은 보통 두 가지 형태의 중복 제거가 필요합니다:
- 콘텐츠 해시 중복 제거 (Content-hash deduplication): 동일한 바이트 데이터에 대해 하나의 저장된 블롭 (Blob)만 유지.
- 멱등성 키 (Idempotency keys): 동일하게 재시도된 작업에 대해 동일한 응답을 보장.
이 둘은 서로 다른 문제를 해결합니다.
콘텐츠 해싱 (Content hashing)은 중복 저장을 방지합니다. 멱등성 (Idempotency)은 재시도로 인해 두 번째 논리적 아티팩트가 생성되는 것을 방지합니다.
이 두 가지를 혼동하는 것은 자체 제작한 래퍼 (Wrapper)에서 흔히 발생하는 버그입니다.
TTL (Time To Live)
아티팩트는 기본적으로 만료되어야 합니다.
실험 (Experiment), 배치 실행 (Batch run), 또는 디버그 파일 (Debug file)이 아무도 정리하는 것을 잊었다는 이유로 영원히 남아있어서는 안 됩니다.
스토리지 수명 주기 규칙 (Storage lifecycle rules)이 도움이 되지만, 이는 보통 버킷 (Bucket) 또는 접두사 (Prefix) 수준에서 작동합니다. 이 규칙들은 아티팩트 메타데이터를 이해하지 못하므로, 아티팩트별 만료 처리를 필요 이상으로 어렵게 만듭니다.
다운로드 링크 (Download links)
사람에게 필요한 것은 파일 경로 (File path)가 아니라 링크 (Link)입니다.
훌륭한 아티팩트 레이어 (Artifact layer)라면 구성 가능한 만료 기간을 가진 안정적인 다운로드 URL을 쉽게 생성할 수 있어야 합니다:
해당 링크는 내부 스토리지 상세 정보와 분리되어야 하며, 팀 동료, 고객 또는 워크플로 단계 (Workflow step)와 공유하기 쉬워야 합니다.
ID / 세션에 의한 검색 (Retrieval by ID / session)
다운스트림 에이전트 (Downstream agents)는 공유된 파일 시스템 경로 (Filesystem paths)를 통해 협업해서는 안 됩니다.
에이전트 A가 아티팩트를 푸시 (Push)하고 ID를 받습니다. 에이전트 B는 해당 ID로 가져오거나 (Pull), 세션을 목록화하여 메타데이터로 필터링합니다.
export ARTIFACTA_SESSION_ID="pipeline_run_42"
python extract.py # CSV를 푸시함
...
세션 봉인 (Session sealing) 또한 중요합니다.
실행 (Run)이 완료되면, 나중에 업로드된 항목이 실행을 조용히 오염시키는 대신 명확하게 실패해야 합니다:
409 Session 'pipeline_run_42' is sealed. No new artifacts can be added.
이를 위해 제가 만들고 있는 작은 도구
저는 AI 에이전트(AI agents)를 위해 특수 제작된 아티팩트 저장소(artifact store)인 Artifacta를 구축하고 있습니다.
이것은 오케스트레이터(orchestrator), 검색 엔진(search engine), 또는 에이전트 프레임워크(agent framework)가 아닙니다. 에이전트와 객체 스토리지(object storage) 사이의 계층입니다. CLI, MCP, Python SDK 및 REST API를 갖춘, 세션 인식(session-aware)이 가능하고 쿼리 가능한(queryable) 아티팩트 저장소입니다.
예를 들어:
pip install artifacta-cli
export ARTIFACTA_API_KEY="ak_live_..."
...
또는 Python에서:
from artifacta import Client
client = Client()
...
Artifacta가 모든 팀이 선택하는 해결책은 아닐지라도, 에이전트 워크플로(agent workflows)에서 제가 계속해서 목격하고 있는 문제이기 때문에 이를 공유합니다.
다른 개발자들에게 던지는 질문
다른 팀들은 현재 이 문제를 어떻게 처리하고 있는지 궁금합니다:
- 프로덕션(production) 환경에서 에이전트의 출력물(outputs)은 어디로 가나요?
- 실행(run) 단위로 출력물을 어떻게 그룹화하나요?
- 재시도(retries), 중복 제거(deduplication), 사람 간의 공유(human sharing), 또는 정리(cleanup) 중 무엇이 가장 먼저 문제가 되었나요?
- 실행(runs)을 확정(finalize)하나요, 아니면 늦게 작업하는 워커(workers)가 여전히 출력물을 작성할 수 있나요?
- 전용 아티팩트 계층(artifact layer)이 유용한가요, 아니면 "S3 + Postgres + 얇은 래퍼(thin wrapper)"가 적절한 절충안(tradeoff)인가요?
여러분의 설정(setup)을 댓글로 남겨주세요. 특히 단순한 객체 스토리지(object storage)에 글루 코드(glue code)를 더한 방식이 아닌 접근 방식에 관심이 많습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기