Cordis: 플러그인을 재시작 없이 넣고 빼기 위한 시공간 조합성 프로그래밍 패러다임에 대한 연구 (feat. DeepSeekAI)

Cordis 논문, 'Programming Paradigm for Spatiotemporal Composability' 소개

플러그인 아키텍처와 스스로를 수정하는 에이전트 하네스(Agent Harness)는 모두 실행 중에 기능을 안전하게 넣고 빼야 하는 소프트웨어입니다. 이번에 정리할 프리프린트(Preprint) "A Programming Paradigm for Spatiotemporal Composability" 는 그 동적 조합(Dynamic Composition) 에 형식 이론을 붙이고, 그 이론을 Cordis 라는 메타 프레임워크로 구현한 연구입니다. 논문은 Peking University 와 DeepSeek-AI 소속 연구진(Yifan Shi, Wei Zhang, Tianyi Cui)이 공개했고, 2026년 8월 13일자 초안으로 활발히 수정 중이라 내용이 크게 바뀔 수 있으니 최신 버전을 인용하라고 안내합니다.

문제의 출발점은 정적 조합과 동적 조합 사이의 이론적 비대칭입니다. 함수 호출, 모듈 임포트, 클래스 상속처럼 컴파일 타임에 해소되고 실행 내내 고정되는 정적 조합에는 풍부한 형식 이론이 쌓여 있습니다. 반면 컴포넌트가 실행 중에 로드되고 언로드되고 재설정되는 동적 조합은 실무에서 갈수록 중요해지는데도 그에 대응하는 형식적 기반이 빈약합니다. 그 공백을 메우기 위해 논문은 동적 조합의 요구사항을 서로 직교하는 두 차원으로 나눕니다: 컴포넌트를 제거할 때 그것이 공유 환경에 가한 변경을 완전하고 안전하게 되돌릴 수 있어야 한다는 시간적 조합성(Temporal Composability), 그리고 컴포넌트가 서로에 대한 의존성을 구조적이고 검증 가능한 방식으로 선언하고 발견하고 해소할 수 있어야 한다는 공간적 조합성(Spatial Composability) 입니다.

이 논문이 PyTorchKR 독자에게 남다른 이유는 구현체의 위치입니다. DeepSeek Harness (:pytorch::kr: DeepSeek Harness: 에이전트 루프까지 설정으로 교체하는 플러그인 구조의 코딩 에이전트) 의 공식 문서는 Cordis를 "DeepSeek Harness 바닥에 벤더 방식으로 포함된 플러그인 프레임워크" 라고 소개합니다. DSH의 에이전트 프리셋 구성 파일 이름이 agent.cordis.yml 인 것도 이 관계에서 나옵니다. 즉 이 논문은 커뮤니티에서 이미 다뤄 온 DSH 플러그인 생태계가 어떤 형식 이론 위에 서 있는지를 설명하는 문서이기도 합니다.

기존 접근법 1: 플러그인 호스트는 개별 확장을 내릴 수단이 없다

논문은 두 한계를 Visual Studio Code 확장 시스템으로 예시합니다. VSCode는 모든 확장을 확장 호스트(Extension Host)라는 하나의 공유 프로세스에서 실행하는데, 이 호스트에는 개별 확장의 코드를 실행 중에 내릴 수단이 없습니다. 일단 확장의 activate 가 실행되면 그 확장을 비활성화하거나 제거하려면 호스트 전체를 재시작해야 하고, 그 재시작은 로드된 모든 확장에 영향을 줍니다. 테마, 키바인딩, 스니펫처럼 코드를 담지 않은 순수 선언형 확장은 자유롭게 제거되지만, 연구팀이 2026년 6월 9일 기준 마켓플레이스 데이터로 확인한 바로는 설치 수 상위 100개 확장 중 87개가 실행 코드를 포함해 제거 시 재시작을 피할 수 없었습니다. deactivate 훅이 있긴 해도 호스트 프로세스가 종료될 때 불리는 정상 종료 콜백일 뿐이라 실행 중 제거를 가능하게 하지 않고, 게다가 효과를 만드는 자리(activate)와 폐기하는 자리를 갈라 놓아 관심사의 지역성(Locality of Concern)을 깨뜨립니다.

공간 쪽 한계도 같은 데이터에서 드러납니다. VSCode는 확장 간 의존성 선언을 위해 extensionDependencies 를 제공하지만, 상위 100개 중 내장 확장이 아닌 확장에 대해 이것을 쓰는 것은 7개뿐이었습니다. 확장 API가 명령, 뷰, 언어 기능처럼 고정된 표면적 확장 지점만 노출하기 때문에 확장은 서로에게 의존하기보다 호스트에 기여하는 형태가 되고, 확장끼리 기능을 주고받는 통로인 vscode.extensions.getExtension(...).exports 는 반환값이 타입이 없어(기본 any) 의존하는 쪽이 검사된 인터페이스에 기댈 수 없습니다. 논문에 따르면 이 두 한계는 VSCode만의 문제가 아니라 플러그인 시스템 전반에서 정도만 달리해 반복됩니다.

기존 접근법 2: 프로세스와 컨테이너라는 굵은 우회로

동적 조합성에 형식적 관심이 덜 쏠린 이유로 논문은 운영체제와 컨테이너 오케스트레이터가 이미 굵은 단위의 대체물을 제공하고 있다는 점을 듭니다. 운영체제는 프로세스 단위로 시간적 조합성을 주고, Kubernetes 같은 컨테이너 오케스트레이터는 서비스 단위로 공간적 조합성을 줍니다. 그래서 실무에서는 오작동하는 모듈을 프로세스 재시작으로 처리하고, 서비스 의존성을 오케스트레이터에 맡기는 방식으로 세밀한 조합성의 부재를 견딥니다.

문제는 그 대가입니다. 시간 쪽에서는 재시작마다 캐시, 연결, 진행 중이던 계산 같은 프로세스 지역 상태가 전부 버려지고, 이를 다시 쌓는 데 수초에서 수분이 걸립니다. 그 사이의 가용성을 유지하려면 중복 복제본을 둬야 하므로, 컴포넌트 하나를 복구하지 못하는 무능을 자원으로 보상하는 셈이 됩니다. 공간 쪽에서는 컨테이너 수준 오케스트레이션이 같은 주소 공간을 공유하는 컴포넌트 사이의 의존성을 표현할 수 없고, 지역 함수 호출이면 될 상호작용에 네트워크 부하를 끌어들입니다. 두 메커니즘은 프로세스와 컨테이너의 경계에서 작동하는데 현대 소프트웨어는 그보다 세밀한 층위에서 조합되고, 이 세분성 불일치가 컴포넌트와 같은 층위에서 효과와 의존성을 다루는 추상을 요구합니다.

기존 접근법 3: 정리 코드와 의존성 주입은 개발자의 규율에 기댄다

관련 연구 정리에서 논문은 시간적 조합성을 다뤄 온 기존 해법을 네 갈래로 나누는데, 실무에서 가장 흔한 것은 개발자가 손으로 쓰는 복구입니다. OSGi의 번들 수명주기나 Eclipse 확장 지점, IntelliJ, VSCode 같은 플러그인 수명주기 관례는 정리를 개발자가 쓴 언로드 콜백에 맡기고, Command 패턴은 연산에 undo 를 짝지우며, 사가(Saga) 모델은 긴 트랜잭션을 보상 동작이 딸린 단계들로 구성합니다. 이들 모두에서 역연산은 강제되지 않는 의무이고 연산과 분리되어 있어서, 하나를 잊으면 자원이 조용히 새어 나갑니다. 효과와 역연산을 구조적으로 짝지운 것에 가장 가까운 것은 React의 useEffect 훅으로, 재실행 전과 언마운트 시점에 런타임(Runtime)이 호출하는 정리 함수를 반환합니다. 논문이 지적하는 그 한계는 조합성입니다. 훅은 컴포넌트나 다른 훅의 최상단에서만 호출할 수 있어 조건문, 루프, 중첩 함수 안에 들어갈 수 없고, 효과 본문이 비동기 함수나 이터레이터를 받지 못합니다. 효과를 다른 효과들로 조립할 수 없으므로 합성된 역연산을 끌어낼 근거 자체가 없습니다.

의존성 쪽도 사정이 비슷합니다. Spring, Guice, Angular, Inversify 같은 의존성 주입(Dependency Injection) 프레임워크는 초기화 시점에 컴포넌트에 의존성을 주입하고, Vue의 provide/inject 와 React의 Context API는 컴포넌트 트리를 따라 그것을 내려보냅니다. 일부가 동적 유효범위를 지원하긴 하지만 어느 것도 반응형으로 다시 해소하지는 않습니다. 제공자가 실행 중에 교체되거나 제거되어도 기존 의존자는 비활성화되지도 재초기화되지도 않고, 논문이 제시하는 컴포넌트 상태 기계 수준의 수명주기 관리를 제공하는 것도 없습니다.

효과 시스템과 코이펙트 시스템: 두 이론적 기둥

논문이 끌어올리려는 두 이론, 효과 시스템(Effect System)과 코이펙트 시스템(Coeffect System)이 어디서 왔는지는 예비지식 절이 짚어 줍니다. 효과 시스템은 단순 타입 람다 계산법의 타이핑 판단을 다듬어 결과 타입에 효과 대수의 원소를 붙이는 것에서 시작합니다. 판단이 \Gamma \vdash t : T_{effect} 형태가 되어, 계산이 만들 수 있는 부작용이 타입에 드러납니다. 출발점은 Lucassen과 Gifford가 병렬 프로그램의 스케줄링 제약을 찾아내려고 타입, 효과, 영역을 구분한 종류(Kind) 기반 타입 시스템입니다. 이후 두 계보로 갈립니다. 모나드 효과 는 Moggi가 계산 효과를 모나드로 범주론적으로 모델링하고 Wadler가 Haskell에서 대중화한 것으로, 효과 있는 계산을 T(A) 타입의 값으로 감싸고 \eta : A \to T(A) 로 순수 값을 올리고 \mu : T(T(A)) \to T(A) 로 중첩된 계산을 순차화합니다. Maybe, State, IO 모나드가 고전적인 사례입니다. 대수적 효과 는 Plotkin과 Power가 대수적 연산이 모나드를 결정한다는 것을 보이면서 효과 인터페이스를 그 구현에서 떼어낸 것이고, Plotkin과 Pretnar의 효과 핸들러가 연산에 계속(Continuation) 의미론을 부여해 예외, 코루틴, 비결정성을 하나의 틀에서 다루게 했습니다. 핸들러가 받는 구분된 계속을 0번, 1번, 여러 번 호출할 수 있다는 점이 그 표현력의 원천이고, Koka, Eff, OCaml 5가 서로 다른 설계 절충으로 이 노선을 채택했습니다.

코이펙트 시스템은 타입 대신 컨텍스트를 다듬어 \Gamma_{coeffect} \vdash t : T 형태의 판단을 얻고, 계산이 환경에서 필요로 하는 것(접근할 자원, 보유할 권한, 의존할 서비스)을 표시합니다. 효과가 프로그램이 세계에 미치는 영향을 모델링한다면 코이펙트는 세계가 프로그램에 가하는 제약을 모델링합니다. 컨텍스트 의존 계산을 코모나드로 구조화하는 발상은 Uustalu와 Vene이 Moggi의 모나드 틀의 쌍대로 제안했고, Petricek 등이 이를 컨텍스트 의존성의 통합 정적 분석인 코이펙트로 발전시켰습니다. 더 세밀한 추적을 위한 등급 코이펙트 는 선순서 반환(Pre-ordered Semiring) \mathcal{S} = (S, \leq, +, \times, 0, 1) 을 코이펙트 대수로 삼아 각 변수 바인딩에 사용량을 표시합니다. 0은 미사용, 1은 선형 사용, n 은 유계 사용, \infty 는 무제한 사용입니다. 반환의 두 연산이 코이펙트를 순차(\times)와 병렬(+)로 합성해 자원 추적, 민감도 분석, 정보 흐름 제어를 한 대수 틀 안에 담습니다.

이 연구의 발상 전환: 효과와 코이펙트를 런타임 메커니즘으로 끌어올린다

동적 조합성의 두 차원은 각각 계산이 환경을 어떻게 바꾸는가 와 환경에 무엇을 요구하는가 에 관한 것입니다. 이것은 정확히 앞 절의 효과 시스템과 코이펙트 시스템이 형식화해 온 두 방향입니다. 다만 기존 정식화는 추론(reasoning)을 컴파일 타임 분석과 어휘적으로 고정된 유효범위 안에 묶어 두므로, 컴포넌트가 실행 중에 오고 가는 상황으로 확장되지 않습니다. 배포 후에 로드된 플러그인을 감싸 줄 어휘적 유효범위는 없고, 런타임 설정에서 생겨나는 의존성을 예측할 컴파일 타임 컨텍스트도 없습니다.

그래서 연구팀이 택한 길은 정적 타입 시스템에 표기를 더 붙이는 방향이 아닙니다. 효과와 코이펙트의 개념적 구조를 구상화(Reify) 해서 런타임이 그것을 직접 다룰 수 있게 만들고, 정적 시스템이 정적으로 주던 보장을 동적으로 세우는 쪽입니다. 효과는 역연산을 함께 지니는 가역 런타임 모델로, 코이펙트는 반응형 의존성 해소 메커니즘으로 각각 끌어올립니다.

Cordis 논문이 정의하는 두 축: 시간과 공간

시간적 조합성이 정적 세계에서는 쉬운 문제라는 점이 논문의 첫 관찰입니다. 어휘적 유효범위(Lexical Scoping)나 RAII(Resource Acquisition Is Initialization), 브래킷 패턴처럼 블록이 끝나면 자원이 정리되는 장치로 해결되기 때문입니다. 공간적 조합성도 정적 세계에서는 모듈 임포트 해소로 환원됩니다. 그러나 컴포넌트가 실행 중에 오가는 동적 세계에서는 두 차원이 모두 훨씬 어려워집니다. 시간적 조합성은 유효범위로 묶이지 않는 오래 살아남는 상태 변화를 추적해야 하고, 공간적 조합성은 실행 중에 나타나고 사라지고 정체가 바뀌는 의존성을 다뤄야 합니다.

에이전트 하네스 쪽 동기는 더 직접적입니다. 현대 AI 에이전트는 도구 모음과 실행 환경을 조합하고, 권한과 샌드박싱을 관리하고, 세션 상태와 지속성을 유지하고, 컨텍스트 관리와 메모리 시스템을 제공하고, 서브에이전트와 다중 에이전트 워크플로를 조율하는 런타임 하네스에 기댑니다. 앞으로의 하네스는 요청을 계속 처리하면서 자기 자신의 컴포넌트에 대한 수정을 생성하고 배포할 수 있고, 그 수정 하나하나가 동적 조합의 사례입니다. 이런 수정이 사람의 감독이 거의 없이 연속적으로 일어난다면, 시간적 조합성이 없을 때는 수정마다 전체 재시작으로 누적 상태를 잃고 잘못된 자기 수정이 복구에 필요한 프로세스 자체를 죽일 수 있습니다. 공간적 조합성이 없으면 각 모듈이 의존 대상의 등장과 소멸을 임시방편으로 감지해야 하고, 순진한 코드 교체 전략은 의존하는 쪽을 조용히 망가뜨립니다.

Cordis 논문의 다섯 가지 기여

논문이 내놓는 기여는 다섯 가지입니다:

  • 가역 효과(Revertible Effects): 모든 컨텍스트 변환이 명시적인 역연산을 함께 지니고 런타임이 이를 추적하며, 추적과 복구가 모두 합성을 보존하므로 컴포넌트 제거 시 컨텍스트가 복구됩니다. 지역적 시간 조합성을 확보하는 장치입니다.
  • 반응형 코이펙트(Reactive Coeffects): 컴포넌트가 필요한 조건을 명세로 선언하면, 컨텍스트의 변화가 그 명세에 대해 활성화, 비활성화, 중립으로 판정되어 통지됩니다. 지역적 공간 조합성을 확보합니다.
  • 통합 컨텍스트 타입(Context Type): 효과의 컨텍스트와 코이펙트의 컨텍스트를 하나의 타입으로 통합하고, 코이펙트 위의 관찰적 동등성이 효과에 독립성을 공급해 시공간 조합성을 위한 프로그래밍 패러다임을 구성합니다.
  • 동적 조합의 계산법(Calculus): 두 메커니즘을 컴포넌트 개념으로 묶고 수명주기에 조작적 의미론을 부여하며, 그 메타이론이 단일 컴포넌트의 성질을 서로 얽힌 컴포넌트 시스템 전체로 확장합니다.
  • 구현체 Cordis: 효과 추적과 코이펙트 해석으로 형식 모델을 실현하는 코어 라이브러리, 그리고 설정 조정(Configuration Reconciliation)과 핫 모듈 교체(HMR, Hot Module Replacement)를 갖춘 선언적 컴포넌트 로더로 구현했습니다.

가역 효과: 역연산을 값으로 들고 다니는 효과

효과 컨텍스트와 누산기

논문은 먼저 순수하지 않은 함수 f_{impure} : X \to Y 를 f : \Gamma \times X \to \Gamma \times Y 형태로 옮깁니다. 여기서 \Gamma 는 컨텍스트이고, 일어날 수 있는 모든 부작용은 \Gamma 위의 변환으로 표현됩니다. 입력 x 를 고정하면 유도되는 사상 \gamma \mapsto \operatorname{pr}_1(f(\gamma, x)) 가 반환값과 무관하게 f 의 부작용만 붙잡습니다. 그래서 \Gamma 위의 효과는 합성 \circ 아래에서 변환 모노이드 \Gamma \to \Gamma 에 살게 되고, 모노이드 공리 하나하나가 효과의 성질로 그대로 읽힙니다: 닫힘은 두 효과의 순차 합성이 다시 효과라는 뜻이고, 결합법칙은 합성된 효과가 괄호 묶는 방식과 무관하다는 뜻이며, 항등원 id_\Gamma 는 합성의 단위입니다.

되돌릴 수 있는 효과를 모델링하려면 각 변환 f 에 그것을 되돌리는 변환 g 를 짝지어야 합니다. 논문은 g 를 f 의 좌역원이라 부르고 이후 그냥 역연산으로 줄여 쓰는데, 여기서 되돌림은 한쪽 방향입니다. 역연산이 책임지는 것은 g \circ f 이고 f \circ g 는 결코 아닙니다. 이 쌍들은 자기만의 곱셈을 지닙니다:

(f_1, g_1) \circ (f_2, g_2) := (f_1 \circ f_2,\ g_2 \circ g_1)

왼쪽 피연산자가 나중에 작용하고 역연산은 반대 순서로 쌓입니다. 이것이 (\Gamma \to \Gamma) \times (\Gamma \to \Gamma) 를 단위원 (id_\Gamma, id_\Gamma) 을 갖는 모노이드로 만들며, 논문은 이를 꼬인 합성 모노이드(Twisted Composition Monoid) 라 부릅니다.

효과를 컨텍스트 안에서 추적하기 위해 도입되는 것이 효과 컨텍스트입니다:

\partial \Gamma := \Gamma \times (\Gamma \to \Gamma)

이것은 쌍 (\gamma, \varphi) 로 읽히는데, \gamma 는 현재 컨텍스트 상태이고 \varphi 는 누산기(Accumulator) 로서 지금까지 수행된 효과들의 역연산을 합성한 것, 즉 컨텍스트를 초기 상태로 되돌리는 함수입니다. 초기 효과 컨텍스트는 (\gamma_0, id_\Gamma) 로 표현됩니다. 순방향 함수 f 와 역연산 후보 g 를 받아 \partial \Gamma 위의 변환으로 바꾸는 것이 \operatorname{track} 이고, 그 정의는 (\gamma, \varphi) \mapsto (f(\gamma), \varphi \circ g) 입니다. 상태를 f 로 옮기고 역연산 g 를 누산기에 이어 붙이는 것이 곧 효과를 추적하는 일입니다.

논문은 \operatorname{track} 이 꼬인 합성 모노이드에서 \partial \Gamma \to \partial \Gamma 로 가는 모노이드 준동형이라는 것을 증명합니다. 여기서 나오는 결론이 건전성 불변량(Soundness Invariant) 입니다. 복구 연산 \operatorname{recover} : (\gamma, \varphi) \mapsto (\varphi(\gamma), id_\Gamma) 는 추적된 효과를 몇 개 적용한 뒤에 취해도 같은 값을 내놓고, (\gamma_0, id_\Gamma) 에서 출발한 모든 상태를 (\gamma_0, id_\Gamma) 로 되돌립니다. 즉 \varphi(\gamma) = \gamma_0 라는 등식이 상태마다 유지되는 불변량이 됩니다.

상태마다 역연산을 고르는 효과 함수

앞 절의 모델은 역연산을 미리 정해진 것으로 취급합니다. \operatorname{track}(f, g) 는 컨텍스트 상태를 보기 전에 g 를 고정하므로 하나의 g 가 그 효과가 적용될 모든 상태를 감당해야 합니다. 현실에서는 각 효과의 역연산이 미리 알려지지 않고 효과가 적용되는 지점에서 호출자가 공급해야 하며, 게다가 \operatorname{recover} 는 전부 아니면 전무라서 다른 효과는 남기고 하나만 골라 되돌릴 수 없습니다. 두 문제를 함께 풀기 위해 논문은 입력과 출력 양쪽을 강화합니다. 입력 쪽에서는 \Gamma 를 변환하는 데 그치지 않고 역연산 함수를 함께 반환해 \Gamma \to \partial \Gamma 가 되고, 출력 쪽에서도 같은 강화를 적용해 \partial \Gamma \to \partial^2 \Gamma 가 됩니다.

그 결과로 얻는 것이 효과 함수 타입 \Gamma \to \Gamma \times (\Gamma \to \Gamma) 와 그 증인이 붙은 정제입니다. 효과 함수 e 를 상태 \gamma 에 적용하면 새 컨텍스트 \delta 와 현재 효과의 역연산 g 가 쌍으로 나오고, 증인 조건은 g(\delta) = \gamma 입니다. 이 제약은 역연산을 효과가 적용된 그 지점에서 효과를 되돌리는 일에만 묶어 두고 다른 모든 곳에서는 g 를 자유롭게 남겨 둡니다. g \circ f = id_\Gamma 를 만족하는 단일 g 는 모든 상태에서 한꺼번에 이 제약을 만족합니다.

효과 함수는 더 이상 컨텍스트 위의 자기사상이 아니라서 곧바로 합성되지 않습니다. 그래서 논문은 효과 합성 \diamond 를 따로 정의하고, 이것이 꼬인 합성 모노이드의 구조를 효과 함수 타입으로 옮긴다는 것과 증인이 붙은 부분이 부분모노이드를 이룬다는 것을 증명합니다. 여기까지 오면 실용적으로 중요한 결론 하나가 나옵니다: 개발자는 원자적 연산 하나하나의 역연산만 공급하면 되고, 합성된 것의 역연산은 합성으로 따라 나온다는 것입니다. 컴포넌트의 해체 코드는 로딩 코드와 나란히 작성되는 것이 아니라 로딩에서 유도됩니다.

LIFO 복구와 효과의 독립성

e_1, \dots, e_n 을 (\gamma_0, id_\Gamma) 에서 순서대로 적용한 뒤 역순으로 되돌리면, 각 되돌림은 자기 적용이 만들어 낸 상태를 정확히 복구하고 모든 중간 상태가 건전성 불변량을 만족합니다. 이 정리는 추가 가정을 전혀 요구하지 않습니다. 역순, 즉 LIFO(Last In First Out) 순서에서는 각 역연산이 자기 적용이 만든 그 상태를 만나기 때문입니다.

문제는 역순이 아닌 경우입니다. 논문은 두 상황을 꼽습니다. 나중 효과들이 아직 자리에 남아 있는 채로 역연산이 실행되는 경우가 하나인데, 이것이 곧 실행 중인 시스템에서 컴포넌트 하나를 빼내는 일입니다. 다른 하나는 한 시퀀스가 여러 컴포넌트의 효과를 교차시키고 각자 자기 것의 역연산만 들고 있어서, 한 컴포넌트의 역연산들이 다른 컴포넌트의 적용들로 나뉘는 경우입니다. 두 경우 모두 역연산이 외부 효과가 옮겨 놓은 상태를 만나므로, 그것이 여전히 자기가 되돌리도록 만들어진 것을 되돌리는지가 가환성 문제로 바뀝니다.

그래서 논문은 두 효과 함수의 독립성(Independence) 을 정의합니다. 한쪽의 모든 변환이 다른 쪽의 모든 변환과 가환하고(순방향 사상과 산출된 역연산 모두 포함), 어느 한쪽의 변환도 다른 쪽이 산출하는 역연산을 교란하지 않아야 합니다. 독립성이 성립하면, 나중 효과들이 상태를 옮겨 놓은 자리에서 역연산을 실행해도 그것이 철회하는 것은 자기 몫이고 그 외에는 아무것도 아닙니다. 따름정리로 n 개의 역연산을 임의의 순열 순서로 적용해도 초기 상태 \gamma_0 에 도달합니다. LIFO는 그 순열 중 하나일 뿐이고, 독립성이 추가로 확보해 주는 것은 나머지 모든 순서와 그로써 여러 컴포넌트의 효과를 교차시키는 시퀀스입니다.

독립성이 깨지는 곳에서는 순서를 다른 데로 옮겨야 합니다. 한 컴포넌트 안에서는 누산기가 효과가 무엇이든 LIFO로 되돌리며 순서를 감당하고, 컴포넌트 사이에서는 선언된 코이펙트가 한 활성화를 다른 활성화보다 앞세우며 순서를 감당합니다. 즉 계산의 가환하는 부분은 효과가 담당하고 순서에 민감한 부분은 코이펙트가 담당하는 분업입니다.

반응형 코이펙트: 의존성을 선언하고 통지받기

코이펙트 컨텍스트와 get/set

전통적인 제어 역전(IoC, Inversion of Control) 컨테이너는 의존성을 단순한 키-값 매핑으로 모델링합니다. 논문은 이것을 코이펙트 컨텍스트, 즉 의존 부분 함수 타입으로 형식화합니다. 타입 족 \mathcal{V} : K \to \text{Type} 이 주어지면 코이펙트 컨텍스트 \Sigma 는 각 키 k 에 타입 \mathcal{V}_k 의 값을 배정하는 유한 부분 함수이고, 타입 족을 쓴 덕분에 각 의존성 키가 특정 값 타입과 묶여 의존성 접근에 정적 타입 안전성이 생깁니다.

핵심 연산은 두 가지입니다. get(k) 는 k \in \operatorname{dom}(\sigma) 를 전제로 값을 읽고, set(k, v) 는 k \notin \operatorname{dom}(\sigma) 를 전제로 값을 꽂으면서 그것을 걷어내는 역연산 \lambda \sigma'. \sigma' \setminus k 를 함께 반환합니다. 여기서 논문의 시너지가 드러납니다: set(k, v) 의 타입이 정확히 코이펙트 컨텍스트 위의 증인 붙은 효과 함수라는 것입니다. 따라서 앞 절의 효과 기계를 그대로 적용할 수 있고, 의존성 등록의 추적과 복구가 자동으로 따라옵니다. 코이펙트 연산은 효과이고, 효과는 가역입니다.

명세와 통지

부재한 의존성에 접근하는 것은 런타임 실패입니다. 그래서 컴포넌트는 낙관적으로 접근한 뒤 없으면 실패하는 대신, 선언한 의존성이 모두 갖춰진 뒤에야 활성화되어야 합니다. 논문은 코이펙트 명세 d \subseteq K 에 대한 만족 술어를 \sigma \models d := \forall k \in d.\ k \in \operatorname{dom}(\sigma) 로 정의합니다. \operatorname{dom}(\sigma) 가 유한하므로 이 술어는 결정 가능하고, \sigma 에 대한 모든 변경이 효과 함수를 통과하므로 만족 여부의 변화는 매 효과 경계에서 검출됩니다. 이것이 반응성의 대수적 근거입니다: 효과 시스템이 모든 코이펙트 변화가 관찰됨을 보장합니다.

여기서 통지 함수가 나옵니다. \sigma 를 \sigma' 로 옮기는 임의의 효과는 명세 d 에 대해 세 가지로 분류됩니다. \sigma \not\models d 였다가 \sigma' \models d 가 되면 활성화(activating), 반대 방향이면 비활성화(deactivating), 그 외는 중립(neutral) 입니다. 반응형 불변량은 활성화 전이가 컴포넌트의 효과 실행을 촉발하고(효과 추적을 온전히 켠 채로), 비활성화 전이가 누산기 적용에 의한 복구를 촉발한다는 것입니다.

논문은 이 지역적 기준이 코이펙트 순서의 한 방향만 덮는다는 점을 명확히 인정합니다. 컴포넌트 A 가 키 k 를 제공하고 B 가 k 를 선언하면, B 는 A 가 활성화되어 k 를 제공한 뒤에야 활성화될 수 있습니다. 그러나 역방향은 성립하지 않습니다. A 를 언로드하면 \operatorname{dom}(\sigma) 에서 k 가 사라져 B 의 만족이 깨지지만, 통지만으로는 B 자신의 해체가 필요한 동안 k 를 읽을 수 있게 유지해 주지도, A 의 복구를 B 가 끝날 때까지 붙잡아 두지도 못합니다. 철회를 그것이 유발한 비활성화들보다 뒤로 미루는 일은 행동하는 쪽이 아니라 다른 컴포넌트들에 대한 조건이므로, 전역 형태의 보장에 속합니다.

격리와 가로채기

평평한 의존성 테이블만으로는 부족한 경우가 있습니다. 같은 논리적 의존성을 컴포넌트마다 다른 값에 묶어야 하는 상황입니다. 논문은 두 메커니즘으로 코이펙트 컨텍스트를 확장합니다.

코이펙트 격리(Coeffect Isolation) 는 격리 영역(Isolation Realm)을 도입해 같은 의존성이 서로 다른 컨텍스트에서 다른 값에 묶이게 합니다. 코이펙트 컨텍스트가 영역 테이블 \rho : K \rightharpoonup R 과 의존성 테이블 \sigma : R \rightharpoonup \mathcal{V} 의 쌍으로 나뉘어, 키 k 에 접근할 때 먼저 \rho(k) 로 영역 식별자 r 을 해소하고 그다음 \sigma(r) 로 실제 값에 닿습니다. 논리 층과 저장 층을 분리한 이 두 단 매핑은 사실상 런타임 애드혹 다형성 시스템이고, 다중 테넌트 시스템, 테스트 환경, 컴포넌트 샌드박스에 폭넓게 쓰입니다.

코이펙트 가로채기(Coeffect Interception) 는 의존성 값을 바꾸지 않고 의존성 접근에 횡단 메타데이터를 붙입니다. 각 키가 자기 메타데이터에 모노이드 구조를 부여하고, 명세 d 를 가진 컴포넌트가 키 k 에 접근하면 시스템이 컴포넌트가 선언한 메타데이터와 컨텍스트가 지닌 메타데이터를 병합한 뒤 제공자 함수에 적용합니다. 병합은 오른쪽 우선이라 컨텍스트 쪽이 우선권을 가지므로, 감싸는 컨텍스트가 컴포넌트를 수정하지 않고도 그 컴포넌트가 코이펙트를 어떻게 쓸지 제약할 수 있습니다.

두 연산은 공유 테이블에 쓰는 대신 상속받은 테이블 하나를 조정한 새 컨텍스트를 파생시킨다는 점에서 set 과 성격이 다릅니다. 논문은 이를 위해 효과 함수의 두 실현 방식을 구분합니다. 제자리 실현(In-place Realization) 은 컨텍스트를 변경하고 자명하지 않은 역연산을 반환하며, 파생 실현(Derived Realization) 은 입력을 그대로 두고 그로부터 파생된 새 컨텍스트를 반환하면서 항등 함수를 역연산으로 삼습니다. 후자에서 복구는 파생된 컨텍스트를 버리는 것으로 끝나므로 추적할 역연산이 아예 없습니다.

통합 컨텍스트와 컨텍스트 패러다임

재귀적 컨텍스트 타입

효과 컨텍스트 구조를 재귀적으로 만들고 코이펙트 컨텍스트와 결합하면 논문의 최종 컨텍스트 타입이 나옵니다:

\Gamma_\infty := \mu \Gamma.\ \Gamma \times (\Gamma \to \Gamma) \times \Sigma

세 투영은 각각 현재 컨텍스트 상태(재귀), 이 층의 효과를 복구하는 누산기, 의존성 정보를 지닌 코이펙트 컨텍스트입니다. 이 정의 아래에서 효과 변환은 \Gamma_\infty 를 자기 자신으로 보내며 \partial 의 탑 전체를 하나의 자기 유사 타입으로 통합합니다. 코이펙트 컨텍스트 \Sigma 를 받치는 타입 족 \mathcal{V} 에 제약이 없으므로, 시스템이 컴포넌트 사이에 공유해야 하는 어떤 상태든 적절한 값 타입의 의존성으로 인코딩할 수 있습니다. 즉 \Sigma 는 컴포넌트 간 의존성만이 아니라 공유 가변 상태 전체를 포섭하고, 컴포넌트와 환경 사이의 모든 상호작용이 이 하나의 실체를 통과합니다.

이 재귀 구조가 계층적 제어를 지탱합니다. 부모 컨텍스트가 여러 자식 층의 효과를 집계해 나무 모양의 제어 구조를 이루므로, 논문의 표현대로 효과 변환이 문자 그대로의 "플러그인" 은유를 실현합니다: 컴포넌트를 로드하는 것은 그 효과를 실행하는 것(꽂기), 언로드하는 것은 그 효과를 복구하는 것(뽑기)이며, 계층의 서로 다른 층에 있는 컴포넌트들이 독립적으로 로드되고 언로드됩니다.

관찰적 동등성이 독립성을 공급한다

복구 보장이 상태의 등식을 주장한다는 것은 이상화입니다. 물리적 상태가 있던 그대로 복구되지는 않기 때문입니다. free 는 블록을 할당기에 반납하지만 malloc 이전의 힙 레이아웃을 되돌리지 않고, 생성적 이름은 그것을 버리는 역연산으로 복원되지 않습니다. 다음 생성이 새 이름을 뽑기 때문입니다. 그래서 논문의 등식들은 모두 동등성 \simeq 아래에서 읽히고, 그 \simeq 는 관찰적 동등성(Observational Equivalence) 입니다. 두 상태는 어떤 관찰자도 구별할 수 없을 때 관련됩니다.

컨텍스트의 관찰자에게 주어지는 것은 그것이 지닌 코이펙트들이고, 각 코이펙트는 자기 동등성을 함께 들고 옵니다. 그래서 컨텍스트 위의 관계는 코이펙트들의 관계로부터 조립됩니다. 어떤 키도 묶지 않은 상태 부분은 그 과정에서 잊히고, 잊는 것이야말로 복구 정리를 \simeq 아래에서 읽을 수 있게 하는 장치입니다. 위 예시의 힙 레이아웃과 생성적 이름은 어떤 키가 그것을 묶지 않는 한 관계 밖에 놓입니다.

이 절의 결론은 실무에 바로 쓸 수 있는 판정 기준입니다. 어떤 키가 가환(commutative) 인지가 곧 독립성이 성립하는지를 결정하는데, 논문은 그 판정을 그 키가 발행하는 인터페이스의 성질에서 읽어 냅니다:

  • 값이 독립적으로 추가되고 제거되는 항목들의 테이블인 키는 가환입니다. 라우트 등록이나 이벤트 리스너 등록이 대표적인 경우로, 두 등록을 어떤 순서로 하든 모든 테스트에 같게 답하는 테이블이 남고, 다른 하나가 남아 있는 채로 어느 한쪽만 철회할 수 있습니다.
  • 값이 순서 있는 체인인 키는 가환이 아닙니다. 다른 미들웨어보다 앞에 삽입된 미들웨어는 다른 요청을 보게 되고, 어느 순서도 다른 쪽을 교란하지 않고서는 철회될 수 없습니다.
  • 할당기는 인터페이스가 무엇을 발행하는지에 따라 갈립니다. 넘겨 주는 핸들이 그 키의 어떤 연산으로도 비교되지 않는다면 \simeq 가 핸들의 재명명까지 감안해 두 힙을 관련지을 수 있고(CompCert가 프로그램과 그 번역의 메모리 상태를 관련짓는 방식이 이것입니다) 할당은 가환입니다. 반면 주소가 등식으로 비교되는 결과값이라면 어떤 \simeq 도 두 할당 순서를 일치시키지 못하므로 그 키는 가환이 아닙니다.

논문은 마지막 두 한계를 명시합니다. 공유되는 모든 위치를 키에 묶는 것은 패러다임의 규율이지 구성의 성질이 아니므로, 시스템이 코이펙트로 구상화할 수 없는 위치는 정리 밖에 놓입니다. 그리고 키의 가환성은 그 키가 발행하는 인터페이스의 성질이므로, 그것을 만족시킬 의무는 키를 소비하는 컴포넌트가 아니라 제공하는 컴포넌트에 있습니다.

함수형과 명령형, 두 극 사이에서

논문은 컨텍스트 패러다임을 부작용 처리 방식의 두 극과 대비시켜 자리매김합니다. 한 극은 함수형의 명시적 상태 전달입니다. 참조 투명성을 지키려고 순수 함수형 언어는 부작용을 상태 위의 명시적 변환으로 모델링하고, State 모나드가 환경을 모든 계산에 꿰어 넣습니다. 강한 조합적 보장을 얻지만 대가가 큽니다. 호출 연쇄의 모든 함수가 상태를 그냥 통과시킬 때조차 상태 매개변수를 받고 반환해야 하고, 효과 차원이 늘어날수록 모나드 적층이나 효과 핸들러 보일러플레이트가 불어납니다.

다른 극은 명령형과 객체지향의 암묵적 변경입니다. 효과 쪽 대표 사례로 논문은 React의 useEffect 를 듭니다. 컴포넌트 내부 파이버에 지속적 부작용을 등록하지만 효과 대상도 등록 메커니즘도 명시적 매개변수로 나타나지 않고, 식별이 숨겨진 런타임 상태 안의 호출 순서 위치에 기댑니다. 코이펙트 쪽 대표 사례는 Java의 서비스 로케이터 패턴으로, Spring의 ApplicationContext.getBean(...) 처럼 프로세스 전역 레지스트리에서 의존성을 꺼내며 호출 지점마다 널 검사와 타입 캐스팅을 요구합니다. f() 가 시스템을 어떻게 바꾸는지 또는 무엇에 의존하는지 알려면 그 구현을 이행적으로 읽어야 하고, 호출을 옮기거나 지우는 것이 멀리 있는 불변량을 조용히 깨뜨릴 수 있어 리팩터링이 취약해집니다.

컨텍스트 패러다임은 함수형의 추적 가능성과 명령형의 사용성을 결합합니다. 효과와 코이펙트가 모두 명시적 컨텍스트 매개변수를 통해 매개되므로 모든 연산이 그것이 호출된 특정 컨텍스트에, 따라서 그 컨텍스트가 속한 컴포넌트에 귀속됩니다. 여기서 한 걸음 더 나아가는 부분을 연구팀은 특히 강조합니다. 개발자는 효과와 의존성을 하나씩 다루면 되고, 시스템의 동작으로 조합하는 일은 자동으로 이루어집니다. 가역 효과 쪽에서는 원자적 연산의 역연산만 공급하면 합성된 것의 역연산이 따라 나오고, 반응형 코이펙트 쪽에서는 필요한 의존성만 선언하면 런타임이 제공자가 추가되고 제거되고 교체되는 동안 배선을 일관되게 유지합니다. 양쪽 모두 개발자의 규율에 얹혀 있던 정확성이 패러다임의 구조적 성질로 바뀝니다.

동적 조합의 계산법: 컴포넌트, 파이버, 레지스트리

여기까지가 지역적 형태의 조합성입니다. 이를 시스템 전체로 옮기려면 시스템을 컴포넌트들로 분해해서 공유 환경과의 모든 상호작용을 그중 하나에 귀속시켜야 합니다. 논문의 컴포넌트는 세 쌍입니다: 환경에서 요구하는 의존성을 선언하는 코이펙트 명세 d, 컴포넌트가 제공할 수 있는 코이펙트 키를 선언하는 제공 p, 그리고 활성 상태일 때 기여하는 효과와 그것을 철회하는 역연산을 정의하는 증인 붙은 효과 함수 e 입니다. d 와 p 는 하나의 인터페이스의 두 방향, 즉 환경에서 읽는 것과 환경에 쓰는 것입니다.

한 컴포넌트는 여러 번 인스턴스화될 수 있고, 각 인스턴스화가 자기 수명주기 상태를 지닙니다. 논문은 그 인스턴스화를 파이버(Fiber) 라 부릅니다. 파이버는 자기를 만든 컴포넌트, 자기가 인스턴스화된 상위 파이버, 자기가 제공하는 코이펙트, 그리고 수명주기의 어디에 서 있는지를 기록합니다. 상태는 파이버들을 이름 아래 담은 레지스트리(Registry) 를 지니고, 코이펙트 컨텍스트는 저장되는 것이 아니라 그 배치에서 읽혀 나옵니다. 즉 활성 파이버들이 함께 제공하는 것의 합집합입니다. 각 파이버가 자기 제공 집합 p 안의 키만 쓰고 서로 다른 파이버의 제공이 서로소이므로, 각 키는 정확히 하나의 활성 파이버 테이블에 놓이고 그 파이버를 그 키의 제공자라 부릅니다. 이 서로소 조건은 인스턴스화 횟수도 제한합니다. 제공이 비어 있지 않은 컴포넌트는 한 번에 파이버 하나만 가질 수 있으므로, 여러 번 인스턴스화되는 것은 아무것도 제공하지 않는 컴포넌트, 즉 소비만 하거나 다른 컴포넌트를 등록하는 흔한 경우입니다.

목표 뷰와 커밋된 뷰

규칙들은 각 파이버를 목표(Target) 와 비교합니다. 목표 뷰는 파이버가 은퇴했거나 의존성이 만족되지 않으면 \bot 이고, 그렇지 않으면 선언한 각 키를 그 제공자 이름으로 보내는 전사상입니다. 파이버가 지닌 커밋된 뷰(Committed View) 는 그 파이버가 활성화할 때 맞춰 놓은 해석이고, 목표 뷰는 지금 맞춰야 하는 해석입니다. 수명주기는 둘을 비교하는 것으로 구동됩니다. 값이 아니라 제공자를 기록하는 것이 비교를 쓸 만하게 만드는 요점입니다. 다른 파이버가 같은 값을 제공하는 경우가 있으므로, 값으로 비교하면 제공자 교체를 놓치게 됩니다.

기본 계산법은 각 전이를 원자적이고 즉각적이며 실패하지 않는 것으로 취급하고 다섯 규칙으로 움직입니다. 오케스트레이터가 수행할 수 있는 조율 규칙은 파이버를 존재하게 하는 삽입(O-Insert), 멈추라고 요청하는 은퇴(O-Retire), 실제로 지우는 제거(O-Remove) 셋입니다. 은퇴가 제거와 분리된 이유가 설계를 잘 보여 줍니다. 은퇴한 파이버가 아직 활성이면 먼저 비활성화되어야 하고, 그보다 먼저 지우면 누산기를 버려 자원이 새기 때문입니다. 시스템이 알아서 밟는 수명주기 규칙은 활성화(L-Reload)와 비활성화(L-Unload) 둘이며, 둘 다 같은 비교로 구동됩니다.

한 컴포넌트가 자기 효과를 설치하는 동안 다른 컴포넌트를 인스턴스화할 수 있다는 점도 계산법 안에 들어옵니다. 플러그인 호스트가 자기 플러그인을 로드할 때 하는 일이 그것입니다. 논문은 이를 등록 원시 연산으로 다루는데, 그 역연산이 제거가 아니라 은퇴라는 선택이 중요합니다. 역연산은 도달한 어느 자리에서든 적용되어야 하고, 제거는 전제 조건을 지니므로 그것으로 만든 역연산은 실패할 수 있습니다.

효과 함수가 지켜야 하는 규율도 계산법이 규정합니다. 논문은 이를 한정(Confinement) 이라 부르고, 한 적용이 무엇을 쓸 수 있는지와 무엇을 읽을 수 있는지를 함께 제한합니다. 쓰기 쪽에서 적용은 자기 파이버의 코이펙트 테이블만 바꾸고 다른 파이버의 항목이나 제어 필드는 건드리지 못합니다. 등록 원시 연산이 추가하는 항목과 그 역연산이 남기는 은퇴 표시만 예외입니다. 읽기 쪽에서는 자기 테이블, 선언한 키에 해당하는 제공자 테이블 조각, 그리고 어떤 파이버도 이름 붙이지 않은 상태 부분만 볼 수 있습니다. 읽기 제한이 이런 모양인 이유는 컴포넌트가 자기가 선언한 값을 읽어야 하기 때문입니다. 그 값들이 제공자의 테이블에 놓여 있으므로 자기 테이블만 읽는 효과 함수는 자기 코이펙트를 쓸 수 없게 됩니다. 반대로 읽을 수 없는 것이 선언 범위 밖의 테이블과 모든 제어 필드이고, 이것이 컴포넌트가 자기가 선언하지 않은 파이버의 수명주기 상태를 보고 분기하는 것을 막습니다.

진행 중인 전이: 철회, 반복, 비동기, 실패

실제 런타임의 전이는 원자적이지도 즉각적이지도 않고 실패하지 않는 것도 아닙니다. 논문은 네 가지 설정에서 기본 계산법을 확장하며, 네 확장이 하나의 구조적 결과를 공유합니다. 전이가 한 단계가 아니라면 진행 중에 머물 상태가 필요하고, 전이가 달릴 수 있는 방향마다 하나씩 필요합니다. 그래서 수명주기 상태가 Inactive와 Active 사이에 Reloading과 Unloading이라는 두 전이 상태를 갖게 됩니다.

철회(Withdrawal) 는 코이펙트 순서의 어려운 절반을 다룹니다. 제공자가 물러나기 때문에 해체되는 컴포넌트는 자기 해체 코드를 실행하는 중이고, 그 코드가 바로 지금 철회되는 그 코이펙트를 필요로 할 수 있습니다. 연결 풀을 닫는다는 것은 보통 연결들을 그것을 제공한 쪽에 돌려주는 일입니다. 기본 계산법은 제공을 걷어내는 일과 역연산을 실행하는 일을 한 단계에서 함께 하므로 소비자의 해체가 들어갈 구간을 남기지 않습니다. 이 층은 그 단계를 둘로 쪼갭니다. L-Leave는 비활성화 결정을 기록하기만 해서 파이버가 코이펙트 제공을 멈추게 하고, L-Unload가 누산기를 적용합니다.

L-Unload에 붙은 전제 \neg relied_n(\gamma) 를 논문은 가드(Guard) 라 부릅니다. 다른 설치된 파이버가 어떤 키를 n 으로 해소하고 있는 동안 n 의 철회를 붙잡아 둡니다. 이런 가드는 보통 데드락을 부르는데, 그것을 막는 것이 Unloading 상태와 코이펙트 컨텍스트가 활성 파이버만의 합집합이라는 사실의 조합입니다. L-Leave가 n 을 표시한 순간 그 테이블이 코이펙트 컨텍스트를 떠나므로 어떤 목표 뷰도 더는 n 을 지목할 수 없고, n 에 커밋했던 모든 소비자는 자기도 나가는 길에 있습니다.

반복(Iteration) 은 활성화가 여러 효과를 순차로 실행하고 비활성화가 그것들을 복구해야 하는 상황을 다룹니다. 논문은 이를 효과 이터레이터로 모델링하는데, 각 반복이 변경된 컨텍스트, 역연산, 그리고 계속 여부를 산출합니다. 각 반복에서 역연산이 적용 순서로 누산기에 이어 붙으므로 누산기는 적용될 때 자연스럽게 LIFO 순서로 효과를 복구합니다. 논문의 표현으로 효과 이터레이터는 구상화된 구분된 계속(Delimited Continuation)이며, 주류 언어가 yield 연산자로 노출하는 그 구조입니다. 그래서 이 모델은 이미 있는 제너레이터에 곧바로 대응합니다.

이 층에서 활성화가 네 규칙으로 나뉩니다. L-Begin이 전이를 시작하고, L-Iter가 반복을 하나 진행하며, L-Finish가 활성 상태로 마무리합니다. 두 연속 반복 사이에서 목표 뷰가 바뀌었으면 L-Divert가 전이를 우회시켜 지금까지 누적한 역연산을 적용합니다. L-Divert가 그 자리에서 누산기를 적용하지 않고 Unloading을 거쳐 가는 것이 설계의 일관성입니다. 이때 만나는 가드는 공허합니다. 한 번도 활성이었던 적 없는 파이버는 아무것도 제공하지 않고 어떤 커밋된 뷰에도 나타나지 않기 때문입니다.

비동기(Asynchrony) 층은 각 반복이 즉시 끝난다는 이상화를 걷어냅니다. 반복이 \text{Future}(A) 타입 값을 산출하고, 제출과 해소 사이에 외부 상태가 바뀔 수 있습니다. 이 층이 더하는 것은 관성(Inertia) 입니다. 일단 시작된 반복은 착지하고 그 착지를 거절할 수 없습니다. 그래서 비행 중에 목표 뷰가 돌아서면 반복을 중단하는 방식으로 답할 수 없고, 반복을 착지시키는 L-Divert의 다른 대안만 남습니다. 반복이 착지한 뒤에 파이버가 비활성화됩니다. 이 층은 규칙도 타입도 더하지 않고, 호스트가 L-Divert의 어느 대안을 취할 수 있는지에 대한 제약으로만 나타납니다.

실패(Failure) 층은 효과가 성공한다는 가정을 걷어냅니다. 컴포넌트가 설치하는 효과는 그것을 추적하는 컨텍스트 밖에 닿고, 닿는 대상은 거절할 수 있습니다. 이미 점유된 포트, 존재하지 않는 파일, 응답하지 않는 상대가 그렇습니다. L-Raise는 복구를 먼저 하고 기록을 나중에 합니다. 파이버가 오류를 결과로 지닌 채 Unloading으로 들어가고, 실패한 반복까지 쌓인 누산기가 거기서 적용되며, 아무것도 설치하지 않은 상태로 Inactive에 도착합니다. L-Begin의 전제가 오류 없는 Inactive이므로 오류 결과에서 수명주기가 다시 시작되지는 않습니다. 이것이 결과값의 실질입니다. 자기가 실행된 상태에 대해 건전하지 않음을 이미 드러낸 효과 함수를, 변하지 않은 환경에 대고 재시도하는 대신 붙잡아 둡니다. 실패가 부모로 전파되지 않고 파이버에 기록되므로 전이가 실패한 컴포넌트는 형제들을 계속 돌게 남겨 둡니다.

메타이론: 전역 조합성이 보장하는 것

메타이론 절은 열 규칙에서 두 조합성 차원을 전역 형태로 읽어 냅니다. 한 파이버의 보장이 그 사이에 다른 파이버들이 무엇을 하든 성립한다는 뜻이고, 여기에 시스템 전체에만 물을 수 있는 두 가지가 더해집니다. 목표가 요구하는 구성에 언제나 도달한다는 것, 그리고 그 구성이 정적 조립이 만들어 냈을 바로 그 구성이라는 것입니다. 절이 세우는 정리는 일곱 개입니다:

정리 보장하는 것
보존(정리 59) 레지스트리가 지녀야 하는 형태와 이후 정리들이 가정하는 조건이 열 규칙 어느 것을 적용해도 유지됩니다.
복구 정확성(정리 61) 파이버의 누산기를 적용하면, 제어 필드를 빼고, 그 사이 다른 파이버들이 밟은 같은 단계들이 에피소드 시작 상태에서 만들어 냈을 상태가 나옵니다.
종료 복구(따름정리 62) 파이버가 어떤 결과에 도달했든, 에피소드가 닫힌 뒤의 상태에는 그 파이버의 기여가 남지 않습니다.
순서(정리 63) 파이버는 의존성이 제공된 자리에서만 전이를 시작하고, 제공자는 그 키를 해소한 모든 의존자가 비활성화된 뒤에 철회하며, 소비자는 자기 해체 내내 그 값을 읽을 수 있습니다.
해석 일관성(정리 64) 한 전이의 모든 반복은 하나의 해석에 대해 실행되고, 그 조건이 깨지면 전이가 우회하거나 오류를 내며 아무것도 설치하지 않은 상태로 정리됩니다.
진행(정리 66) 정지 상태가 아니면 적용 가능한 수명주기 규칙이 있고(데드락 없음), 각 파이버가 밟는 단계 수가 유한하므로 모든 최대 단계열은 정지 상태로 끝납니다.
합류성(정리 73) 정지 상태가 최종 설정만의 함수이고, 수명주기 관계가 유일한 정규형으로 수렴합니다.

두 정리에는 조건이 붙습니다. 진행과 합류성은 선행 관계 n \prec m := p_n \cap d_m \neq \emptyset 가 비순환이라는 가정 위에 세워지고, 논문은 이것이 정의가 주는 것이 아니라 가정임을 명시합니다. 자기가 제공하는 키를 자기가 선언하는 컴포넌트는 n \prec n 을 만족하기 때문입니다. 복구 정확성과 합류성은 단계열이 쌍마다 독립이어야 하고, 합류성은 추가로 정지 상태에 실패한 파이버가 없어야 하며 모든 컴포넌트가 자기 제공에 대해 전사적(total on its provision) 이어야 합니다. 끝까지 진행된 활성화가 p 의 모든 키를 설치했다는 조건입니다. 진행 정리는 이름 집합이 유한하다는 것도 가정하는데, 이 가정이 배제하는 것은 자기 자신의 인스턴스를 무한정 등록하는 컴포넌트입니다.

관성이 만드는 예외도 논문에 명시되어 있습니다. 목표 뷰가 돌아설 때 이미 비행 중이던 반복은 그와 무관하게 착지하고, 그 착지는 더 이상 유효하지 않은 해석에 대해 계산된 효과를 설치합니다. 그래서 규칙들이 주는 보장은 두 갈래로 갈리고, 두 번째 갈래가 첫 번째를 안전하게 만듭니다. 전이가 하나의 해석에 대해 완주하거나, 그렇지 못하면 아무것도 설치하지 않은 상태로 정리되는 두 갈래입니다.

합류성이 실무에서 무엇을 허락하는지가 이 절의 결론입니다. 연구팀의 표현으로 이 정리는 시스템의 동적 이력이 흔적을 남기지 않는다는 것을 말합니다. 실행 중인 시스템이 어떤 활성화와 비활성화를 거쳐 왔든, 정지하는 상태는 결국 활성으로 남는 각 컴포넌트를 의존성 순서로 한 번 로드하고 한 번도 언로드하지 않았을 때의 상태입니다. 증분 계산에서 변화 전파가 세우는 처음부터 다시 계산한 결과와의 일관성이 동적 조합에서 대응하는 형태입니다. 그래서 컴포넌트를 추가하고 제거하고 제공자를 교체하고 그 교체를 되돌린 오케스트레이터는 최종 조합을 처음에 그대로 적어 놓았을 때 얻었을 상태에 도달하고, 어떤 코이펙트가 유효범위 안에 있는지 따지는 컴포넌트 작성자는 정지 상태만 놓고 추론하면 됩니다. 정적으로 조립된 시스템처럼 Cordis 응용을 추론해도 된다는 허가가 이 정리입니다.

같은 정리가 보장의 경계도 그립니다. 정리가 말하는 것은 상태이고, 그 과정에서 시스템이 밖으로 내보낸 것이 아닙니다. 시스템 경계 안에서 추적되는 획득과 경계를 넘는 배출의 구분이 여기서 다시 걸립니다. 실패도 진술에서 제외되는데, 연구팀은 이것이 진짜 발산 원인이라고 밝힙니다. 어떤 반복이 오류를 내는지는 그것이 실행된 상태에 달렸으므로 한 스케줄이 실패시킨 파이버를 다른 스케줄은 완주시킬 수 있고, 두 정지 상태는 그 파이버의 수명주기 상태에서 달라집니다. 다만 종료 복구 따름정리가 실패한 파이버의 기여를 영으로 만들므로 그 밖에서는 달라지지 않습니다.

Cordis 구현: 코어 라이브러리에서 로더까지

Cordis는 형식 모델을 실용적 프로그래밍 추상으로 실현한 구현체입니다. 웹 라우팅, ORM(Object Relational Mapping), UI 렌더링처럼 특정 도메인을 겨냥하는 응용 프레임워크와 달리 Cordis는 어떤 구체적 시나리오도 규정하지 않고, 보편적인 동적 조합 의미론을 공급하는 것만이 유일한 책임인 메타 프레임워크(Meta-framework) 입니다. 구현은 세 층으로 나뉩니다: 효과와 코이펙트 시스템을 직접 구현하는 코어 라이브러리, 여기에 설정 조정과 핫 모듈 교체를 더한 컴포넌트 로더, 그리고 그 위에 도메인 기능을 올리는 Koishi 같은 응용 프레임워크입니다.

논문은 이론 구성물과 런타임 대응물을 표로 정리하는데, 주요 항목만 옮기면 다음과 같습니다:

이론 구현
컨텍스트 타입 \Gamma_\infty ctx, 일급 컨텍스트
효과 함수와 효과 이터레이터 역연산을 반환하거나 산출하는 Effect 콜백
효과 리프팅 effect_\Gamma(e) ctx.effect(callback)
코이펙트 컨텍스트와 격리, 가로채기 테이블 ctx[@@store], ctx[@@isolate], ctx[@@intercept]
get(k), set(k, v) ctx.get(key), ctx.set(key, value)
파이버의 코이펙트 명세 d fiber.inject
파이버의 효과 함수 e fiber.apply
누산기 g fiber.dispose
커밋된 뷰 \omega fiber.committed
목표 뷰 fiber.target, refresh 가 재계산
Future 와 관성 fiber.inertia, 비행 중인 전이의 핸들
등록 원시 연산(O-Insert와 그 역연산) ctx.use 와 그 콜백의 역연산
L-Begin, L-Iter, L-Finish execute 의 반복 루프
L-Leave refresh 가 파이버를 UNLOADING으로 표시
L-Unload와 그 가드 unload 와, 통지된 의존자들을 기다리는 대기

ctx.effect: 모든 컨텍스트 변경의 단일 관문

Cordis에서 컨텍스트에 대한 모든 변경은 ctx.effect 라는 단일 원시 연산을 통과합니다. 코이펙트 제공, 컴포넌트 인스턴스화, 그 밖의 모든 컨텍스트 변경 연산이 ctx.effect 호출로 환원되므로, 컨텍스트를 통해 수행된 연산은 컴포넌트 언로드 시 자동으로 추적되고 복구됩니다. 동작상 ctx.effect 는 효과 이터레이터 리프팅의 실현입니다. 이터레이터형 콜백을 받아 한 층 위로 올리고, 호출되면 효과를 복구하는 dispose 클로저를 돌려줍니다.

구동 엔진 execute 는 콜백을 효과 이터레이터로 구동하면서 각 단계에서 산출된 역연산을 하나의 합성으로 접습니다. 각 단계 전에 호출자가 준 가드를 확인하고, 가드가 풀리면 반복을 멈춰 그때까지 누적한 역연산만 남깁니다. 이것이 계산법의 단계 경계 중단이고, 이터레이터의 done 플래그와 가드가 함께 계속 여부를 실현합니다:

async function execute(callback, guard)
    iter ← callback()
    inverse ← id
    while guard()
        (value, done) ← await iter.next()
        if value then inverse ← value ∘ inverse
        if done then break
    return inverse

ctx.effect 는 그 위에 두 가지를 얹은 얇은 래퍼(Wrapper)입니다. 하나는 자기 폐기로, 반환된 dispose 가 armed 플래그를 내려 비행 중인 반복을 멈추는 동시에 복구가 최대 한 번만 발화하게 만듭니다. 두 번 발화하면 그 효과의 어떤 적용도 만들지 않은 상태에서 역연산을 적용하게 되고, 그 자리에서는 역연산이 무엇을 되돌리도록 붙잡아 두는 것이 아무것도 없습니다. 다른 하나는 부모 합성으로, dispose 가 상위 컨텍스트의 누산기 앞에 붙습니다. 자식 효과의 역연산이 그 자체로 부모에 대한 효과라는 재귀 구조가 여기서 코드로 나타납니다.

연구팀이 함께 밝히는 한계가 하나 있습니다. ctx.effect 는 증인 조건을 검사하지 않습니다. 콜백이 역연산을 공급하지만, 그 역연산이 짝지어진 효과를 실제로 복구하는지는 런타임이 검증하는 성질이 아니라 컴포넌트 작성자의 의무입니다.

코이펙트 연산과 반응형 통지

코이펙트 연산은 각 컨텍스트가 지닌 세 개의 심볼 키 슬롯 위에서 작동합니다. @@store 는 영역 심볼에서 타입 있는 값으로 가는 값 저장소, @@isolate 는 코이펙트 키에서 영역 심볼로 가는 영역 테이블, @@intercept 는 각 키에 메타데이터를 배정하는 가로채기 테이블입니다. 앞의 둘이 두 단 해소로 합성되고, 세 번째는 바인딩에 접근할 때만 참조되어 그것이 무엇으로 해소되는지가 아니라 어떻게 쓰이는지를 조정합니다.

ctx.set(key, value) 는 ctx.effect 호출이므로 자동 추적과 복구를 물려받습니다. 콜백이 영역 심볼 아래에 값을 묶고, 반환되는 dispose 가 그것을 지웁니다. 설치와 제거 양쪽이 notify 를 불러 변화를 의존자들에게 전파합니다. notify 는 살아 있는 각 파이버에 대해 바뀐 키가 그 파이버의 inject 에 있고 같은 영역으로 해소되는지 검사해서, 그렇다면 refresh 를 불러 새 상태에 대해 그 파이버를 재평가하고, 재평가한 파이버들을 반환해 호출자가 기다릴 수 있게 합니다. 만족 여부를 뒤집는 변화가 파이버를 활성화하거나 비활성화하고, refresh 의 멱등성이 중립 변화를 무해하게 만듭니다.

여기서 구현의 결정적 선택이 나옵니다. 바인딩은 그것을 설치한 파이버가 ACTIVE인 동안만 의존자에게 가용한 것으로 셉니다. 그래서 refresh 는 선언된 각 키를 저장소가 아니라 활성 제공자에 대해 해소하고, 이것이 철회를 실제로 일어나기 한 단계 전에 의존자에게 보이게 만듭니다. UNLOADING에 들어간 제공자는 제공을 멈춘 상태이므로, 그 의존자들은 불만족 목표 뷰를 다시 계산하고 자기 해체를 시작하는데 그 시점에 제공자의 바인딩은 아직 전부 자리에 있습니다.

파이버 수명주기와 관성 상태 기계

수명주기 알고리즘에서 reload 와 unload 는 관성적입니다. 일단 들어가면 시스템이 목표 상태 변화에 응답하기 전에 전이가 완주합니다. refresh 는 코이펙트 저장소에서 fiber.target 을 다시 계산하고, 파이버가 이미 전이 중이 아니라면 reload나 unload 작업을 시작합니다:

function refresh(fiber)
    target ← target(γ, n)
    if target = fiber.target then return
    fiber.target ← target
    if fiber.inertia then return
    if target ≠ ⊥ then
        fiber.state ← LOADING
        fiber.inertia ← create_task(reload(fiber))
    else
        fiber.state ← UNLOADING     ▷ 어떤 역연산도 예약되기 전에 제공을 멈춘다
        fiber.inertia ← create_task(unload(fiber))

async function unload(fiber)
    await all(notify(fiber.ctx, provided(fiber)).map(f ↦ f.await()))   ▷ 의존자를 배수한다
    await fiber.dispose()
    fiber.dispose ← id
    fiber.committed ← ⊥
    if fiber.target = ⊥ then
        fiber.state ← INACTIVE
        fiber.inertia ← null
    else
        fiber.state ← LOADING
        fiber.inertia ← create_task(reload(fiber))

reload 는 현재 목표를 기록하고 커밋된 뷰를 확정한 뒤 컴포넌트의 효과 함수를 실행합니다. 완료 시 목표가 여전히 일치하면 ACTIVE로 들어가고, 아니면 새 목표가 \bot 인지 다른 제공자 집합인지와 무관하게 unload 로 연쇄합니다. 대칭적으로 unload 는 추적된 효과를 LIFO 순서로 복구한 뒤 INACTIVE로 들어가거나 reload 로 연쇄합니다. 이 상호 재귀가 관성을 구현합니다.

알고리즘은 두 층위에서 작동합니다. 전이 층위에서는 reload 와 unload 가 완료 시점에 목표를 확인해 전이 사이의 관성적 연쇄를 가능하게 하고, 각 전이 안의 반복 층위에서는 효과 실행이 매 반복 경계에서 목표를 확인해 한 전이 안의 부분 롤백을 가능하게 합니다. 그리고 코이펙트 순서를 실제로 담당하는 것은 세 줄입니다: reload 가 해소된 뷰를 커밋하는 줄, refresh 가 전이 작업을 만들기 전에 파이버를 UNLOADING으로 표시하는 줄, 그리고 unload 가 통지된 각 의존자가 INACTIVE에 도달하기를 기다리는 줄입니다. 대기가 개별 역연산 안이 아니라 복구 전체 앞에 놓인 이유도 연구팀이 밝히는데, fiber.dispose 가 파이버의 효과들을 동시에 시작하므로 대기를 그중 하나 안에 두면 나머지가 순서 없이 남게 됩니다.

의존성 식별에도 같은 원리가 적용됩니다. fiber.target 은 선언된 각 키를 현재 저장소에 대해 해소하고 그것을 제공하는 파이버의 uid 를 묶은 값입니다. uid 는 새로 뽑히고 재사용되지 않으므로, 교체된 제공자를 그것이 대체한 제공자로 오인할 수 없습니다. 두 제공자가 같은 값을 제공하더라도 마찬가지입니다. 그래서 파이버는 선언한 키 중 하나가 다른 파이버에 의해 제공되게 된 정확히 그 시점에 다시 로드됩니다. 제자리에서 자기 바인딩을 덮어쓰는 제공자는 관찰되지 않으므로, 교체를 전파하고 싶은 컴포넌트는 바인딩을 걷어낸 뒤 새로 설치해야 합니다.

Proxy로 매개되는 컨텍스트 접근

코이펙트 연산은 이름으로 키를 지정하는 반영적 API입니다. Cordis는 그 위에 컨텍스트를 확장하고 소비하는 두 번째 방식을 얹는데, 프로퍼티 접근입니다. 컴포넌트가 코이펙트를 메서드 호출 대신 ctx[key] 프로퍼티로, 마치 컨텍스트의 고유 구조인 것처럼 접근할 수 있습니다. TypeScript에서는 모든 프로퍼티 접근을 매개하는 get 트랩을 가진 Proxy 로 실현됩니다.

해소는 접근하는 컨텍스트에서 파이버 사슬을 위로 걸어 올라갑니다. key 를 묶은 커밋된 뷰를 가진 첫 파이버에서 접근이 승인되고 그 바인딩이 반환됩니다. key 를 선언했지만 커밋하지 않은 파이버에 닿으면 그 파이버가 로드되지 않은 것이므로 접근이 실패하고, 어떤 선언도 없이 루트에 닿으면 선언되지 않은 접근으로 거부됩니다. 이것이 맨 ctx.get 과 프록시가 달라지는 부분입니다. ctx.get(key) 는 저장소에 대한 조회라서 묶인 값이나 아무것도 반환하며 결코 실패하지 않는데, 프록시는 접근하는 파이버 자신의 뷰에 대해 해소하며 사용 지점에서 코이펙트 명세를 강제합니다. 저장소가 아니라 뷰를 읽는다는 점이 순서 정리가 기대는 지점이기도 합니다. 의존성이 사라져서 해체가 촉발된 컴포넌트에게 그 의존성을 계속 읽을 수 있게 유지해 주는 것이 바로 그것입니다.

선언적 설정과 조정

코어 라이브러리는 컴포넌트 개발자에게 명령형 원시 연산을 줍니다. 반면 응용 오케스트레이터의 관심사는 기존 컴포넌트들을 조립해 시스템을 실행하고 그 구성을 수명 내내 조정하는 일입니다. 컴포넌트 로더가 이 관심사를 선언적 설정 층으로 다룹니다. 오케스트레이터가 원하는 구성을 지속적 데이터 구조로 명시하면, 로더가 그 명세의 변화를 대응하는 명령형 파이버 연산으로 번역합니다.

설정은 항목(Entry)들로 구성되고, 각 항목은 파이버 하나를 선언하고 관리합니다. 항목은 안정적 식별자 id, 인스턴스화할 컴포넌트 모듈의 url, 항목 컨텍스트에 적용되는 isolate 와 intercept 주석, 컴포넌트에 묶여 효과 함수를 이루는 config, 그리고 관리상 꺼짐 여부인 disabled 를 기록합니다. 항목이 충실한 명세가 될 수 있는 이유는 파이버를 지탱하는 것이 정확히 항목이 기록하는 것이기 때문입니다. 지지 집합이 읽는 네 필드가 모두 항목에서 나옵니다.

로더가 항목을 통째로 허물고 다시 세우는 대신 점진적으로 조정할 수 있는 근거를 논문은 메타이론에서 끌어옵니다. 합류성 정리가 정지 상태를 최종 설정만의 함수로 만들고, 진행 정리가 시스템이 실제로 정지함을 보이므로 인스턴스화와 은퇴를 발행한 시점에 조정이 완료되며, 종료 복구 따름정리가 떠나는 파이버의 기여를 영으로 만들어 한 항목을 다시 세워도 주변 파이버가 그대로 남게 합니다. 그리고 순서 정리 덕분에 오케스트레이터가 로드 순서를 짤 필요 없이 항목들을 함께 인스턴스화할 수 있습니다. 선언된 키가 아직 제공되지 않은 파이버는 자기 L-Begin에서 기다리고, 제공자가 떠나는 파이버는 그보다 앞서 비활성화됩니다. 의존성이 제약하는 것은 모듈이 언제 가져와지고 평가되는지가 아니라 파이버가 언제 활성화되는지이므로, 로더는 모듈을 동시에 로드합니다. 큰 설정을 띄울 때 시간이 실제로 쓰이는 지점이 거기입니다.

바뀐 필드에 따라 로더는 가장 덜 파괴적인 연산을 고릅니다. id 나 url 이 바뀌면 정체나 컴포넌트가 바뀐 것이므로 항목을 다시 세우고, isolate 는 영역을 재배정하며, intercept 는 가로채기 메타데이터가 읽기 시점에 참조되므로 제자리에서 갱신되고 다시 로드할 필요가 없습니다. config 는 컴포넌트에 넘겨져 컴포넌트가 새 페이로드를 어떻게 적용할지 결정하는데, 보통 이전 것과 비교해 실질적 변화가 있을 때만 다시 로드합니다. disabled 는 설정되면 파이버를 언로드하고 해제되면 다시 로드합니다. @cordisjs/group 항목의 config 가 자식 항목 목록이라서, 그룹 조정과 항목 갱신이 나무를 따라 함께 재귀합니다.

영역 재배정에는 한 가지 까다로움이 있습니다. 항목이 실행 중에 그룹 사이를 옮겨 다닐 수 있으므로 로더가 자기 영역을 직접 관리하는데, isolate 값이 true 면 항목 전용이고 그 id 로 태그된 지역 영역을 요청해 항목이 어디로 옮겨 가든 함께 따라가고, 문자열이면 그 문자열을 지목한 모든 항목이 공유하는 전역 영역을 요청해 항목을 옮기면 어느 영역에 속하는지가 아니라 누구와 바인딩을 공유하는지가 바뀝니다. 어려운 질문은 영역 심볼이 여러 파이버에 공유될 수 있는데 그중 제공자가 누구인지 가리는 일입니다. 로더는 이를 델리미터(Delimiter) 로 답합니다. 키마다 심볼 하나를 두고 각 컨텍스트가 그 아래 자기 태그를 저장하는데, 델리미터는 컨텍스트에 쓰이고 그 자손에게 상속되므로 항목의 태그와 제공자의 태그가 일치하는 것은 정확히 두 컨텍스트가 그 키에 대해 한 격리 범위 안에서 파생된 경우, 즉 그 키의 바인딩이 항목 자신의 것이어서 함께 옮겨 가야 하는 경우입니다.

핫 모듈 교체

핫 모듈 교체는 가역 효과 패턴을 모듈 층위에 적용합니다. 파이버가 이미 자기 컴포넌트의 효과와 코이펙트 전부를 경계 짓고 있으므로, 컴포넌트인 모듈은 파이버 연산만으로 교체됩니다. 낡은 파이버를 폐기하면 그 컴포넌트가 설치한 모든 것이 복구되고, 다시 로드된 모듈에서 인스턴스화한 새 파이버가 그것을 재설치합니다. 그래서 Cordis의 HMR은 Webpack이나 Vite의 HMR과 달리 개발자가 표시한 수용 경계를 필요로 하지 않습니다.

@cordisjs/hmr 컴포넌트의 엔진은 세 단계로 작동합니다. 1단계는 모듈 분류로, 변경된 파일 집합과 핫 교체가 불가능해 전체 재시작을 유발하는 외부 모듈 집합을 입력받아 의존 부분 그래프를 순회하며 각 모듈을 수용 또는 거부로 표시합니다. 임포트 중 하나가 수용되면 그 모듈을 수용하고 임포트 전부가 거부되면 거부하며, 임포트 순환에 걸려 미결정으로 남은 모듈은 기본값으로 거부됩니다. 2단계는 낡은 항목 검출로, 의존 나무가 변경된 모듈에 닿는 항목만 걸러냅니다. 3단계는 트랜잭션 재로드로, 수용된 모듈의 캐시를 무효화하면서 제거되는 각 모듈을 백업해 두고 각 낡은 항목의 컴포넌트 모듈을 다시 임포트해 새 파이버로 갈아 끼웁니다. 어떤 모듈이 문법 오류 등으로 임포트에 실패하면 캐시를 복원하고 모든 낡은 항목을 백업본에서 다시 세워, 이미 이루어진 교체를 되돌립니다. 시스템이 절반만 다시 로드된 상태에 결코 들어가지 않게 하는 트랜잭션 보장입니다.

사례 연구: Koishi

Koishi 는 Cordis 위에 세워진 오픈소스 챗봇 응용 프레임워크입니다. 4년의 개발 기간 동안 커뮤니티가 기여한 플러그인이 4000개를 넘었고, 인스턴트 메시징 어댑터와 데이터베이스 드라이버부터 관리 콘솔과 최종 사용자 기능까지 폭이 넓습니다. 그 규모와 다양성이 실제 운영 환경에서 Cordis의 동적 조합성을 검증하는 근거가 됩니다.

논문은 사례 연구에서 세 가지를 주장합니다. 첫째는 메타 프레임워크의 표현력과 일반성입니다. Koishi의 모든 기능이 코어 라이브러리의 컨텍스트 원시 연산 위의 플러그인으로 실현되고 Koishi 자신은 챗봇 도메인의 어휘만 기여하는데, 같은 모델이 전혀 다른 런타임에서 다시 나타납니다. Koishi의 웹 콘솔은 서버가 아니라 브라우저와 그 사용자 인터페이스의 원시 연산을 조합하는, 두 번째 독립 Cordis 응용입니다. 즉 이 모델은 호스트 프레임워크가 도메인 어휘만 공급한 채로 완결된 운영 시스템을 지탱할 만큼 표현력이 있고, 효과와 코이펙트가 어떻게 조합되는지만 고정한 채 그 의미는 응용에 맡기므로 특정 도메인도 특정 런타임도 전제하지 않습니다.

둘째는 인지 부담 없는 시간적 조합성입니다. 앞서 살펴본 플러그인 시스템들은 확장 호스트를 재시작하지 않고 개별 확장의 효과를 언로드할 수 없는데, Koishi는 이 연산을 일상적으로 수행합니다. 오케스트레이터가 콘솔에서 플러그인을 비활성화하면 그 효과가 제자리에서 철회되고, 개발 중에는 HMR 엔진이 저장할 때마다 편집된 플러그인을 다시 적용하면서 시스템 다른 곳의 캐시 상태와 라이브 연결을 보존합니다. 컨텍스트를 통해 수행된 효과가 추적되고 역연산이 자동으로 합성되므로, 경험이 적은 작성자도 언인스톨 경로를 쓰지 않고 순서 있는 정리를 얻습니다.

셋째는 열린 생태계에서의 공간적 조합성입니다. 플러그인 간 의존성이 거의 없는 다른 시스템들과 달리 Koishi 생태계에는 실제 의존성 위상이 있습니다. 인스턴트 메시징 어댑터가 각 메시징 플랫폼 접근을 제공하고, 데이터베이스 드라이버가 지속 저장소를 제공하며, 기능 플러그인들이 이것들을 코이펙트로 선언하고 접근합니다. 저장소 백엔드를 바꾸거나 어댑터를 다시 연결하는 것처럼 실행 중에 제공자를 재구성하면, 해소된 의존성이 바뀐 의존자만 재활성화됩니다. 의존성이 아직 없는 플러그인은 오류를 내지 않고 그것이 나타날 때까지 비활성 상태로 기다립니다. 이 조합이 독립적으로 작성된 코드 사이에서 성립한다는 것이 사례 연구가 실증하는 부분입니다. 플러그인과 그 의존성은 보통 서로 다른 작성자가 쓰고, 그들이 조율하는 것은 둘을 잇는 코이펙트 외에 아무것도 없습니다.

논문은 타당성 위협도 명시합니다. 근거가 단일 호스트 언어의 단일 생태계에서 나왔으므로 패러다임의 장점을 그 TypeScript 실현이나 Koishi 도메인의 장점과 분리할 수 없고, 대안 아키텍처와의 통제된 비교가 아니라 관찰적 보고입니다. 따라서 사례 연구가 세우는 것은 정량적 결과가 아니라 존재와 채택의 결과이며, 추상의 부하와 개발자 생산성에 대한 영향을 기준선과 견주어 측정하는 일은 향후 과제로 남습니다. 그리고 Koishi가 현재 쓰는 것은 Cordis v3인데 논문이 제시하는 것은 효과와 코이펙트 의미론을 다듬고 로더를 재설계한 v4입니다. 핵심 조합 모델은 두 버전이 공유합니다.

논의와 한계

시스템 경계: 무엇을 되돌릴 수 있는가

모든 효과가 역연산을 지닌다고 할 때 그 역연산이 무엇을 뜻하는지는 시스템 경계(System Boundary) 가 정합니다. 시스템이 어떤 위치를 배타적으로 변경할 수 있고 변경 전 상태를 복원할 수도 있으면 그 위치는 안쪽에 놓여 추적되고 복구되며, 두 능력 중 하나라도 없으면 바깥쪽이라 그 위치에 대한 연산은 항등 함수처럼 작용해 추적도 복구도 되지 않습니다. 경계는 매체가 아니라 위치별로 그려집니다. 메모리 영역은 시스템만 쓸 때 안쪽이고 다른 프로세스도 쓸 때 바깥쪽이며, 파일은 비공개 경로의 스크래치 파일처럼 시스템만 닿을 때 안쪽이고 다른 프로그램이 읽고 쓰는 경로일 때 바깥쪽입니다. 코이펙트는 외부 위치를 구상화해 경계를 옮기는 장치입니다. 그 위치에 대한 모든 접근을 자기가 제공하는 연산 집합으로 한정하고 각 연산에 역연산을 공급하면, 항등처럼 작용했던 연산이 추적되고 복구되기 시작합니다.

경계를 넘는 연산은 보통 두 단계로 진행됩니다. 획득 단계는 접근을 얻고 경계 안쪽에 기록을 설치합니다. open 이 close 로 제거되는 서술자를 설치하고, malloc 이 free 로 반납되는 블록을 예약하고, fork 가 kill 로 종료되는 자식 프로세스를 시작하는 식입니다. 배출 단계는 그 기록이 곧 통로가 되어 데이터를 밀어 넣는데, 이 밀어 넣기는 항등처럼 작용하며 데이터를 다른 당사자가 읽고 쓸 수 있는 곳에 남깁니다. 두 단계가 경계의 반대쪽에 놓입니다. 그래도 배출에서 복구해야 하는 시스템에는 두 길이 있습니다. 그것을 만든 상태가 확실히 지속될 때까지 배출을 미루는 방식(롤백 복구의 출력 커밋 문제)과, 응용이 공급한 더 거친 동등성까지만 상태를 복원하는 보상 동작입니다. 파일을 만들었으면 지우고 청구했으면 환불하는 것이 후자입니다. 보상 동작은 역연산과 같은 LIFO 순서로 합성되지만, 메타이론은 그대로 옮겨지지 않습니다. 가환성이 더 세밀한 동등성에 대해 증명되었기 때문에 거친 동등성에 대해 다시 세워야 합니다.

서비스 다중화와 접근 제어

코이펙트 모델은 OSGi 같은 동적 컴포넌트 플랫폼의 서비스 개념과 통합니다. 키 뒤의 인터페이스가 서비스에 대응하고, 그 서비스를 제공하는 컴포넌트가 제공자, 주입하는 컴포넌트가 소비자입니다. 하나의 서비스가 여러 제공자로 구현될 때 논문은 두 형태를 구분합니다. 배타적 바인딩 은 한 번에 최대 하나만 묶여서, 구현을 바꾸려면 한 제공자를 언로드하고 다른 것을 로드해야 하므로 모든 소비자의 의존성이 순간적으로 교란됩니다. 서비스 브로커 는 인터페이스의 진입점이 되는 중앙 서비스를 두고 뒷단 제공자들과 소비자들이 모두 그것을 주입받아, 여러 제공자가 공존하고 브로커가 요청을 분배합니다. 브로커가 교란을 흡수하므로 뒷단 제공자를 갱신해도 소비자는 의존성 변화를 보지 않고 재로드도 촉발되지 않습니다.

브로커가 받치는 세 능력을 논문은 부하 분산, 롤링 업데이트, 프로세스 간 호출로 정리합니다. 제공자가 보통 컴포넌트이므로 용량을 늘리거나 줄이기 위해 추가하고 제거할 수 있고, 각 제공자가 가역 효과로 브로커에 등록하므로 언로드하면 등록이 되돌려져 브로커의 라우팅 집합에서 자동으로 빠집니다. 롤링 업데이트는 새 제공자를 추가 파이버로 로드해 브로커에 등록하고, ACTIVE가 되면 선택 가중치를 조정해 트래픽을 옮기고, 비행 중인 요청이 없어진 낡은 제공자들을 언로드하는 절차로 환원됩니다. 논문의 표현으로 이것은 전통적으로 인프라 층위의 연산이던 것을 응용 층위의 조합 패턴으로 바꿉니다.

접근 제어 쪽에서 프록시 매개 의존성 접근은 이미 일종의 접근 제어입니다. 컴포넌트는 선언한 의존성만 접근할 수 있고 선언되지 않은 접근은 오류를 냅니다. 논문은 이것이 권한이 앰비언트 권위가 아니라 참조의 소유로 부여되는 능력 기반 보안(Capability-based Security)과 구조적으로 닮았다고 봅니다. inject 선언이 능력 요청이고 컨텍스트 프록시가 능력 매개자입니다. 이 요청이 정적으로 선언되므로, 프록시가 매개하는 능력의 전체 집합이 실행 전에 알려지고 오케스트레이터가 로드 시점에 검토하고 승인할 수 있습니다. 가로채기를 쓰면 여기서 세밀한 정책으로 일반화됩니다. 파일시스템 의존성이 어떤 컴포넌트가 어떤 경로를 읽고 쓸 수 있는지 선언한 메타데이터를 지니면 제공자가 호출마다 그것을 검사하는 식이고, 이 가로채기가 어느 쪽 코드도 아니라 컨텍스트에 얹히므로 오케스트레이터가 제공자를 수정하지 않고 임의 컴포넌트의 접근을 제약할 수 있습니다. 다만 컴포넌트 코드를 신뢰할 수 없을 때는 언어 층위 접근 제어가 불충분하다는 점을 논문은 분명히 합니다. 호스트 런타임에 접근하는 악의적 컴포넌트가 하부 객체에 직접 닿을 수 있어 이런 검사가 무의미해지므로, 언어 수단이 미치지 못하는 실행 경계가 필요합니다.

순환 의존성과 컴포넌트 세분성

반응형 코이펙트 모델에서 의존성 순환은 관련 컴포넌트를 영구히 비활성으로 남깁니다. A 가 B 가 제공하는 키를 요구하고 B 가 A 가 제공하는 키를 요구하면 어느 쪽 만족 술어도 참이 될 수 없습니다. 스케줄에 의존해 발생하는 대로 검출해야 하는 병행 시스템의 데드락과 달리 이 조건은 의존성 선언만으로 예측 가능하므로, 런타임이 컴포넌트를 로드할 때 보고할 수 있습니다.

실무에서 겉보기 상호 의존성은 대개 순환을 없애는 더 세밀한 컴포넌트로 분해됩니다. 논문의 예시는 네트워크 인터페이스를 제공하는 서버와 인가 정책을 강제하는 접근 제어기입니다. 접근 제어기가 서버에 도착한 요청을 매개하고, 서버가 접근 제어 정책을 수정하는 엔드포인트를 노출하므로 두 방향의 상호작용이 있는데, 이 두 방향은 논리적으로 독립적인 관심사입니다. 분해하면 server-core, access-control-core, 두 코어에 의존해 들어오는 요청에 접근 제어를 적용하는 request-mediation, 두 코어에 의존해 서버를 통한 정책 수정을 노출하는 policy-management의 네 컴포넌트가 나옵니다. 어느 코어도 다른 코어에 의존하지 않으므로 순환이 사라집니다.

논문은 이 분해가 원리상 항상 가능하지만 컴포넌트 수를 늘린다는 대가를 인정합니다. 일반적으로 n 개의 상호작용 컴포넌트가 있으면 통합 컴포넌트 수가 n 에 대해 제곱으로 자랄 수 있습니다. 각 상호작용 쌍이 방향마다 별도 컴포넌트를 요구할 수 있기 때문입니다. 정확성이나 런타임 성능에 영향을 주지는 않고 사용자가 필요한 통합 바인딩만 골라 로드할 수 있게 되어 조합성이 오히려 올라가지만, 설정과 이름이 늘고 의존성 그래프를 파악하는 인지 부담이 커집니다. 완화 전략으로는 관련 세밀 컴포넌트를 한 설치 단위로 묶는 패키지 번들링, 이름이나 타입이 패턴에 맞는 컴포넌트를 자동으로 연결하는 관례 기반 배선, 선언적 명세에서 통합 컴포넌트 보일러플레이트를 생성하는 스캐폴드 도구가 제시됩니다.

의존성 타이핑과 버전

형식 모델에서 의존성 연결은 순수하게 키의 정체로만 성립합니다. 키 k 를 제공하는 컴포넌트가 k 를 의존성 집합에 선언한 어떤 컴포넌트든 만족시킵니다. 타입 족이 단일 컴파일 단위 안의 타입 수준 합의를 보장하지만, 컴포넌트가 독립적으로 개발되고 빌드되는 흔한 상황에서는 이 보장이 무너지고 두 문제가 생깁니다.

인터페이스 표류(Interface Drift) 는 제공자가 버전 사이에 k 에 결부된 인터페이스를 바꾸는데(필드 추가, 메서드 시그니처 변경, 행위 계약 변경) 이전 인터페이스로 컴파일된 소비자가 같은 키를 계속 선언하는 경우입니다. 코이펙트 층위에서는 의존성이 만족되지만 런타임 값이 소비자의 기대에 더 이상 부합하지 않아 타입 오류나 메서드 부재 실패, 조용한 행위 발산으로 이어집니다. 키 충돌(Key Collision) 은 독립적으로 개발된 두 제공자가 전혀 무관한 인터페이스를 같은 키 이름으로 지칭하는 경우입니다. 정체만으로 연결이 성립하므로 한 제공자의 인터페이스를 기대한 소비자가 다른 쪽 값을 호환성 검사 없이 받아들이고, 공통 계보를 공유하는 표류와 달리 기대 타입과 실제 타입 사이에 아무 관계가 없어 실패가 예측 불가하고 진단이 어렵습니다.

논문은 세 접근을 인프라 결합도 순으로 논의합니다. 키 이름 공간화는 키 공간을 K \times P 로 확장해 인터페이스 정의 패키지를 식별자에 넣어 충돌을 구성적으로 제거하지만, 패키지 이름 공간을 형식 모델 자체에 박아 넣어 외부 패키지 레지스트리에 시스템을 의존하게 만듭니다. 피어 의존성은 호스트 언어 패키지 매니저를 통해 버전 제약을 선언하는 더 가벼운 결합으로, Cordis가 현재 채택한 방식입니다. 컴포넌트 의존성은 의미상 피어 의존성이라 내부에 번들하지 않고 런타임 컨텍스트가 공급할 것을 기대하므로, npm처럼 피어 의존성을 지원하는 패키지 매니저가 설치 시점에 비호환을 잡아낼 수 있습니다. 한계는 두 가지인데, 제공자가 유의적 버전 규약을 충실히 따르는지가 강제되지 않는 관례라는 점과, 패키지 매니저가 보통 각 의존성을 단일 버전으로 해소해 한 응용 안에서 같은 패키지의 여러 버전을 로드할 수 없다는 점입니다. 구조적 호환성은 소속 검사를 제공자의 실제 인터페이스가 소비자의 기대를 구조적으로 포섭하는지 검증하는 술어로 대체하는 완전히 언어 독립적인 접근입니다. 어려움은 그 술어를 언어 독립적으로 정의하는 데 있습니다. 레코드 타입에는 간단하지만 사전 조건과 사후 조건 같은 행위 계약에서는 복잡해지고, 매개 다형성이 유계 한정을 들여오면 결정 불가능해집니다.

언어 독립성과 공동 설계

Cordis가 TypeScript로 구현되었지만 컨텍스트 패러다임은 언어에 종속되지 않습니다. 시간적 조합성이 요구하는 최소 조건은 클로저입니다. 가역 효과가 동작과 역연산을 짝지으므로 그 역연산이 복원할 상태와 함께 값으로 붙잡혀야 합니다. 그 위에 컴포넌트 코드와 그것을 로드한 부작용이 실행 중에 도입되고 철회될 수 있어야 하는데, 이 조건을 충족하는 방식이 실행 모델에 따라 갈립니다. 관리 런타임에서는 프로그래밍 가능한 모듈 레지스트리 형태를 띠어 로드된 모듈이 레지스트리에서 퇴출되고 참조가 없어지면 수집됩니다. 네이티브 코드는 모듈 레지스트리를 노출하지 않으므로 dlopen/dlclose 나 LoadLibrary/FreeLibrary 같은 명시적 동적 링킹과 언링킹 형태가 됩니다. WebAssembly는 임베더에 따라 두 길 중 하나를 갑니다.

공간적 조합성은 의존성 주입 문제로 환원되고, 언어마다 갈리는 두 층위가 있습니다. 타입 층위에서는 컨텍스트 타입이 각 키의 코이펙트를 기록해야 하는데, Haskell의 타입클래스와 Rust의 트레이트는 제공자가 자기 모듈에서 인스턴스나 impl 로 컨텍스트 타입을 확장하게 해 이를 달성하고, TypeScript의 모듈 증강도 같은 일을 합니다. 런타임 층위에서는 의존성 접근이 동적으로 매개되어야 합니다. 키 뒤의 코이펙트가 제공자의 로드와 언로드에 따라 바뀌고 컨텍스트마다 다르게 해소될 수 있기 때문입니다. JavaScript의 Proxy 나 Python의 디스크립터 프로토콜 같은 원시 연산이 그 역할을 하고, 그런 원시 연산이 없으면 런타임 리플렉션이 타입 안전성과 개발 경험을 대가로 매개할 수 있습니다.

논문의 마지막 논의는 반대 방향의 질문입니다. 패러다임과 공동 설계된 언어나 운영체제가 그 최소 조건을 넘어 무엇을 줄 수 있는가입니다. 언어 쪽에서는 컨텍스트 의미론을 지키면서 컨텍스트를 다시 암묵적으로 만들 수 있고, 그렇게 하면 효과나 코이펙트를 다루는 함수가 컨텍스트를 인자로 받지 않아도 되는 사용성 이득과, 컴포넌트가 클로저나 전역 변수를 통해 다른 컴포넌트의 컨텍스트에 실수로 닿는 일을 막는 안전성 이득이 함께 옵니다. 컴파일러에 효과와 코이펙트를 알릴 수도 있습니다. 효과 이터레이터는 역연산과 그것이 복원할 상태를 담기 위해 매 단계 클로저를 할당하는데, 효과 수행 문법이 있으면 컴파일러가 반복 전체에 대해 하나의 상태 기계를 내고 그 역연산들을 프레임에 담을 수 있습니다. 코이펙트 명세를 타입 시스템에 들이면 의존성 순환이 런타임이 아니라 컴파일 타임에 보고되고, 의존성을 키 정체가 아니라 타입 구조로 비교하는 구조적 호환성의 타입 수준 지원이 생깁니다. 운영체제 쪽에서는 컴포넌트가 선언한 코이펙트 명세를 그 컴포넌트가 닿을 수 있는 것 전부로 만들고 자기 자원을 코이펙트로 제공함으로써, 언어 밖 메커니즘에 미뤄 둔 샌드박스를 운영체제가 직접 공급할 수 있습니다. 메모리와 파일 서술자가 즉각적인 후보입니다.

기존 연구와 Cordis가 갈라지는 지점

관련 연구 절에서 논문이 스스로를 위치시키는 방식이 이 연구의 성격을 잘 드러냅니다.

모나드 효과 시스템 은 기존 범용 언어의 타입 시스템 안에 효과를 인코딩합니다. Scala의 ZIO는 계산을 ZIO[R,E,A] 로, TypeScript의 Effect-TS는 Effect<A,E,R> 로 모델링하는데, 타입 매개변수가 결과와 타입 있는 오류와 컨텍스트가 공급해야 하는 서비스를 각각 서술합니다. fp-ts 는 같은 오류와 요구 채널을 Reader 기반 모나드 변환자로 인코딩합니다. 논문이 꼽는 차이는 두 가지입니다. 첫째, 추적이 모나드 임베딩의 대가로 얻어집니다. 프로그램이 효과 타입 안에서 작성되어야만 추적을 얻는데, Cordis는 보통의 호스트 코드 위에 덧씌우는 층으로 효과를 추적합니다. 둘째, 요구가 해석으로 해소됩니다. 설치된 서비스가 연산을 공급하고, 그 서비스가 철회되면 그 연산이 수행한 것은 자리에 그대로 남습니다. Cordis는 대신 각 효과에 역연산을 짝지우고 제공자가 오고 갈 때 요구를 다시 해소합니다.

능력으로서의 대수적 효과 쪽에서 가장 가까운 연구는 Brachthäuser 등의 Effekt 언어로, 효과 타입을 능력(Capability)으로 재해석해 계산이 만들 수 있는 부작용이 아니라 컨텍스트에서 요구하는 것을 표현하게 합니다. 컨텍스트를 능력의 매개자로 다룬다는 점이 Cordis와 같습니다. 갈라지는 곳은 목적과 설정입니다. 대수적 효과는 한 연산에 여러 핸들러 의미론을 주는 모듈적 해석을 위해 효과를 드러내고, Cordis는 추적과 되돌림을 위해 드러냅니다. 그리고 Effekt는 효과를 타입 층위에서 정적으로 규율하며 능력을 기본적으로 이급(Second-class)으로 두어 어휘적 유효범위에 가두는데, Cordis는 런타임에서 규율하며 컴포넌트 제거 시 완전한 자원 복구를 겨냥합니다.

가역 효과 의미론과 등급 타입 쪽에도 가까운 선행 연구가 있습니다. Heunen 등은 Hughes의 화살(Arrow)을 단검 화살(Dagger Arrow)과 역화살(Inverse Arrow)로 각색해 가역 설정에서 부작용을 모델링했는데, 효과를 핸들러로 해소하는 대신 되돌릴 수단과 짝지운다는 점이 가역 효과와 같습니다. 차이는 가역성이 어디에 있고 얼마나 요구되는가입니다. Heunen 등은 모든 계산이 가역이라 구성적으로 보장되고 역연산이 양방향인 표시적 범주론 설정에서 작업하는데, Cordis는 계산 전체가 아니라 각 원자적 효과가 한쪽 방향 역연산을 지니는 것만 요구하고 그 역연산도 유도되는 것이 아니라 적용 지점에서 호출자가 공급합니다. 한편 Orchard 등은 등급 모달 타입을 등급 모나드를 통한 효과 추론과 등급 코모나드를 통한 코이펙트 추론을 함께 포섭하는 개념으로 제안하고 Granule 언어로 구현해, 하나의 타입 시스템이 계산이 무엇을 하는지와 무엇을 필요로 하는지를 함께 추적할 수 있음을 보였습니다. 이 계열은 모두 타입 층위에서 작동해 효과와 코이펙트가 어휘적으로 고정된 유효범위에 대해 컴파일 타임에 검사되는 정적 표기입니다. 논문은 자기 기여가 이 분석과 직교한다고 정리합니다. 같은 두 개념을 런타임 메커니즘으로 끌어올려, 고정된 프로그램 텍스트에 대해 한 번 결정되는 것이 아니라 로드된 컴포넌트 집합이 변하는 동안 다시 해소되게 만든 것입니다.

컨텍스트 지향 프로그래밍(COP, Context-oriented Programming) 은 이름을 공유하는 패러다임입니다. 실행 컨텍스트에 따라 실행 중에 활성화되고 비활성화되는 부분 메서드와 부분 클래스 정의, 즉 레이어를 언어에 넣어 기반 코드가 자기 컨텍스트 의존성을 지칭하지 않고도 동작이 적응하게 합니다. 컨텍스트를 일급이고 실행 중 변경 가능한 실체로 다루고 동작을 동적으로 활성화하고 비활성화한다는 점에서 겹치지만, 논문은 그 닮음이 명목상이라고 봅니다. COP에서 컨텍스트는 위치, 사용자, 모드 같은 주변 실행 상황을 뜻하고 활성화는 동적으로 유효범위가 정해진 범위 안에서 메서드 디스패치를 바꿉니다. 레이어는 자기가 유발한 부작용을 추적하지도 되돌리지도 않고, 활성화가 의존성 만족에 의해 통제되지도 않습니다. COP는 어떤 동작이 실행되는지를 바꾸고, Cordis는 컴포넌트가 설치하는 효과와 의존성을 조합하고 되돌립니다.

관점 지향 프로그래밍(AOP, Aspect-oriented Programming) 은 횡단 관심사를 관점으로 모듈화합니다. 기반 프로그램에서 선택된 결합점들을 정량화하는 포인트컷과 각 지점에 짜여 들어가는 어드바이스가 그것입니다. Cordis는 같은 문제를 다루지만 관점에 대응하는 것이 코이펙트입니다. 여러 컴포넌트가 의존을 선언하는 공유된 매개 지점이라서, 그중 어느 것도 수정하지 않고 그 자리에서 횡단 동작을 바꿀 수 있습니다. 두 축에서 갈립니다. AOP 포인트컷은 자기가 조언받는지 모르는 코드의 임의 결합점에 매칭되는 무자각 정량화인데, Cordis는 횡단을 각 컴포넌트가 선언한 코이펙트로 한정해 도달 범위가 정확히 그 선언된 표면입니다. 그래서 응용 오케스트레이터가 소스를 읽거나 분석하지 않고 설정 층에서 무엇이 컴포넌트를 횡단하는지 검토하고 통제할 수 있습니다. 그리고 Cordis에서 횡단 변경은 컴포넌트의 효과로 수행되므로 컴포넌트가 언로드될 때 되돌려지고 의존자에게 반응형으로 전파되는 동적 조합 모델 안의 한 수인데, 실행 중 짜기와 풀기가 가능한 동적 AOP 시스템에서도 그것은 컴포넌트 수명주기에 묶이지 않은 독립 연산입니다.

전향 상태 이전 계열 은 실행 중인 프로그램의 컴포넌트를 버전 사이에 상태를 옮기며 교체합니다. Kramer와 Magee의 정지성(Quiescence), Vandewoude 등의 완화된 평온성(Tranquility)이 세운 시점 규율 위에서, 동적 소프트웨어 갱신(DSU)은 손으로 쓴 변환 함수로 상태를 전향 이전하고, Erlang/OTP는 code_change/3 로 프로세스 층위에서 같은 일을 하며, Webpack과 Vite의 HMR은 module.hot API로 모듈 층위에서 같은 일을 합니다. 이들은 메모리 내 상태를 더 우아하게 이전하고, Cordis는 낡은 컴포넌트의 추적된 효과를 되돌린 뒤 새 컴포넌트의 효과를 깨끗한 상태에서 다시 적용하므로 컴포넌트 자신의 메모리 내 상태는 더 오래 사는 의존성에 두지 않으면 재로드를 넘기지 못합니다. 대신 Cordis는 손으로 쓴 이전 함수를 요구하지 않고, 컴포넌트를 제자리에서 갱신하는 것에 그치지 않고 완전히 언로드해 자원을 복구하는 것까지 지원합니다.

정적으로 유효범위가 정해진 되돌림 계열 은 되돌림을 구성적으로 자동화하지만 그 범위를 미리 고정합니다. 소프트웨어 트랜잭셔널 메모리는 읽기와 쓰기 로그를 기록해 메모리 연산 묶음이 커밋되거나 어보트하게 하고, 가역 컴퓨팅과 Janus 같은 가역 언어는 계산의 모든 단계를 전역적으로 가역으로 만들며, RCCS(Reversible CCS) 같은 가역 프로세스 계산법은 되돌리기를 의미론 자체에 넣습니다. 이들의 인과 일관성 기준이 Cordis의 복구가 따르는 순서의 병행 대응물이지만, 도달 범위가 의미론에 의해 고정되어 수행된 모든 동작이 되돌릴 수 있는 상태로 남습니다. Cordis 컴포넌트는 원자적 효과마다 역연산을 공급하고 누산기가 컨텍스트를 그 합성이 시작된 자리로 되돌립니다. 선형 타입, RAII, Rust의 소유권은 자원 해제를 어휘적 영역에 묶는데, Cordis는 그런 범위를 미리 고정하지 않고 컴포넌트 수명주기에 걸쳐 임의의 컨텍스트 연산을 되돌리며 어휘적 자원 관리를 한 컴포넌트 안의 지역 자원에 적합한 보완물로 취급합니다.

개입된 회수 계열 은 컴포넌트가 스스로 역연산을 공급하지 않아도 런타임이 통제하는 인터페이스에서 획득을 기록해 회수합니다. Nooks는 Linux 커널과 로드 가능한 확장 사이 경계를 넘는 모든 호출을 감싸 객체 추적기를 통과시키고, 섀도 드라이버는 같은 호출을 반대쪽에서 받아 재시작된 인스턴스를 복원하며, Akeso는 컴파일러 계측으로 기록을 얻습니다. 개발자가 기억해 쓰는 정리가 아니라 런타임이 유지하는 기록에서 회수가 따라 나오므로 가역 효과에 가장 가까운 시스템 층위 선행 연구인데, 어휘와 도달 범위가 다릅니다. 플랫폼이 무엇을 기록할 수 있는지 고정하므로 컴포넌트는 플랫폼이 이미 해제 방법을 아는 자원만 지닐 수 있고, 회수도 커밋되는 요청 하나나 같은 확장의 재시작으로 제한됩니다.

가용성 반응형 컴포넌트 모델 이 반응형 코이펙트에 가장 가까운 선행 연구입니다. OSGi의 Declarative Services와 iPOJO는 컴포넌트가 제공 서비스와 요구 서비스를 선언하게 하고 런타임이 서비스의 등장과 소멸에 따라 활성화와 비활성화를 자동으로 처리하며, iPOJO의 Gravity 프로젝트는 변화하는 서비스 가용성에 대한 자율적 런타임 적응을 명시적으로 겨냥합니다. 이들은 비활성화 콜백으로 복구하는데 두 가지 제약이 있습니다. 콜백이 손으로 쓰인 것이라 자원 안전성이 개발자 규율에 얹히고, 콜백이 동기적이라 해체가 떠나는 의존성과의 비동기 교환을 필요로 하면 기다릴 프로토콜이 없어 이미 낡았을 수 있는 참조에 대고 블로킹 대기를 하게 됩니다. 반응형 코이펙트는 비활성화가 의존자의 누적 효과를 되돌리고 관성적 Unloading 상태가 비동기 해체를 완주시켜 두 틈을 메웁니다.

값 층위 반응성 과의 대비도 유용합니다. 함수형 반응형 프로그래밍과 SolidJS, Vue, Angular Signals 같은 현대적 후신은 값 층위 세분성으로 변화를 전파합니다. Cordis의 반응형 코이펙트는 컴포넌트 층위 세분성에서 작동하며 값 층위 전파가 모델링하지 않는 비동기 수명주기 의미론을 더합니다. 일관성 면에서는 세분성 차이가 반대로 작용합니다. 의존성 그래프가 정한 순서로 한 턴 안에서 전파하는 FRP는 파생 계산이 갱신된 입력과 낡은 입력을 섞어 읽지 않을 것을 요구하는 글리치 프리덤(Glitch Freedom)을 세울 수 있지만, Cordis에는 턴에 대응하는 것이 없고 조율 동작이 하나씩 도착하므로 한 전이가 자기 코이펙트의 두 해석에 걸치지 않는다는 것만 보장합니다. 논문은 둘을 경쟁 관계가 아니라 보완 관계로 봅니다. Cordis 코이펙트가 반응형 값을 담을 수 있고, 컴포넌트가 실제로 소비하는 부분에 대해서만 갱신되면 두 층위를 아우르는 더 세밀한 반응형 코이펙트로 정련됩니다.

누구에게 유용한가

플러그인 시스템, 에이전트 하네스, 실행 중 재구성이 필요한 서버처럼 동적 조합을 직접 설계하는 개발자에게는 자신이 임시방편으로 풀어 온 문제를 명명된 개념과 정리로 다시 검토해 볼 수 있는 자료입니다. 제거 시 정리 누락, 의존성 순서, 핫 리로드의 안전성이 각각 가역 효과, 철회 가드, 관성 상태 기계라는 이름을 얻습니다. 특히 가환 키와 비가환 키를 가르는 기준(값이 독립적 항목들의 테이블인가, 순서 있는 체인인가)은 논문을 읽지 않고도 자기 플러그인 API 설계에 바로 적용할 수 있는 판정입니다.

DSH 플러그인을 작성하는 사용자라면 논문 전체보다 DSH 문서의 Cordis 입문서가 실용적인 출발점이고, 이 논문은 그 뒤에 있는 이론이 궁금할 때 읽는 쪽입니다. 반면 바로 가져다 쓸 코드를 찾는 독자에게 이 저장소는 적합하지 않습니다. 88쪽 분량의 형식적 정의와 정리가 중심인 논문 저장소이고, 실행 가능한 코드는 별도의 Cordis 저장소에 있습니다.

논문 자체의 한계도 분명합니다. 정량적 평가가 없고, 검증은 단일 언어의 단일 생태계에 대한 관찰적 보고입니다. 연구팀이 향후 검증 방향으로 꼽는 것은 에이전트 하네스입니다. AI 에이전트가 사람의 감독이 거의 없이 자기 하네스 컴포넌트를 계속 생성하고 교체하는 환경에서, 빠른 컴포넌트 교체 아래 완전 복구라는 시간적 보장과 빈번한 위상 변화 아래 의존성 조율이라는 공간적 보장을 함께 검증하겠다는 것입니다.

:scroll: Programming Paradigm for Spatiotemporal Composability 논문

:github: Cordis 논문 GitHub 저장소

:github: Cordis GitHub 저장소 (논문의 구현체)

:github: Koishi GitHub 저장소 (논문의 사례 연구)

더 읽어보기




이 글은 GPT 모델로 정리한 초안을 바탕으로 한 것으로, 원문의 내용 또는 의도와 다르게 정리된 내용이 있을 수 있습니다. 관심있는 내용이시라면 원문도 함께 참고해주세요! 읽으시면서 어색하거나 잘못된 내용을 발견하시면 댓글로 알려주시기를 부탁드립니다. :hugs:

논문의 주장이나 벤치마크에 대한 의견, 직접 재현해보신 결과가 있다면 :pytorch:파이토치 한국 사용자 모임:south_korea: 회원들을 위해 댓글로 공유해주세요! :folded_hands: