XGEN 15일 무료 체험 — 설치 없이 브라우저에서 바로 시작하세요체험 신청
PlateerAI Labs
·Reading Time | 3 min
작성 | 김진수

엔진 코어와 제품 통합 계층을 분리한 이유(2편)

범용 실행 순서와 XGEN의 데이터·권한·캔버스 변환을 ServiceProvider, registry, 제품 연결 계층으로 나눈 과정을 설명합니다.

엔진 코어와 제품 통합 계층을 분리한 이유(2편) — 커버 일러스트Series · 하네스 개발기시리즈 2번째 글 · 전체 10편 보기

에이전트 하네스 설계 · 2/10

기존 실행기에 판정 로직을 직접 넣지 않고 독립 상태 머신을 만든 이유는 제품 변경 범위를 줄이기 위해서였습니다. 그런데 Python으로 옮긴 실행기를 XGEN Agentic AI Platform(이하 XGEN)의 제품 워크플로우에 연결하자 같은 결합이 다른 모습으로 돌아왔습니다. 엔진 코드는 별도 모듈에 있었지만 실행하려면 제품의 설정 객체와 데이터베이스 세션, 캔버스 노드 클래스를 먼저 불러와야 했습니다. 저장소가 나뉘었다고 의존성이 나뉜 것은 아니었습니다.

제품 안에서만 실행할 때는 이 결합이 편합니다. 설정 서비스에서 모델 이름을 바로 읽고, 노드 레지스트리에서 도구를 찾고, 제품 이벤트 형식으로 로그를 내보내면 됩니다. 하지만 같은 엔진을 독립 테스트에 올리면 제품 패키지가 없다는 이유로 import부터 실패합니다. 캔버스 스키마가 바뀔 때 범용 상태 머신도 함께 수정해야 했습니다.

4월 중순부터 말까지의 작업은 코드를 다른 폴더로 옮기는 일이 아니라 이 의존 방향을 바로잡는 일이었습니다. 기준은 하나였습니다. 제품은 하네스를 알아도 되지만, 하네스의 실행 원리는 제품을 몰라야 한다.

제품 객체 대신 필요한 동작만 받았습니다

처음에는 엔진 함수에 제품 객체를 인자로 넘겼습니다. 전역 import는 줄었지만 엔진이 여전히 그 객체의 필드와 생명주기를 알고 있었습니다. 테스트를 하려면 거대한 가짜 제품 객체를 만들어야 했고, 제품 모델의 필드 하나가 바뀌면 엔진도 깨졌습니다.

4월에 먼저 끊은 경계는 XGEN 서비스 접근이었습니다. ServiceProvider 아래에서 데이터베이스, 설정, MCP, 문서 서비스를 각각 작은 계약으로 노출하고, 도구 조회와 실행은 별도 ToolSource로 받았습니다. 코어가 요구한 것은 제품 객체 전체가 아니라 다음과 같은 동작이었습니다.

DatabaseService → 실행에 필요한 영속 데이터를 읽고 쓴다
ConfigService   → 제품 설정을 실행 설정으로 제공한다
MCPService      → 등록된 MCP 연결을 조회한다
DocumentService → 문서와 검색 자원을 제공한다
ToolSource      → 도구를 찾고 정해진 입력으로 호출한다

이후 모델 공급자와 세션·메모리, 실행 이벤트가 확장되면서 LLMProvider, SessionStore, EventEmitter 같은 계약이 추가됐습니다. 후대의 인터페이스를 4월 설계에 한꺼번에 투영하기보다, 제품 서비스 경계를 먼저 끊고 실행 기능이 늘 때마다 같은 원칙으로 계약을 확장한 것입니다.

실제 구현이 제품 데이터베이스인지 메모리 저장소인지, 캔버스 노드인지 외부 MCP 서버인지는 코어가 결정하지 않습니다. 이 방식으로 경계를 세우자 제품 환경 없이도 가짜 모델과 도구만으로 상태 전이 전체를 실행할 수 있었습니다.

작은 인터페이스가 단순히 테스트를 편하게 만든 것은 아닙니다. 제품이 제공해야 할 최소 계약을 드러냈습니다. 엔진이 새 제품 필드를 요구하기 시작하면 인터페이스 변경으로 보이고, 제품의 내부 객체가 슬며시 코어로 들어오는 것도 막을 수 있습니다.

실행 순서와 제품 정책을 갈라놓았습니다

인터페이스만 나눈다고 경계가 완성되지는 않습니다. 특정 모델 이름, 조직 전용 도구, 한국어 검색 별칭, 기본 컬렉션 같은 값을 코어에 넣으면 제품 정책이 다른 모습으로 남습니다.

코어가 알아야 하는 것은 정책을 언제 적용할지입니다. 실행 전 입력을 볼지, 모델 응답 뒤에 검사할지, 도구 호출 직전에 막을지, 회차 경계에서 비용과 반복 횟수를 확인할지는 상태 머신의 일부입니다. 반면 무엇을 차단할지, 어느 도구를 노출할지, 기본 한도를 얼마로 둘지는 제품이 정합니다.

이 구분을 적용하면 엔진의 단계는 안정적으로 유지됩니다. 제품은 공개된 설정과 검사 지점에 자신의 정책을 연결하고, 코어는 그 값이 데이터베이스에서 왔는지 캔버스에서 왔는지 모른 채 실행합니다. 새 제품 요구가 생길 때 상태 머신 안에 조건문을 추가하는 대신 연결 계층의 변환과 정책 구현을 바꿀 수 있습니다.

확장은 조건문이 아니라 등록 지점으로 넣었습니다

공급자와 도구 종류가 적을 때는 코어에 if provider == ... 같은 분기를 넣는 것이 빠릅니다. 하지만 제품별 분기가 늘어나면 코어 릴리스가 모든 통합 변경의 병목이 됩니다. 확장 패키지를 제거해도 관련 조건문이 남고, 선택하지 않은 제품 의존성이 설치 과정에 따라붙습니다.

그래서 공급자, 도구 소스, 정책 검사, 평가 전략, 세션 저장소를 registry에 등록하게 했습니다. 별도 패키지에서 제공하는 구현은 Python entry_points로 발견할 수 있게 했습니다. 코어는 제품 이름을 알지 않고 등록 이름과 입력 계약만 확인합니다.

등록 방식도 마법처럼 자동으로 연결되게 두지는 않았습니다. 같은 이름의 구현이 충돌하면 어느 것을 선택할지, 플러그인을 불러오지 못하면 실행을 중단할지 기능을 비활성화할지 정해야 합니다. 입력 스키마와 오류 형식도 공개 계약이 됩니다. 확장 지점을 만든다는 것은 조건문을 없애는 것뿐 아니라 발견, 선택, 실패의 규칙을 정하는 일이었습니다.

캔버스의 의미는 bridge에서 번역했습니다

가장 많은 제품 의존성은 캔버스 실행 경로에 있었습니다. 노드와 포트, 사용자 권한, 스트리밍 이벤트는 XGEN에서 중요한 정보지만 범용 엔진의 개념은 아닙니다. 이 코드를 엔진 안에 두면 캔버스 기능이 바뀔 때마다 독립 실행기까지 함께 배포해야 합니다.

4월 말 이 책임을 harness_bridge로 모았습니다. bridge는 캔버스의 노드와 포트를 일반 하네스 설정으로 바꾸고, 연결된 노드를 ToolSource로 구성하며, 사용자와 워크플로우의 권한을 실행 정책에 연결합니다. 엔진 이벤트를 제품의 스트리밍 응답으로 바꾸는 일도 이 계층이 맡습니다.

캔버스·제품 API → harness_bridge → 하네스 공개 계약 → 상태 머신

중요한 점은 bridge가 상태 머신의 내부 필드를 직접 고치는 우회로가 아니라는 것입니다. 모든 제품 값은 공개된 설정과 인터페이스를 통해 들어가야 합니다. 그래야 캔버스에서 직접 실행한 경로와 독립 실행 경로가 같은 단계와 전이를 사용할 수 있습니다.

서로 다른 환경에서 같은 경계를 검증했습니다

코어 테스트는 제품 패키지와 데이터베이스가 없는 환경에서 시작했습니다. 가짜 모델과 도구, 메모리 저장소만으로 도구 회차와 품질 재시도, 종료 조건이 동작해야 했습니다. 여기서 제품 import가 하나라도 필요하면 경계가 아직 닫히지 않은 것입니다.

그다음 bridge 테스트에서는 캔버스 값이 하네스 설정으로 변환되는지, 연결된 노드가 표준 도구 호출로 이어지는지, 엔진 이벤트가 스트리밍 응답에 담기는지를 확인했습니다. 독립 패키지는 제품 의존성이 없는 깨끗한 설치 환경에서 import와 실행 테스트를 따로 통과시켰습니다. 코어, 제품 변환, 독립 패키징을 각각의 경계에서 검증한 것입니다.

독립 엔진과 SDK, 제품 연결 계층은 배포 대상이 달랐습니다

이 시점의 핵심은 독립 엔진과 제품 연결 계층의 책임을 나눈 것이었습니다. 두 달 뒤에는 XGEN 서비스들이 이미 의존하는 배포 단위에 맞추기 위해 실행 코드를 xgen-sdkxgen_sdk.harness 안으로 옮겼습니다. 제품 서비스는 SDK 한 패키지를 올리면 하네스 실행 API와 XGEN용 저장소 구현을 함께 받을 수 있게 됐습니다.

그렇다고 독립 배포용 xgen-harness 패키지를 바로 없애지는 않았습니다. 제품 밖에서 엔진만 사용하는 소비자와 이미 특정 엔진 버전을 고정한 컴파일 산출물이 있었기 때문입니다. 독립 패키지는 제품 비종속 실행 계약과 기존 소비자의 호환 경로를 맡고, SDK 흡수본은 XGEN의 배포 단위와 통합 API를 맡았습니다. 이후 엔진 변경을 SDK에 먼저 반영하고 독립 저장소에는 호환 범위를 맞춰 옮기는 기준이 필요해진 이유입니다.

harness_bridge는 둘 중 어느 패키지로도 흡수하지 않았습니다. 캔버스 포트, 사용자 권한, 제품 스트리밍처럼 XGEN에서만 의미가 있는 변환까지 SDK 엔진에 넣으면 앞에서 끊은 의존성이 다시 생깁니다. 세 경로는 같은 엔진을 중복해서 홍보하기 위한 구성이 아니라 서로 다른 배포 책임을 가집니다.

독립 배포 xgen-harness → 제품 밖 실행·기존 컴파일 산출물 호환
xgen_sdk.harness       → XGEN 서비스가 소비하는 엔진 배포 단위
harness_bridge         → 캔버스·권한·스트리밍의 XGEN 전용 변환

6월의 SDK 재배치는 4월에 만든 인터페이스를 유지한 채 배포 위치를 바꾼 별도 변화였습니다.

엔진 분리는 저장소를 하나 더 만드는 일이 아니었습니다. 제품 밖에서 상태 머신을 실행할 수 있고, 제품은 코어를 수정하지 않고 자신의 데이터와 권한을 번역할 수 있어야 경계가 생깁니다. 다음 편에서는 이 경계를 실행 중인 프로세스에만 두지 않고, 다른 환경에 설치할 산출물 안에 어떤 실행 계약을 남길지 살펴봅니다.


이전 편 → 검증 루프를 실행 상태로 나눈 이유(1편) 다음 편 → 고정된 실행 계약을 하나의 MCP 도구로 내보내기(3편)

#하네스#아키텍처#의존성 설계
블로그 목록으로