스폰서LobeHubLobeHub자세히 알아보기
dshfind

6강: 되돌릴 수 있는 이펙트: 설치할 수 있으면, 제거할 수도 있어야 한다

한 줄 요약: 되돌릴 수 있는 이펙트는 「부수 효과」를 「변환 + 역변환」으로 탈바꿈시킵니다——환경을 수정할 때마다 자동으로 「역방향 열쇠」가 마련되어, 설치할 때는 장부에 기록되고 제거할 때는 역순으로 자동 복원됩니다. 이후로 「제거핟고 흔적이 남지 않음」은 손수 작성하는 정리 코드가 필요 없이, 구조 자체에 의해 보장됩니다.

1단계: 복습——시간 합성 가능성은 「제거할 수 있음」을 요구한다

이전 강의에서는 「상자(모나드)」를 사용해 부수 효과를 명시적이고 제어 가능하게 만들었습니다. 하지만 명시적인 것은 첫걸음일 뿐이며, 이 논문이 진짜로 원하는 것은 동적 합성입니다: 런타임에 컴포넌트를 설치하고, 다 사용하면 제거하는 것——마치 운영체제가 드라이버를 로드하거나 DSH가 플러그인을 마운트하는 것처럼요.

논문은 이 능력에 공식적인 이름을 붙입니다: 시간 합성 가능성(time composability). 이는 다음을 요구합니다:

  • 컴포넌트는 런타임에 로드될 수 있다;
  • 컴포넌트는 런타임에 언로드될 수 있다;
  • 언로드 시 공유 환경은 합성 전 상태로 복원되어야 한다——설치하기 전이 어떤 모습이었다면, 제거한 후에도 그 모습이어야 합니다.

🏠 비유: 기숙사에 새 룸메이트가 이사 와서 벽에 못을 박고 장식 그림을 걸었습니다. 룸메이트가 이사 나갈 때 벽은 원래대로 복원되어야 합니다——그렇지 않으면 다음 룸메이트가 들어와서 보게 되는 것은 구멍투성이 벽일 것입니다.

이로부터 하나의 엄격한 요구 사항이 도출됩니다: 컴포넌트가 환경에 가하는 모든 수정은 추적 가능하면서 동시에 가역적이어야 합니다.

  • 추적 가능(trackable): 시스템이 무엇을 변경했는지 알아야 합니다;
  • 가역적(invertible): 시스템이 모든 변경을 그대로 되돌릴 수 있어야 합니다.

이 두 가지를 만족하지 못하면 어떻게 될까요? 가장 전형적인 결과는 손수 작성하는 정리 로직입니다: 각 컴포넌트가 언로드 시 스스로 「복원 코드」를 작성합니다. 많이 작성하다 볩면 누락되고, 틀리고, 잊어버리게 됩니다——한 곳을 잊으면 환경이 조용히 오염되고, 점점 쌓여서 추적할 방법이 없어집니다. 논문의 답은 이렇습니다: 프로그래머에게 정리를 손수 작성하게 하지 말고, 「되돌리기」를 이펙트 자체의 구조적 속성으로 만들어라.

2단계: 핵심 아이디어——모든 변환에는 「역방향 열쇠」가 딸려 있다

어떻게 하면 「손수 정리를 작성하지 않아도」 될까요? 논문의 발상은 매우 소박합니다: 이펙트를 「변환 + 역변환」의 함수로 모델링한다.

순수 함수 형태의 이펙트는 이렇게 생겼습니다:

f : Γ × X → Γ × Y

읽기 어려워도 괜찮습니다. 사람의 말로 번역하면: 환경 Γ와 입력 X를 입력받아, 변경된 환경과 반환값 Y를 출력한다. 그리고 핵심 업그레이드는 이 행에 있습니다:

이펙트 : Γ → Γ × (Γ → Γ)

의미는 이렇습니다: 이펙트를 현재 환경에 적용하면 변경된 환경을 얻는 것 외에도, 추가로 명시적 역함수(Γ에서 Γ로의 함수)를 반환합니다. 이 역함수가 바로 「역방향 열쇠」입니다:

  • 설치할 때: 환경이 γ에서 f(γ)로 바뀝니다;
  • 제거할 때: 역함수 f⁻¹을 꺼내서, 환경이 f(γ)에서 γ로 되돌아갑니다.

🚿 비유(주방 편): 식기세척기를 설치한다고 합시다. 설치하면서 수도관을 바꾸고 전기 배선을 건드렸습니다——모든 변경을 하나하나 기록합니다: 「급수관을 여기에 연결했고, 배선을 저기에서 여기로 옮겼다」고. 나중에 식기세척기를 떼어낼 때 기억핟필요가 없습니다. 메모를 따라 한 단계씩 복원하면 주방이 원래대로 돌아갑니다.

🧳 비유(짐 편): 출장으로 호텔에 묵으면서 방을 자기 취향대로 꾸몄습니다(스탠드를 침대 머리맡으로 옮기고, 책을 책장에 올려놓기). 체크아웃할 때 모든 물건의 원래 위치를 기억할 필요가 없습니다——「복원 체크리스트」만 있으면 그대로 따라 제자리에 놓으면 됩니다. 역변환이 바로 이 체크리스트입니다.

역함수를 런타임에 반환하면 일거양득입니다:

  • 가역적: f⁻¹이 있으면 어떤 변경이든 되돌릴 수 있습니다;
  • 추적 가능: 런타임이 모든 f⁻¹을 수집하면 컴포넌트가 무엇을 변경했는지 알 수 있습니다.

논문은 이런 종류의 이펙트를 **되돌릴 수 있는 이펙트(revertible effect)**라고 부릅니다: 실행 중에 이러한 역함수를 기록하고 합성하면, 환경을 완전히 복원하는 것은 더 이상 프로그래머의 의무가 아니라 구조적 보장이 됩니다.

环境(初始)装上组件执行变换 f,记录逆 f⁻¹环境(被改)抽屉里存着 f⁻¹卸载组件应用 f⁻¹ → 环境还原装上时记录,拆下时还原

每个上下文变换都配一个显式逆变换——卸载 = 播放逆变换

그림은 세 부분으로 구성됩니다: 컴포넌트를 설치할 때 변환 f가 실행되고 서랍에 f⁻¹이 자동으로 저장됩니다; 언로드할 때 f⁻¹이 적용되어 환경이 복원됩니다——설치할 때 기록하고, 제거할 때 복원한다——그 사이에 손으로 작성한 정리 코드는 한 줄도 없습니다.

3단계: 이펙트 컨텍스트 ∂Γ = Γ × 𝔉Γ——「상태 + 장부」

역방향 열쇠는 생겼으니, 이것을 어디에 저장할까요? 논문은 새로운 것을 발명합니다: 이펙트 컨텍스트(effect context).

먼저 표기를 도입합니다: 𝔉Γ를 모든 「허용 가능한 이펙트」의 집합이라고 합시다(𝔉는 필기체 F). 이 이펙트들은 세 가지 공리를 만족합니다:

공리의미일상 버전
닫힘성두 허용 가능한 이펙트를 합성핟 여전히 허용 가능하다컴포넌트 두 개를 차례로 설치핟 순서대로 합성에 문제가 없다
항등원항등 변환 idΓ는 합성의 항등원이다「아무것도 바꾸지 않음」도 합법적인 연산이다
역원모든 이펙트 f에는 역 f⁻¹이 있어, 임의의 합성도 되돌릴 수 있다어떤 변경에도 역방향 열쇠가 마련되어 있다

이 세 가지를 합치면, 𝔉Γ는 합성 연산 ∘ 아래에서 **군(group)**을 이룹니다——「어떻게 설치하든 어떻게든 제거할 수 있음」이 수학적으로 보장됩니다. 한 가지 범위 제한에 주의하세요: Γ는 시스템이 완전히 통제하는 낸부 상태만 모델링합니다. 네트워크 요청이나 파일 I/O처럼 시스템 외부를 향하는 연산은 Γ를 변경하지 않으며, 𝔉Γ의 관점에서는 아무것도 하지 않은 것(idΓ)과 같고, 도메인 특화 정책(예: 보상 트랜잭션)이 별도로 처리합니다.

다음은 이 절에서 가장 중요한 정의입니다:

정의 1(이펙트 컨텍스트): 컨텍스트 Γ가 주어졌을 때, 이펙트 컨텍스트는 다음과 같이 정의됩니다. ∂Γ = Γ × 𝔉Γ

이는 순서쌍 (γ, φ)으로 이해할 수 있습니다:

  • γ ∈ Γ: 현재의 컨텍스트 상태;
  • φ ∈ 𝔉Γ: 컨텍스트를 초기 상태로 복원하는 변환(누적된 역변환 장부).

특히 초기 상태는 (γ₀, idΓ)입니다——상태는 최초의 γ₀이고, 장부는 아직 비어 있습니다(항등 변환).

🧳 짐 편: ∂Γ는 「여행 가방 + 복원 체크리스트」입니다. γ는 여행 가방의 현재 모습이고, φ는 체크리스트에 누적된 복원 단계입니다. 막 출발했을 때 여행 가방은 원래 모습 γ₀이고, 체크리스트는 텅 비어 있습니다(idΓ).

이 순서쌍이 있으면, 설치/제거는 두 개의 자동화된 연산——trackrecover——이 됩니다.

track: 설치할 때 자동으로 장부에 기록

정의 2(trackΓ): trackΓ = f ↦ (γ, φ) ↦ (f(γ), φ ∘ f⁻¹)

읽는 법: 일반 변환 f를 「실행하면서 역을 기록하는」 버전으로 바꿉니다. trackΓ(f)를 실행할 때:

  • 상태 부분: f를 적용해 γ가 f(γ)가 됩니다;
  • 장부 부분: f의 역 f⁻¹을 φ에 추가(∘로 합성)해 φ가 φ ∘ f⁻¹이 됩니다.

컴포넌트 설치 = 환경 변경 + 역변환이 자동으로 장부에 기록됨. 컴포넌트는 장부 기록에 대해 전혀 신경 쓸 필요가 없습니다.

논문은 두 가지 보장(정리 3, 정리 4)도 제시합니다. 직관적으로 이해하면 충분합니다:

  • 정리 3(추적은 동작을 바꾸지 않는다): pr1 ∘ trackΓ(f) = f ∘ pr1——track 버전은 「상태 성분」에서 원래 변환과 완전히 동일하게 동작하며, 장부가 추가되었지만 정상적인 사용에 영향을 주지 않습니다;
  • 정리 4(추적은 합성을 보존한다): trackΓ(f ∘ g) = trackΓ(f) ∘ trackΓ(g)——먼저 합성하고 추적하는 것은, 먼저 추적하고 합성하는 것과 같습니다. 즉 역변환의 장부 기록은 안심하고 쌓을 수 있습니다: 컴포넌트를 10개 설치하면 장부에 역변환 10개가 있고, 구조가 흐트러지지 않습니다.

recover: 제거할 때 자동으로 복원

정의 5(recoverΓ): recoverΓ = (γ, φ) ↦ (φ(γ), idΓ)

읽는 법: 장부 φ를 통째로 현재 상태 γ에 적용해 복원된 환경 φ(γ)를 얻습니다. 그다음 장부를 비워서 φ를 항등 변환 idΓ로 리셋합니다.

φ에는 역변환이 누적되어 있고, 새로운 역변환이 뒤에 배치되어 먼저 적용되므로, 복원은 정확히 나중에 설치한 것을 먼저 제거하는 역순 되돌리기가 됩니다——자동 재생되는 일련의 되돌리기 연산처럼, 환경을 한 걸음씩 최초의 γ₀로 되밀어 갑니다.

4단계: 설치하면 자동 기록, 제거하면 자동 복원

track과 recover를 이어 붙이면 완전한 「설치 → 제거」 사이클이 됩니다:

초기           (γ₀, idΓ)
f₁ 설치        →  (f₁(γ₀), idΓ ∘ f₁⁻¹)
f₂ 설치        →  (f₂(f₁(γ₀)), f₁⁻¹ ∘ f₂⁻¹)
f₃ 설치        →  (f₃(f₂(f₁(γ₀))), f₁⁻¹ ∘ f₂⁻¹ ∘ f₃⁻¹)
recover로 제거  →  (γ₀, idΓ)

마지막 행을 보세요: recover는 세 개의 역변환을 역순으로 차례로 적용하여, 환경이 정확히 γ₀로 돌아가고 장부가 비워집니다——사이클 전체가 닫힙니다.

🎮 일상 버전: 게임에서 캐릭터에 버프(힘, 속도, 실드) 세 개를 연속으로 추가한다고 상상해 보세요. 시스템은 버프를 추가하는 그 순간에 대응하는 「되돌리기 카드」를 자동으로 기록합니다. 버프를 잘못 추가했다고요? 「복원」을 누륩면, 시스템이 나중에 추가한 것을 먼저 사용하는 순서로 버프를 하나씩 해제하여 캐릭터가 버프 추가 전 상태로 돌아갑니다——카드 한 장도 빠뜨리지 않습니다——여러분은 「되돌리기 로직」을 한 줄도 직접 작성하지 않았습니다. 그것은 메커니즘 자체가 보장합니다.

왜 이것이 「구조적 보장」이지, 또 다른 「자동으로 정리해 주는」 마법이 아닐까요? 보장이 구조 자체에서 나오기 때문입니다:

  1. 𝔉Γ의 역원 공리: 모든 변환에는 역변환이 있으므로, 모든 변경이 되돌릴 수 있습니다;
  2. 정리 4의 합성 보존: 역변환을 임의로 합성핟 여전히 되돌릴 수 있으므로, 컴포넌트를 아무리 많이 설치핟 recover 한 번으로 전부 복원할 수 있습니다;
  3. track은 장부 기록을 이펙트 자체에 내장하므로, 프로그래머에게는 기록을 누락할 기회가 없습니다——기록 누락은 손수 작성하는 방식에만 있는 버그입니다.

다시 말해: 컴포넌트가 track을 통해 변환을 적용하고 언로드 시 recover로 마무리하기만 하면, 환경 복원은 수학적으로 보장이 성립하는 일이며, 프로그래머가 기억하느냐 못 하느냐와는 무관합니다.

💡 본줄거리로 돌아가기: 이것이 바로 DSH의 「설치한 것은 제거할 수 있고, 제거핟 흔적이 남지 않는다」는 이론적 출처입니다——에이전트가 런타임에 플러그인을 안전하게 마운트/언마운트할 수 있는 이유는, 기반의 모든 이펙트가 역변환을 자체적으로 갖추고 있어서, 언로드 = 역변환 재생이기 때문입니다.

핵심 포인트 복습

  1. 시간 합성 가능성 = 런타임에 컴포넌트를 로드/언로드할 수 있고, 언로드 시 공유 환경이 원래 상태로 복원됨. 모든 수정이 추적 가능하면서 가역적이어야 합니다.
  2. 되돌릴 수 있는 이펙트 = 이펙트를 「변환 + 역변환」으로 모델링: Γ → Γ × (Γ → Γ). 역함수를 런타임에 반환하면 되돌리기가 구조적 보장이 됩니다.
  3. 이펙트 컨텍스트 ∂Γ = Γ × 𝔉Γ: 순서쌍 (γ, φ)——γ는 현재 상태, φ는 누적된 복원 변환(역변환 장부)이며, 초깃값은 (γ₀, idΓ)입니다.
  4. trackΓ은 일반 변환을 「실행하면서 역을 기록하는」 버전으로 바꿉니다: (γ, φ) ↦ (f(γ), φ ∘ f⁻¹). recoverΓ은 φ를 통째로 적용한 후 idΓ로 리셋합니다: (γ, φ) ↦ (φ(γ), idΓ).
  5. 구조적 보장: 역원 공리가 되돌릴 수 있음을 보장하고, 정리 4가 임의 합성의 원클릭 되돌리기를 보장하며, track이 장부 기록을 이펙트에 내장합니다——그래서 손으로 작성하는 정리 로직이 필요 없습니다.

🚀 다음 강의 「7강: 이펙트 함수와 합성: 역변환의 자동 합성」에서는 역변환이 어떻게 자동으로 합성되는지 자세히 살펴흡니다——정리 4가 도대체 무엇을 보장하는지, 여러 이펙트가 겹쳐도 「원클릭 되돌리기」가 왜 여전히 성립하는지를요.

자가 테스트 · 되돌릴 수 있는 이펙트

답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.

1. 되돌릴 수 있는 이펙트에서 「역변환 f⁻¹」이란 무엇을 가리키나요?
2. 이펙트 컨텍스트 ∂Γ = Γ × 𝔉Γ의 순서쌍 (γ, φ)에서 φ는 무엇을 나타낸요?
3. recoverΓ가 하는 두 가지 일은 무엇인요?
4. 왜 「자동 추적」 덕분에 환경 복원이 구조적 보장이 되어 손으로 작성하는 정리 로직이 필요 없다고 말할 수 있나요?