제8과: 리액티브 코이펙트: 의존성이 갖춰지면 자동으로 시작된다
한 문장 요약: 리액티브 코이펙트란 「스스로 변화하는 의존성 테이블」입니다. 컴포넌트는 자신이 무엇을 필요로 하는지만 선언하면 되고, 시스템이 이 테이블을 감시하면서 의존성이 갖춰지는 즉시 컴포넌트를 자동으로 시작(Reload)하고, 의존성이 없어지는 즉시 컴포넌트를 자동으로 언로드(Unload)합니다. 순서와 타이밍은 전혀 신경 쓸 필요가 없습니다.
1. 코이펙트 컨텍스트: 「키 → 타입이 있는 값」의 의존성 테이블
지난 과에서는 롤백 가능한 이펙트——「하다 말고 되돌릴 수 있다」를 해결하는 메커니즘——을 배웠습니다. 하지만 컴포넌트들이 서로 어떻게 의존하는가는 아직 해결되지 않았습니다. 이번 과에서 이 부분을 채웁니다.
전통적인 답은 IoC 컨테이너(제어 역전 컨테이너) 입니다. 날것의 키-값 매핑으로, "database" → 데이터베이스 객체를 등록하고 다른 컴포넌트가 이름으로 가져갑니다. Cordis 논문은 이것을 타입이 있는 의존성 테이블로 업그레이드하여 코이펙트 컨텍스트라고 부릅니다:
Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱𝑘
읽는 법: 모든 종류의 「의존성 키」 k(키 집합 K에서 온)에 대해 테이블에는 타입 𝒱𝑘의 값을 하나 저장할 수 있습니다. 저 기호가 부분 함수라는 점에 주목하세요(⇀는 부분을 의미합니다). 테이블에는 어떤 키가 없어도 됩니다. 쉬운 말로 분해하면:
| 구성 요소 | 무엇인가 | 예시 |
|---|---|---|
키 k | 의존성의 이름 | "database", "translator" |
| 값 | 의존성 그 자체 | 데이터베이스 연결, 번역기 객체 |
타입 패밀리 𝒱 | 「키 → 그 키의 값 타입」 대응표 | 키 "database"의 값은 반드시 데이터베이스 연결 타입이어야 함 |
𝒱 덕분에 각 키는 자신의 값의 타입에 정적으로 바인딩됩니다. 이것이 날것의 키-값 테이블보다 강한 점입니다. 의존성 접근에 정적 타입 안전성이 있어서, 잘못된 타입을 가져오면 실행 도중에 터지는 대신 컴파일 타임에 오류가 발생합니다.
테이블에는 네 가지 표기가 있습니다(논문 정의 12):
σ(k): 조회——키k가 테이블에 있을 때의 값;σ[k ↦ v]: 넣기——테이블에 키k, 값v를 추가(전제:k가 이전에 테이블에 없어야 함);σ ∖ k: 빼기——키k를 테이블에서 제거(전제:k가 원래 테이블에 있어야 함);k ∈ dom(σ):k가 테이블에 있는지 여부.
두 가지 전제 조건에 주목하세요: 「중복으로 넣을 수 없다」「존재하지 않는 것은 뺄 수 없다」. 이것은 롤백 가능한 이펙트의 멱등성 요구와 정확히 대응됩니다. 같은 일을 두 번 할 수 없어야 되돌리기가 의미를 갖습니다.
두 가지 핵심 연산(논문 정의 13):
get(k): 테이블에서 키k의 값을 가져옴(전제:k가 테이블에 있어야 하며, 그렇지 않으면 런타임에 실패);set(k, v): 테이블에 키k, 값v를 넣음(전제:k가 테이블에 없어야 함). 「새 테이블 + 되돌리기 함수」를 반환하며, 되돌리기 함수가k를 테이블에서 다시 제거하는 역할을 담당합니다.
여기에 이번 과 전체에서 가장 중요한 통찰이 숨어 있습니다. set 자체가 코이펙트 컨텍스트 위의 이펙트 함수라는 것입니다. 따라서 지난 과의 이펙트 메커니즘을 그대로 사용할 수 있습니다. 시스템은 의존성 등록을 자동으로 추적하고, 필요할 때 자동으로 등록을 취소합니다. 코이펙트 연산은 이펙트이고, 이펙트는 롤백 가능합니다. 하나는 「필요에 따라 설치하기」를 담당하고, 다른 하나는 「해체할 수 있기」를 담당하며, 둘은 빈틈없이 맞물립니다.
IoC 컨테이너와 비교해 보겠습니다:
| IoC 컨테이너 | 코이펙트 컨텍스트 Σ | 차이점 |
|---|---|---|
| 날것의 키-값 매핑 | 의존성의 부분 함수 | 타입 패밀리 𝒱가 있어 정적 타입 안전성 확보 |
| 수동 등록, 수동 주입 | set / get | set은 이펙트 함수로서 자동으로 되돌릴 수 있음 |
| 의존성이 없으면 오류 | 의존성이 갖춰질 때까지 기다렸다가 활성화 | 낙관적 접근을 하지 않음(다음 절에서 설명) |
2. 스펙과 만족성: 전부 갖춰져야 인정된다
이 테이블이 있으면 컴포넌트는 「준비됐다」를 어떻게 표현할까요? 답은 의존성 스펙 d 를 선언하는 것입니다. 「나는 이 키들을 쓰겠다」를 의미하는 키의 집합입니다.
시스템이 컴포넌트를 시작할 수 있는지 판단할 때 의존하는 것은 만족 술어입니다:
σ ⊨ d 는 d 안의 모든 키가 dom(σ) 안에 있을 때만 성립한다
수식으로 쓰면 σ ⊨ d ≔ ∀ k ∈ d. k ∈ dom(σ)입니다. 쉬운 말로 하면 하나라도 빠지면 만족하지 않는다는 것입니다. 이것은 「또는」이 아니라 「그리고」 관계이며, 전부 갖춰져야 인정됩니다.
이번 과 전체를 관통하는 예를 들어 보겠습니다. 어떤 번역 플러그인이 일을 하려면 두 가지가 필요합니다. "database"(어휘 조회)와 "translator"(번역 엔진)입니다. 이 플러그인이 선언하는 스펙은:
d = { "database", "translator" }
테이블의 변화와 플러그인 상태:
| 테이블에 있는 것 | 만족하는가 | 번역 플러그인 |
|---|---|---|
"database"만 있음 | 만족하지 않음(translator 부재) | 시작하지 않음 |
"database"와 "translator"가 모두 있음 | 만족함 | 시작해서 작동 |
"translator"는 그대로인데 "database"가 빠짐 | 만족하지 않음(database 부재) | 정지 |
왜 이렇게 엄격할까요? 논문은 단도직입적으로 말합니다. 컴포넌트는 의존성에 낙관적으로 접근해서는 안 된다는 것입니다. 존재하지 않는 의존성에 접근하면 런타임에 실패합니다. 올바른 자세는 「접근하다가 터지기」가 아니라 「모든 의존성이 갖춰진 후에 활성화하기」입니다.
이 판단이 정말로 가능함을 보장하는 기술적 포인트가 두 가지 더 있습니다:
- 결정 가능:
dom(σ)는 유한 집합이므로 「만족하는지 아닌지」는 항상 계산할 수 있고, 멈춰 버리는 일이 없습니다; - 모든 변화가 관측됨: 테이블에 대한 모든 수정은 이펙트 함수를 통해 이루어지고(이 함수들의 역은 이전 정의역을 복원합니다), 따라서 모든 이펙트 경계에서 만족성 변화가 검출될 수 있습니다. 이것이 「리액티브성」의 대수적 기초입니다. 시스템은 모든 코이펙트 변화가 관측됨을 보장합니다.
3. 통지와 분류: 모든 변화를 활성화형·정지형·중립으로 나누기
이제 시스템은 테이블 σ를, 각 컴포넌트는 스펙 d를 갖고 있습니다. 앞으로의 규칙은 간단합니다. 테이블이 변할 때마다 「변화 전 σ」와 「변화 후 σ′」를 비교하고, d의 만족성이 바뀌었는지에 따라 분류합니다(논문 정의 15):
| 분류 | 조건 | 의미 | 시스템 동작 |
|---|---|---|---|
| 활성화형 | 전에는 불만족, 후에는 만족 | 의존성이 방금 갖춰짐 | 컴포넌트의 이펙트 실행(완전 추적) → Reload |
| 정지형 | 전에는 만족, 후에는 불만족 | 의존성이 방금 사라짐 | 누적된 이펙트를 복원 → Unload |
| 중립 | 그 외(양쪽 모두 만족하거나, 양쪽 모두 불만족) | 만족성 변화 없음 | 시작도 정지도 하지 않음(값이 바뀌면 리로드) |
의사 코드로 쓰면 논문의 그 삼지 분기 판단입니다:
if 전에는 불만족 그리고 후에는 만족 → 활성화형
else if 전에는 만족 그리고 후에는 불만족 → 정지형
else → 중립
세 가지 핵심 포인트:
① 활성화는 Reload를, 정지는 Unload를 트리거합니다. 활성화형 전이는 의존성이 방금 갖춰졌음을 의미하며, 시스템은 컴포넌트의 이펙트를 실행하고 완전히 추적하며 컴포넌트가 동작하기 시작합니다. 정지형 전이는 의존성이 방금 사라졌음을 의미하며, 시스템은 그동안 누적된 모든 이펙트를 복원합니다. 컴포넌트는 깨끗하게 언로드되어 어떤 부수 효과도 남기지 않습니다. 그럼 중립 전이는요? 예컨대 「값은 바뀌었지만 의존성은 여전히 갖춰져 있는」 경우입니다. 만족성이 변하지 않았으므로 시작도 정지도 하지 않지만, 시스템은 리로드(reload)를 한 번 트리거해서 컴포넌트가 새 값을 받을 수 있게 합니다.
② 의존성 순서는 자동으로 생기며, 수동으로 선언할 필요가 없습니다. 컴포넌트 A가 set("database", ...)로 키를 제공하고, 컴포넌트 B가 "database" ∈ d_B를 선언한다고 합시다. 그러면:
- B의 만족성은
"database" ∈ dom(σ)를 요구하며,"database"는 A가 완전히 활성화되고set이 실제로 효력을 발휘한 후에야 테이블에 나타납니다. 따라서 B는 A 이후에 활성화됩니다(의존하는 쪽은 의존 대상 이후에 활성화됨); - 반대로 A를 언로드하면
"database"가 테이블에서 빠져 B의 만족성이 순식간에 깨집니다. 따라서 시스템은 A가 복원을 시작하기 전에 B가 완전히 정지되어 있음을 보장합니다(의존 대상은 의존하는 쪽 이후에 언로드됨).
이 순서는 누구도 선언할 필요가 없습니다. 통지 메커니즘의 자연스러운 결과입니다. 논문의 원문을 빌리면, 올바른 의존성 순서는 구조적 보장이 됩니다. 순서를 시스템이 도출하기 때문에 잘못 작성할 수가 없습니다.
③ 컴포넌트 자신은 아무것도 관리하지 않습니다. 시작, 대기, 정지는 모두 시스템이 만족성을 감시하며 자동으로 수행하고, 컴포넌트가 해야 할 유일한 일은 「무엇이 필요한지」를 선언하는 것뿐입니다. 아래 그림이 이 전체 흐름입니다:
依赖满足性变化 → 激活 / 停用 / 中性 三种迁移分类
그림의 흐름은 대기(무언가 부족) → 의존성 갖춰짐 → 활성화(실행 중) → 의존성 사라짐 → 정지(모든 부수 효과 취소) 입니다. 하단의 「값은 바뀌었지만 의존성은 여전히 갖춰짐 → 자동 재시작(리로드)」이라는 문구는 중립 전이에서의 리로드 동작과 정확히 대응됩니다.
4. 격리와 인터셉션: 같은 테이블의 두 가지 고급 활용법
기본 컨텍스트 Σ는 모든 컴포넌트가 공유하는 평탄한 테이블입니다. 실제 시스템에서는 이것만으로 부족한 경우가 많아서, 논문은 두 가지 확장 방향——**격리(Isolation)**와 인터셉션(Interception)——을 제시합니다. 이 둘은 완전히 다른 문제를 해결합니다.
4.1 격리(Isolation): 같은 키가 다른 컨텍스트에서 다른 값으로 해석된다
먼저 시나리오를 보겠습니다. 멀티테넌트 시스템에서 테넌트 A와 테넌트 B 모두 "database"라는 키를 쓰고 싶지만, 반드시 각자의 데이터베이스에 연결해야 합니다. 평탄한 테이블 하나로는 불가능합니다.
핵심은 테이블을 두 계층으로 나누는 것입니다:
Σiso = 격리 도메인 테이블 ρ(키 → 도메인 식별자 r) × 의존성 테이블 σ(도메인 식별자 r → 타입이 있는 값)
키 k에 접근할 때, 먼저 ρ(k)를 조회해 도메인 식별자 r을 얻고, 다음으로 σ(r)를 조회해 실제 값을 얻습니다. 연산이 하나 더 추가됩니다. isolate(k, r): 키 k를 도메인 r에 바인딩합니다. 이로써:
- 같은 키
k가 서로 다른 격리 도메인에서 완전히 다른 값으로 해석됩니다; - 격리는 런타임에 동적으로 조정할 수 있습니다. 전통적인 의존성 주입보다 세밀해서, 특정 컴포넌트에 맞게 개별적으로 커스터마이즈할 수 있습니다;
- 모든 연산은 여전히 이펙트 함수(
𝔈Σiso)이며, 그대로 롤백 가능합니다.
논문은 이것을 「런타임 임시 다형성 시스템」이라고 부릅니다. 이것은 멀티테넌트 시스템, 테스트 환경(테스트 케이스마다 격리 도메인 하나씩, 서로 오염시키지 않음), 컴포넌트 샌드박스에 폭넓게 적용됩니다.
4.2 인터셉션(Interception): 의존성 값을 건드리지 않고 횡단 메타데이터를 부가한다
다른 시나리오를 보겠습니다. 의존성 값을 바꾸고 싶은 게 아니라, 접근할 때 약간의 추가 정보를 붙이고 싶은 경우입니다. 예컨대 모든 데이터베이스 접근에 「현재 사용자가 누구인지」를 붙이거나, 로그 태그나 권한 표시를 추가하는 경우입니다.
핵심은 테이블에 메타데이터 계층을 추가하는 것입니다:
Σinter = 컨텍스트 메타데이터 ι(컨텍스트가 지님) × 프로바이더 함수 테이블 σ(키 → 「메타데이터 → 값」의 함수)
키 k에 접근할 때, 시스템은 컴포넌트가 선언한 메타데이터 d(k)와 컨텍스트가 지닌 메타데이터 ι(k)를 병합하고(각 키는 자체 병합 의미론을 가지며, 예컨대 스칼라 필드는 오른쪽 값을, 컬렉션 필드는 합집합을 취함), 프로바이더 함수를 병합 결과에 적용합니다. 병합이 오른쪽 편향이라는 점에 주목하세요. 컨텍스트 메타데이터가 우선권을 가지며 컴포넌트의 선언을 덮어쓸 수 있습니다. 이렇게 하면 바깥쪽 컨텍스트가 컴포넌트 자체를 수정하지 않고도 컴포넌트가 코이펙트를 어떻게 사용하는지를 제약할 수 있습니다.
격리 vs 인터셉션, 한 문장으로 구분하기
| 격리 Σiso | 인터셉션 Σinter | |
|---|---|---|
| 해결하는 문제 | 같은 키가 다른 컨텍스트에서 다른 값으로 해석됨 | 접근 시 추가 동작 / 메타데이터를 부가 |
| 구현 수단 | 키 → 도메인 → 값, 이중 매핑 | 메타데이터 병합 + 프로바이더 함수 |
| 비유 | 각 테넌트가 자기 데이터베이스 연결을 사용 | 모든 접근에 자동으로 「현재 사용자」 태그가 붙음 |
| 의존성 값이 바뀌는가 | 바뀜(다른 값으로 교체) | 바뀌지 않음(같은 값이고 정볼만 부가됨) |
핵심 포인트 정리
- 코이펙트 컨텍스트 Σ는 「키 → 타입이 있는 값」의 의존성 테이블:
set으로 넣고,get으로 꺼내고, 되돌릴 수 있습니다. 타입 패밀리𝒱가 정적 안전성을 보장하여 IoC 컨테이너의 날것 키-값 테이블보다 강합니다. - 만족 술어 σ ⊨ d: d 안의 모든 키가 테이블에 있어야 함. 하나라도 빠지면 불만족이며, 전부 갖춰져야 인정됩니다.
- 모든 테이블 변화는 만족성 변화 여부에 따라 분류됨: 활성화형 → Reload, 정지형 → Unload, 중립 → 시작도 정지도 하지 않음(값이 바뀌면 리로드).
- 의존성 순서는 구조적 보장이며 수동 선언이 필요 없음: 의존하는 쪽은 의존 대상이 활성화된 후에 활성화되고, 의존 대상은 의존하는 쪽이 정지된 후에야 언로드됩니다.
- 격리 = 같은 키가 다른 컨텍스트에서 다른 값으로 해석됨(멀티테넌트 / 테스트); 인터셉션 = 횡단 메타데이터를 부가하고 의존성 값은 건드리지 않음.
🚀 다음 과에서는 카메라를 가까이 가져가 단일 컴포넌트의 일생을 살펴 봅니다. 제9과 「컴포넌트 생명주기: 멱등성, 반복, 에포크, 비동기」.
자가 테스트 · 리액티브 코이펙트
답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.
