1강: 초록과 서론: 동적 컴포지션과 두 가지 차원
한 줄 요약: 이 논문은 "런타임에 컴포넌트를 안전하게 장착하고 분리하는 것"에 이론적 기반을 마련하려 합니다. 이를 위해서는 서로 독립적인 두 가지 차원이 동시에 성립해야 함을 발견했습니다. 시간적으로는 분리했을 때 원상복구가 가능해야 하고(시간적 컴포저빌리티), 공간적으로는 의존성이 자동으로 조정되어야 합니다(공간적 컴포저빌리티). 그리고 두 개의 열쇠를 제시합니다. 가역 이펙트와 반응형 코이펙트입니다. Cordis는 바로 이 이론의 구현이며, DSH는 그 어깨 위에 세워졌습니다.
1. 동적 컴포지션: 런타임에 장착하고 분리하는 컴포넌트
1.1 먼저 "정적 컴포지션"과 "동적 컴포지션"을 구분하기
컴포지션, 즉 더 단순한 부품으로 복잡한 시스템을 조립하는 것은 소프트웨어 공학의 토대가 되는 원칙입니다.
전통적으로 컴포지션은 정적입니다. 함수 호출, 모듈 임포트, 클래스 상속은 모두 컴파일 타임에 해석되어 프로그램 실행 내내 고정됩니다:
// 정적 컴포지션: 컴파일 타임에 고정되어 런타임에는 바꿀 수 없음
import { readFile } from "node:fs"; // 이 단계는 컴파일 시점에 해석 완료
function loadConfig() {
return readFile("config.json", "utf8"); // 이 호출 관계는 전 기간 동안 고정
}
반면 동적 컴포지션은 런타임에 컴포넌트를 로드, 언로드, 재설정하는 것을 말합니다. 코드를 바꿀 필요도, 재컴파일할 필요도 없이, 시스템이 돌아가는 동안 컴포넌트를 장착하거나 분리할 수 있습니다:
// 동적 컴포지션: 시스템 실행 중 언제든 컴포넌트를 탈착
installComponent(chatPlugin); // 장착: 채팅 기능이 즉시 사용 가능
uninstallComponent(chatPlugin); // 분리: 채팅 기능이 즉시 사라지고 시스템은 계속 동작
| 비교 | 정적 컴포지션 | 동적 컴포지션 |
|---|---|---|
| 언제 결정되는가 | 컴파일 타임 | 런타임 |
| 컴포넌트를 교체할 수 있는가 | 교체 = 코드 수정, 재컴파일 | 언제든 장착·분리·재설정 가능 |
| 대표적인 예 | 함수 호출, import, 클래스 상속 | 플러그인 시스템, 자기 진화 에이전트 프레임워크 |
🎁 비유: 정적 컴포지션은 "다 지어진 건물"입니다. 평면은 설계도 단계에서 이미 결정되어, 바꾸려면 내력벽을 허무는 수밖에 없습니다. 동적 컴포지션은 "레고 한 판"으로, 어떤 블록이든 언제든 끼우고, 빼고, 자리를 옮길 수 있습니다.
1.2 누가 동적 컴포지션을 필요로 하는가? — 플러그인 시스템과 자기 진화 에이전트
논문은 두 부류의 "사용자"를 지목합니다:
- 플러그인 시스템: VS Code 확장, 브라우저 플러그인, 게임 모드(Mod) — 사용자가 새 플러그인을 설치하면 기능이 즉시 나타나고, 플러그인을 제거하면 기능이 즉시 사라집니다. 플러그인 마켓플레이스의 번영은 바로 "런타임에 장착·분리가 가능하다"는 점에 기반합니다.
- 자기 진화 에이전트 프레임워크: 에이전트는 장기간 실행되는 동안 스스로에게 새 도구를 장착하고, 오래된 도구를 교체하며, 심지어 자기 자신의 런타임을 수정해야 합니다 — 실행 중인 시스템이 자기 자신을 수정하는 것입니다(DSH의 "자기 참조적 수정"을 기억하시나요?).
1.3 왜 기존 소프트웨어는 할 수 없는가?
"플러그인 시스템은 진작에 있지 않았나?"라고 말할 수도 있습니다. 문제는 — 장착·분리가 가능하다는 것이 깔끔하게 장착·분리된다는 뜻은 아니라는 것입니다.
현재의 관행은 조악한(coarse-grained) 메커니즘에 의존하며, 논문은 두 가지 문제점을 정리합니다:
- 재시작으로만 재설정 가능 — 중요한 설정을 바꾸고 싶다고요? 재시작하세요. 재시작은 모든 런타임 상태(메모리 안의 세션, 캐시, 임시 데이터가 모두 사라짐)를 잃는다는 뜻입니다.
- 분리해도 깨끗하지 않음 — 플러그인을 제거한 후에도 등록했던 이벤트 리스너가 시스템에 "눌러붙어" 계속 이벤트에 응답할 수 있고, 할당했던 리소스가 회수되지 않을 수 있으며, 시스템에 "좀비"가 잔뜩 남습니다.
🎁 비유: 어항의 물을 갈고 싶을 뿐인데, 먼저 "물고기를 건져내고, 물을 버리고, 다시 물고기를 넣어야" 하는 것과 같습니다. 물만 갈고 싶었을 뿐인데, 대가는 어항의 생태계 전체를 다시 구축하는 것입니다. 더 나쁜 것은, 물고기를 건져낸 후에도 어항 속 수초가 눌러붙어 떠나지 않는 경우가 있다는 점입니다.
논문의 핵심 주장은 동적 컴포지션에 부족한 것이 공학적 기교가 아니라 이론적 기반이라는 것입니다. 정적 컴포지션에는 매우 성숙한 형식화 프레임워크(타입 시스템, 범주론……)가 있지만, 동적 컴포지션에는 그에 상응하는 이론적 뒷받침이 없습니다. 이 논문은 바로 그 공백을 메우러 온 것입니다.
2. 두 개의 직교 차원: 깔끔한 분리 × 단정한 배치
논문은 "동적 컴포지션에 실제로 필요한 것"을 규명하려면 컴포지션의 대수적 성질만 봐서는 부족하며, 서로 직교하는 두 가지 차원을 식별해야 한다고 주장합니다.
직교란 무슨 뜻일까요? 바로 두 가지 일이 서로 독립적이고 서로 간섭하지 않는다는 뜻입니다 — 한쪽을 해결해도 다른 한쪽이 덩달아 해결되지 않습니다. 그리고 어느 한쪽이라도 없으면 동적 컴포지션은 성립하지 않습니다.
2.1 시간적 컴포저빌리티: 분리하면 원상복구 가능
정의(쉬운 말로): 컴포넌트를 제거할 때, 해당 컴포넌트가 공유 환경에 가한 모든 수정을 완전하고 안전하게 롤백해야 합니다 — 모든 리소스 할당, 이벤트 등록, 상태 변경이 추적되어야 하고, 컴포넌트 제거 시 순서 있게 회수되어야 합니다.
다르게 말하면, 컴포넌트는 실행되는 동안 "환경을 건드립니다" — 메모리를 점유하고, 리스너를 등록하고, 설정을 바꾸고, 파일을 씁니다. 시간적 컴포저빌리티는 이것을 분리할 때 건드린 모든 것이, 마치 처음부터 없었던 것처럼 복원될 것을 요구합니다.
🎁 비유: 주방입니다. 요리하는 동안 물을 쓰고, 조리대를 차지하고, 불을 켜고, 조미료 위치를 옮깁니다. 요리가 끝나면 모든 것을 제자리에 돌려놓고, 조리대를 깨끗이 닦고, 불을 꺼야 합니다. "요리 후 주방이 원래대로 복구되는가"가 바로 시간적 컴포저빌리티입니다. 제대로 된 요리사는 "탄 냄비를 싱크대에 담가둔 채" 두지 않습니다.
정적 환경에서 이 일은 간단합니다 — 함수가 종료되면 지역 변수가 자동으로 소멸됩니다(이것이 논문에서 언급하는 RAII와 중괄호 스코프입니다). 마치 "함수 종료 = 주방이 자동으로 정리"되는 것과 같습니다. 하지만 동적 환경에서는 컴포넌트가 오래 살아남고 상태를 가지며, 그 이펙트의 스코프가 어휘적 경계의 구속을 받지 않습니다 — 컴포넌트가 제거되는 시점에 그 "주방"은 이미 시스템 전체에 흩어져 있을 수 있습니다.
2.2 공간적 컴포저빌리티: 의존성의 자동 조정
정의(쉬운 말로): 컴포넌트는 서로 간의 의존성을 구조화되고 검증 가능한 방식으로 선언하고, 발견하고, 해석할 수 있어야 합니다. 그리고 의존성이 변경될 때 컴포넌트의 생명주기를 조정해야 합니다.
다르게 말하면, 컴포넌트들은 고립되어 있지 않습니다 — 컴포넌트 A가 컴포넌트 B가 제공하는 서비스를 필요로 할 수 있습니다. 공간적 컴포저빌리티는 A가 "내가 필요한 것"을 명확히 선언할 수 있고, 시스템이 "누가 제공하는가"를 자동으로 찾을 수 있으며, 제공자가 변경될 때(장착, 제거, 버전 교체) 의존하는 쪽이 자동으로 따라서 시작하거나 중지할 것을 요구합니다.
🎁 비유: 콘센트입니다. 가전제품은 발전소가 어디 있는지 알 필요가 없습니다. 표준 플러그 하나로 "220V 교류 전원이 필요합니다"라고 선언하기만 하면, 콘센트가 전력 공급을 책임집니다. 가전제품을 뽑을 때(제거), 그것에 의존하는 것들(예: 그 화면 출력이 필요한 모니터)이 억지로 버티며 작동해서는 안 됩니다. "꽂으면 작동하고, 뽑으면 멈추고, 정전되면 자동으로 차단되는 것"이 바로 공간적 컴포저빌리티입니다.
정적 환경에서 이 일은 "모듈 임포트 해석"입니다 — import 문이 컴파일 타임에 의존성을 연결합니다. 하지만 동적 환경에서는 의존성이 실행 중에 나타나고, 사라지고, 심지어 정체가 바뀝니다. 오늘은 B가 이 서비스를 제공하다가, 내일 B가 제거되고 C로 교체된다면 A는 어떻게 해야 할까요? 이것이 공간적 컴포저빌리티가 답해야 할 질문입니다.
2.3 두 차원 비교 + 다이어그램
| 시간적 컴포저빌리티 | 공간적 컴포저빌리티 | |
|---|---|---|
| 다루는 문제 | 분리하면 원상복구가 가능한가? | 컴포넌트끼리 어떻게 조정되는가? |
| 관할하는 것 | 컴포넌트가 환경에 가한 수정(부수 효과) | 컴포넌트가 환경에 요구하는 것(의존성) |
| 생활 속 예시 | 요리 후 주방 원상복구 | 꽂으면 작동, 뽑으면 정지 |
| 정적 환경의 대응물 | 어휘 스코프 / RAII | 모듈 임포트 해석 |
| 동적 환경의 어려움 | 오래 살아남고 상태를 가지는 이펙트로, 스코프가 어휘적 경계의 구속을 받지 않음 | 의존성이 실행 중에 나타나고, 사라지고, 정체가 바뀜 |
两个维度互相独立(正交):一个管「拆了干不干净」,一个管「组件怎么协调」
💡 그림 속 두 키워드에 주목하세요. 시간 차원은 "부수 효과의 완전한 취소"에 대응하고, 공간 차원은 "의존성이 갖춰져야 시작"에 대응합니다. 두 차원은 서로 독립적입니다 — 하나는 "분리가 깔끔한가"를 관할하고, 다른 하나는 "배치가 단정한가"를 관할합니다.
3. 두 개의 열쇠: 이펙트와 코이펙트
논문은 두 가지 고전적 개념을 사용해 두 차원에 각각 대응하며, 이들을 "정적 분석 도구"에서 "런타임 메커니즘"으로 격상시킵니다.
3.1 이펙트(effect): 계산이 환경에 무엇을 했는가
이펙트 = 계산이 발생시킬 수 있는 부수 효과 — 프로그램이 세상에 미치는 영향: 상태를 바꾸고, 파일을 읽고, 네트워크 요청을 보내고, 리스너를 등록하는 것.
이펙트는 "출력" 방향의 개념입니다. 무엇을 건드렸는가?
논문은 이펙트를 가역 이펙트(reversible effects)로 격상시킵니다. 모든 컨텍스트 변환에는 명시적 역변환이 짝을 이룹니다 — 당신이 한 모든 일에 "취소 설명서"가 등록되는 것입니다. 컴포넌트 제거 시 목록에 따라 동작을 역순으로 재생하면 상태가 완전히 복원됩니다:
// 직관 버전: 각 작업에 "취소 작업"을 등록
const install = () => {
registerListener("click", onClick); // 실행: 리스너 등록
setConfig("theme", "dark"); // 실행: 테마 변경
};
// 컴포넌트 제거 = 모든 undo를 역순으로 실행
const uninstall = () => {
setConfig("theme", "light"); // 취소 2: 테마를 원래대로
unregisterListener("click", onClick); // 취소 1: 리스너 해제
};
🎁 비유: 가역 이펙트 = "전 과정 녹화 + 모든 프레임을 되감기 가능". 컴포넌트가 무엇을 했는지 시스템은 모두 장부에 기록합니다. 제거할 때는 장부대로 거꾸로 정산하며, 한 푼도 떼먹지 않습니다.
3.2 코이펙트(coeffect): 계산이 환경에 무엇을 필요로 하는가
코이펙트 = 계산이 자신의 환경에 요구하는 것 — 접근해야 할 리소스, 갖춰야 할 능력, 의존해야 할 서비스.
코이펙트는 "입력" 방향의 개념입니다. 무엇이 필요한가? 이것은 이펙트의 쌍대(뒤집은 것)입니다. 이펙트는 프로그램의 세상에 대한 영향을 기술하고, 코이펙트는 프로그램의 세상에 대한 요구를 기술합니다.
논문은 코이펙트를 반응형 코이펙트(reactive coeffects)로 격상시킵니다. 컴포넌트는 의존성을 타입이 지정된 컨텍스트에 등록하고, 시스템은 이 의존성 목록을 주시합니다 — 의존성이 갖춰지면 컴포넌트가 자동으로 활성화되고, 의존성이 사라지면 자동으로 비활성화됩니다:
// 직관 버전: 의존성을 선언하고, 갖춰져야 시작
const plugin = {
requires: ["database", "logger"], // 코이펙트: 필요한 것
start() { /* database와 logger가 모두 있어야 호출됨 */ },
stop() { /* database가 죽으면 자동으로 중지 */ },
};
🎁 비유: 반응형 코이펙트 = 콘센트 + 전기계량기. 가전제품의 플러그가 "전기가 필요합니다"라고 선언하면, 콘센트는 전기가 있음을 감지해야만 전력을 공급하고, 정전되면 자동으로 차단합니다 — 가전제품이 스스로 발전소를 주시할 필요는 없습니다.
3.3 마무리 표
| 개념 | 답하는 질문 | 방향 | 생활 속 비유 | 논문의 격상 후 |
|---|---|---|---|---|
| 이펙트 effect | 무엇을 건드렸는가? | 세상에 대한 영향(출력) | 요리할 때 물을 쓰고 조리대를 차지 | 가역 이펙트(명시적 취소 포함) |
| 코이펙트 coeffect | 무엇이 필요한가? | 세상에 대한 요구(입력) | 가전제품이 콘센트의 전력 공급을 필요로 함 | 반응형 코이펙트(의존성이 갖춰지면 자동 시작/중지) |
⚠️ 기억법: 이펙트는 "한 것", 코이펙트는 "필요한 것"을 본다 — 하나는 시간 차원(분리하면 복원)을, 다른 하나는 공간 차원(의존성 조정)을 관할합니다.
마지막으로, 논문은 이 이론을 실제로 사용 가능한 것으로 만들었습니다. Cordis — 시공간적 컴포저빌리티를 위한 메타 프레임워크("프레임워크를 만드는 프레임워크")로, 두 부분으로 구성됩니다. 이펙트 추적과 코이펙트 해석을 갖춘 코어 라이브러리, 그리고 설정 조정과 핫 모듈 교체를 갖춘 선언적 컴포넌트 로더입니다. DSH는 바로 Cordis 위에 구축되었습니다 — 그러니 이 논문을 정독하는 것은 곧 DSH의 토대를 배우는 것입니다.
4. 핵심 정리
- 동적 컴포지션 = 런타임의 컴포넌트 탈착: 플러그인 시스템과 자기 진화 에이전트 모두 이것을 필요로 합니다. 현재의 방식은 "재시작 + 조악한 메커니즘"에 의존하여 상태를 잃고 분리도 깨끗하지 않습니다.
- 두 개의 직교 차원: 시간적 컴포저빌리티(부수 효과를 복원할 수 있는 분리)와 공간적 컴포저빌리티(의존성의 자동 선언·발견·조정)는 서로 독립적이며 둘 다 없어서는 안 됩니다.
- 정적 대 동적의 격차: 정적 세계에서는 시간적 컴포저빌리티 ≈ 어휘 스코프(RAII), 공간적 컴포저빌리티 ≈ 모듈 임포트이지만, 동적 세계에서는 둘 다 매우 어려워집니다.
- 이펙트 = 계산이 환경에 무엇을 했는가(부수 효과, 출력 방향). 가역 이펙트로 격상: 모든 작업에 명시적 역변환이 짝을 이루고, 제거 시 역순으로 재생됩니다.
- 코이펙트 = 계산이 환경에 무엇을 필요로 하는가(의존성, 능력, 리소스, 입력 방향). 반응형 코이펙트로 격상: 의존성이 갖춰지면 자동으로 활성화되고, 사라지면 자동으로 비활성화됩니다.
🚀 다음 섹션에서는 논문의 "동기 부여 예제"로 들어갑니다. 플러그인 시스템과 자기 진화 에이전트 프레임워크라는 두 가지 실제 시나리오에서 동적 컴포지션이 구체적으로 어떻게 무너지는지, 그리고 조악한 우회책이 왜 겉핥기식 처방에 그치는지 살펴겠습니다.
자가 테스트 · 초록과 서론
답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.
