본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 05:17

출력 오염 문제를 해결하고, 지속성(Persistence)과 TTL을 구축했습니다. 모두 v0.6에서 이루어졌습니다.

요약

AI 에이전트 워크플로의 안정성을 높이기 위해 v0.6 업데이트를 통해 출력 오염 문제 해결과 샌드박스 지속성을 구축했습니다. Docker 호출 분리를 통해 깨끗한 출력을 보장하고, SQLite를 도입하여 서비스 재시작 시에도 샌드박스 데이터를 유지할 수 있게 했습니다.

핵심 포인트

  • Docker 호출 분리로 의존성 설치 로그와 실행 결과 스트림을 격리하여 출력 오염 해결
  • AI 에이전트의 파싱 정확도를 위해 깨끗한 출력(Clean Output)을 API 계약의 일부로 간주
  • 인메모리 방식 대신 SQLite를 사용하여 샌드박스 레지스트리의 데이터 지속성 확보
  • TTL(Time-To-Live) 기능을 통한 자동 정리 시스템 구축

신뢰할 수 없는 AI 생성 코드를 안전하게 실행하는 것은 명백히 어려운 문제입니다.

하지만 때로는 에이전트 워크플로(Agent workflow)를 망가뜨리는 문제들이 지루한 인프라 작업처럼 보이기도 합니다.

v0.6은 다음과 같은 기반 작업(Plumbing)으로 시작되었습니다:

  • 지속 가능한 샌드박스 레지스트리 (Persistent sandbox registry)
  • TTL을 이용한 자동 정리 (Automatic cleanup with TTL)

필요하긴 하지만, 특별히 화려하지는 않은 작업들입니다.

그러다 테스트가 실패하기 시작했습니다.

출력 오염 (Output corruption) 문제

모든 실행 결과가 다음과 같이 반환되었습니다:

WARNING: Running pip as the 'root' user can result in broken permissions...
[notice] A new release of pip is available: 25.0.1 -> 26.1.2
hello world

실제 프로그램 출력값이 의존성 설치 노이즈(Dependency-installation noise) 아래에 파묻혀 버린 것입니다.

터미널을 읽는 사람에게는 짜증 나는 일입니다.

하지만 실행 출력을 파싱(Parsing)하는 AI 에이전트에게는, 이는 망가진 상태나 다름없습니다.

원인은 명확했습니다: 의존성 설치와 코드 실행이 단일 Docker 호출로 체이닝(Chained)되어 있었고, stderr가 stdout으로 리다이렉션(Redirected)되어 있었습니다.

모든 것이 동일한 스트림(Stream)에 섞여버린 것입니다.

해결책: 하나의 호출이 아닌 두 개의 Docker 호출

우리는 작업을 분리했습니다.

호출 1: 의존성을 조용히 설치합니다.

subprocess.run(
    [...dependency_install_command],
    stdout=subprocess.DEVNULL,
...

호출 2: 사용자 명령을 실행하고 그 출력을 캡처합니다.

result = subprocess.run(
    [...execution_command],
    stdout=subprocess.PIPE,
...

작은 변화이지만, 원칙이 중요합니다:

AI 에이전트를 위한 인프라를 구축할 때, 깨끗한 출력은 API 계약(API contract)의 일부입니다.

에이전트는 당신이 반환하는 것을 파싱합니다. 설치 로그, 경고, 그리고 런타임 출력(Runtime output)을 구분되지 않은 하나의 스트림으로 취급해서는 안 됩니다.

지속성 (Persistence): 인메모리 딕셔너리 대신 SQLite 사용

기존의 샌드박스 레지스트리는 Python 딕셔너리(Dictionary)였습니다.

서비스를 재시작하면 모든 샌드박스 기록이 사라졌습니다.

컨테이너는 여전히 존재할 수 있지만, Jhansi는 더 이상 그것들에 대해 알지 못했습니다. 서비스 재시작 후 재연결을 기대하는 모든 에이전트 워크플로는 실패하게 될 것이었습니다.

우리는 다음과 같은 사항들을 고려했습니다:

  • JSON: 단순하지만, 크래시(crash) 발생 시 부분 쓰기(partial writes) 및 데이터 손상(corruption)에 취약함
  • Redis: 네이티브 TTL(Time-To-Live)과 우수한 운영 모델을 제공하지만, 셀프 호스팅(self-host) 사용자가 실행해야 하는 별도의 서비스임
  • SQLite: 내구성이 있고 트랜잭션(transactional)을 지원하며, 이미 Python에 포함되어 있음

우리는 SQLite를 선택했습니다.

스키마는 의도적으로 작게 설계되었습니다:

CREATE TABLE IF NOT EXISTS sandboxes (
    id TEXT PRIMARY KEY,
    language TEXT NOT NULL,
...

ORM(Object-Relational Mapping)은 사용하지 않았습니다.

마이그레이션 프레임워크(migration framework)도 없습니다.

그저 SQLite가 잘하는 일을 SQLite가 하도록 두었습니다.

TTL: 생성 시간이 아닌 마지막 활동 시간 기준

각 샌드박스(sandbox)는 생성 후 초기 1시간 뒤로 설정된 expires_at 값을 할당받습니다.

중요한 결정 사항은 모든 실행(execution) 시 시계가 초기화된다는 점입니다:

new_expires = (
    datetime.now(timezone.utc)
    + timedelta(seconds=TTL_SECONDS)
...

백그라운드 태스크(background task)가 60초마다 실행되어 만료된 샌드박스를 제거합니다.

이를 통해 TTL은 연령(age) 기준이 아닌 활동(activity) 기준으로 작동하게 됩니다.

에이전트(agent)는 20분간의 분석 과정 동안 수십 번의 작은 실행을 수행할 수 있습니다. 생성 시간 기준의 TTL은 활발한 워크플로(workflow) 도중에 샌드박스를 종료시킬 수 있습니다.

하지만 마지막 활동(last-active) 기준의 TTL은 그렇지 않습니다.

활발한 샌드박스는 계속 사용 가능한 상태로 유지됩니다. 오직 유휴(idle) 상태인 것들만 정리됩니다.

이를 통해 가능해지는 것들

지속성(persistence)과 활동 기반 TTL을 통해, Jhansi 샌드박스는 신뢰할 수 있는 실행 프리미티브(execution primitives)로 거듭나고 있습니다:

샌드박스를 한 번 생성하고.

반복해서 사용하며.

서비스 재시작 시에도 살아남습니다.

활발한 작업이 에이전트 밑에서 사라지지 않을 것이라고 신뢰할 수 있습니다.

이것이 바로 더 오래 지속되는 에이전트 워크플로가 필요로 하는 기반입니다.

v0.7의 다음 예정 사항: Server-Sent Events를 통한 스트리밍 실행(streaming execution).

이제 출력을 확인하기 위해 전체 명령이 끝날 때까지 기다릴 필요가 없습니다.

Jhansi는 AI가 생성한 코드를 안전하게 실행하기 위한 오픈 소스 클라우드 샌드박스입니다.

다음 명령으로 셀프 호스팅할 수 있습니다:

docker compose up

AI 에이전트에게 필요한 것은 자격 증명(credentials)이 아니라 실행 환경입니다.

이 문제가 공감된다면 Star를 눌러주세요: https://github.com/jhansi-io/petri

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0