플러그인 시스템에서 가장 쉽게 구현할 수 있는 부분은 로딩이다.
주어진 모듈:
이를 호출하면 새로운 기능이 시스템에 추가된다.
컴포넌트를 프로세스 시작 시 한 번만 로딩하고 프로세스 종료 시 통째로 폐기한다면 이 모델은 대개 충분하다. 동적 Harness의 제약은 더 강하다. 플러그인은 비활성화되거나 교체되거나 핫 업데이트될 수 있다. 일부 플러그인은 의존성이 존재할 때만 실행되어야 하며, 의존성이 사라지면 언로드되었다가 의존성이 복구되면 다시 활성화되어야 한다.
Cordis는 이런 문제를 Spatiotemporal Composability라는 개념으로 묶는다. 2026년 8월 13일에 공개된 프리프린트 《A Programming Paradigm for Spatiotemporal Composability》는 문제를 서로 독립적인 두 차원으로 나눈다. [1]
- Temporal Composability: 컴포넌트가 시스템에서 빠져나갈 때, 그 컴포넌트가 만든 부작용을 완전히 되돌릴 수 있다.
- Spatial Composability: 컴포넌트가 환경에 대한 요구 사항을 선언하고, 환경이 변할 때마다 자신의 유효성을 동적으로 조정할 수 있다.
이 두 차원을 설명하기 위해 논문은 Effect, Coeffect, 그리고 통합된 Context를 도입한다.
이 글은 논문의 목차를 따라 형식적 정의를 반복하는 대신, 먼저 엔지니어링 문제를 정리한 다음 이 추상화가 왜 성립하는지 설명한다.
동적 플러그인이 처음 해결해야 할 문제는 “되돌리기”다
플러그인이 실행된 뒤에는 일반적으로 여러 외부 구조가 바뀐다.
로딩 후 실행 환경에는 세 가지 변화가 생긴다:
Tools + tool
Listeners + listener
Timers + timer
언로드가 플러그인 객체만 삭제한다면 이러한 변화는 그대로 남는다.
따라서 동적 플러그인의 수명 주기는 다음 상태만 기록해서는 안 된다:
plugin = loaded
그뿐만 아니라 플러그인이 환경에 가한 변경 사항도 기록해야 한다.
가장 직접적인 방법은 각 작업이 그에 대응하는 되돌리기 함수를 반환하도록 하는 것이다:
이제 논문에서 말하는 revertible effect에 상당히 가까워졌다.
한 번의 등록을 다음과 같이 상태 변환으로 표현할 수 있다:
Context C
│
│ apply effect E
▼
Context C'
컴포넌트를 안전하게 제거하려면 그에 대응하는 역연산이 존재해야 한다:
Context C'
│
│ revert E
▼
Context C
핵심은 메모리의 모든 바이트를 엄격히 복원할 필요가 없다는 점이다. 컴포넌트가 공유 실행 환경에 남긴 관찰 가능한 영향이 수명 주기 시스템에 의해 되돌려질 수 있으면 되는 것이다.
이것이 Temporal Composability의 엔지니어링적 의미다.
Effect는 Runtime이 소유해야 한다
모든 플러그인 작성자가 cleanup callback들을 직접 관리한다면 이론적으로는 작동할 수 있다. 하지만 플러그인 내부에 비동기 초기화, 중첩된 리소스, 여러 등록, 오류 롤백이 등장하면서 수명 주기는 쉽게 일관성을 잃는다.
더 안정적인 모델은 다음과 같다:
플러그인이 실행되는 동안 생성된 Effect는 현재 실행 단위에서 수집된다.
언로드할 때:
dispose runtime unit
↓
revert owned effects
그 결과 플러그인의 수명 주기는 “함수 하나를 호출하는 일”에서 “자원을 소유하는 실행 단위를 만드는 일”로 바뀐다.
Cordis는 이 실행 단위를 Fiber라고 부른다. [2][3]
주의할 점은 이것이 JavaScript의 coroutine이나 Effect-TS의 Fiber와는 다른 추상화라는 것이다. Cordis의 Fiber는 Plugin Runtime Instance에 더 가깝다. 플러그인 설정, 의존성 스냅샷, 수명 주기 상태, 현재 보유한 Effect를 저장한다.
DeepSeek Harness가 벤더링한 Cordis의 현재 소스에서 Fiber 상태는 다음과 같다:
PENDING
LOADING
ACTIVE
FAILED
UNLOADING
DISPOSED
이것만으로도 플러그인이 loaded=true/false 같은 불리언 값이 아니라, 의존성 대기, 로딩, 언로드, 실패를 거칠 수 있는 상태 머신임을 알 수 있다. [3]
되돌릴 수 있는 부작용만으로는 의존성을 처리할 수 없다
두 플러그인을 생각해 보자:
Plugin A
└── provides FileSystem
Plugin B
└── requires FileSystem
B가 A보다 먼저 시작하면 어떻게 될까?
기존 Plugin Manager는 로딩 순서에 의존할 수 있다:
A
↓
B
규모가 커지면 수동 정렬은 점차 의존성 그래프 관리 문제로 바뀌어 간다.
더 중요한 것은 동적 시스템에서 의존성이 실행 도중에 변할 수 있다는 점이다.
A는 B가 이미 실행된 뒤에도 언로드될 수 있다:
time ───────────────────────────>
A: ACTIVE ─────────── DISPOSED
B: ?
B가 계속 실행된다면 자신이 보유한 의존성이 더 이상 유효하지 않다. Runtime이 시작할 때만 의존성을 확인한다면 이 문제 역시 해결되지 않는다.
따라서 컴포넌트는 “내가 무엇을 수정하는가”뿐 아니라 “내가 실행되기 위해 환경이 무엇을 제공해야 하는가”도 설명해야 한다.
논문은 후자의 제약 조건을 Coeffect로 나타낸다. [1]
Effect는 컴포넌트가 환경에 가하는 작용을 설명한다:
component → environment
Coeffect는 환경이 컴포넌트에 제공해야 하는 조건을 설명한다:
environment → component
예를 들면 다음과 같다:
requires FileSystem
requires ToolRegistry
requires SessionStore
이 요구 조건들을 함께 보면 컴포넌트가 현재 실행할 수 있는지가 결정된다.
Reactive Coeffect는 의존성 변화를 처리한다
일반적인 의존성 주입은 보통 객체 생성 시점에 이루어진다:
construct B(FileSystem)
객체가 생성된 뒤에는 의존 관계가 대체로 고정된다.
동적 Harness의 요구 사항은 이보다 훨씬 동적이다:
논문은 Context 변화에 따라 컴포넌트의 유효성을 다시 판단하는 메커니즘을 reactive coeffect라고 부른다. [1]
여기서 “reactive”가 매우 중요하다.
의존성 검사가 한 번 수행하고 끝나는 constructor validation이 아니라, Runtime이 지속적으로 유지하는 하나의 관계라는 뜻이다:
Context changed
↓
which coeffects are affected?
↓
re-evaluate components
엔지니어링 구현에서는 시스템 전체를 계속 폴링할 필요가 없다. Service가 등록되거나 제거될 때 의존성 업데이트를 발동시키는 편이 더 합리적이다.
Cordis의 현재 구현은 바로 이 이벤트 기반 방식이다. Service가 변경되면 그 Service에 의존하는 Fiber에 알리고, Fiber는 자신의 의존성 epoch를 다시 계산한 뒤 ACTIVE를 유지할지, UNLOADING으로 들어갈지, 다시 LOADING할지를 결정한다. [3]
따라서 말하는 “재계산”은 무한히 실행되는 while (true)가 아니다:
환경에 구조적 변화가 생길 때 실행된다.
왜 Spatial Composability인가
“공간”은 물리적 위치를 떠올리게 하지만, 여기서 뜻은 같은 시점에 존재하는 컴포넌트들 사이의 조합 관계에 가깝다.
다음과 같다고 하자:
A provides X
B requires X, provides Y
C requires Y
어느 한 시점에 유효한 시스템은 설치된 모든 플러그인의 집합과 같지 않다. 현재 Context에서 충족할 수 있는 의존 관계가 이를 결정한다:
Installed:
A B C D E
Current Context:
A ACTIVE
B ACTIVE
C ACTIVE
D PENDING
E PENDING
X가 사라지면:
A removed
↓
X disappears
↓
B loses requirement
↓
B unloads
↓
Y disappears
↓
C unloads
공간적 조합성이 다루는 것은 바로 그 순간 “어떤 컴포넌트들이 함께 존재할 수 있는가”다.
Temporal Composability와 연결하면 동적인 변화의 전체 흐름이 완성된다:
依赖消失
↓
组件失效
↓
撤销组件 Effects
↓
组件提供的 Service 消失
↓
影响下一层依赖
연쇄 반응이 발생하지만, Runtime이 전체 시스템에 대한 재귀 추론을 수행할 필요는 없다. Service를 되돌릴 때마다 다음 단계의 국소적인 의존성 변화가 생기며, 시스템이 새로운 안정 상태에 도달할 때까지 이어진다.
Context가 Effect와 Coeffect를 통합해야 하는 이유
Effect와 의존성을 각각 독립된 세계에서 관리한다면 어려운 문제가 생긴다. 컴포넌트가 수정하는 환경은 무엇이고, 의존성이 관찰하는 환경은 또 무엇인가?
논문은 effect context와 coeffect context를 하나의 Context type으로 통합할 것을 제안한다. [1]
엔지니어링 관점에서 보면 다음과 같이 이해할 수 있다:
Context는 다음 역할을 동시에 수행한다:
- 현재 사용 가능한 Service 찾기
- 새로운 Service 등록
- 리스너와 기타 Effect 등록
- 해당 Effect가 어느 실행 단위에 속하는지 기록
- 하위 Scope / Isolation 생성
- 구조적 변화가 발생한 뒤 관련 의존성에 알림
그 결과 Plugin은 전역 Application, ServiceRegistry, LifecycleManager, EventBus 같은 여러 객체를 따로 받을 필요가 없다. 하나의 Context를 통해 실행 환경과 상호작용하고, Runtime은 그 상호작용에서 만들어진 수명 주기 관계를 통합적으로 추적할 수 있다.
단일 컴포넌트에서 컴포넌트 시스템으로
논문은 결국 한 가지 플러그인을 안전하게 언로드할 수 있는지만 다루는 것이 아니다. 여러 컴포넌트가 서로 엇갈려 변화할 때에도 이 성질들이 계속 성립하는지가 핵심이다.
예를 들어:
A provides X
B requires X -> provides Y
C requires X + Y
A를 언로드하면:
X removed
↓
B invalid
C invalid
↓
B effects reverted
C effects reverted
↓
Y removed
이후 A가 다시 나타난다면:
X available
↓
B activatable
↓
B provides Y
↓
C activatable
전체 시스템은 고정된 시작 순서에 의존하지 않는다. 그 순서는 현재 Context에서 의존 관계가 자연스럽게 만들어 낸 결과다.
“Composable”의 진짜 가치는 여기 있다. 각 컴포넌트는 자신이 제공할 것과 요구하는 것만 선언한다. 컴포넌트를 조합한 시스템의 수명 주기는 Runtime이 추론한다.
이 모델이 Agent Harness에 적합한 이유
Agent Harness의 역량은 본질적으로 매우 동적이다.
LLM Provider는 교체될 수 있고, Tool Provider는 Session별로 격리될 수 있으며, Sandbox는 실행 환경에 따라 바뀔 수 있다. MCP Server는 연결이 끊어질 수 있고, SubAgent는 특정 실행 구간에만 존재하며, UI Contribution도 플러그인의 시작과 정지에 따라 나타나거나 사라질 수 있다.
이 모든 관계를 수동 시작 순서와 수동 cleanup으로 관리한다면 Harness 안에 모듈을 가로지르는 수명 주기 코드가 빠르게 늘어날 것이다.
시공간적 조합성 모델은 다른 조직 방식을 제공한다:
이 세 객체가 Cordis의 핵심을 이룬다.
다음 글에서는 실제 소스를 살펴보며 다음의 더 구체적인 질문에 답한다:
- 의존 관계를 구성하는 Service가 언로드될 때, 그 Service에 의존하는 Fiber는 어떻게 변화를 감지하는가
- 왜 하위 Service가 계속해서 연쇄적인 무효화를 일으키는가
- 언로드 도중 의존성이 다시 나타나면 Runtime은 어떻게 수명 주기 경쟁을 피하는가
- DeepSeek Harness는 이 메커니즘을 바탕으로 Everything is a Plugin을 어떻게 구현하는가
참고 자료
[1] A Programming Paradigm for Spatiotemporal Composability: https://github.com/cordiverse/paper
[2] Cordis 저장소: https://github.com/cordiverse/cordis
[3] DeepSeek Harness에 벤더링된 Cordis: https://github.com/deepseek-ai/deepseek-harness/tree/master/vendor/cordis
[4] DeepSeek Harness Cordis Primer: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md