시스템 설계의 두 가지 추상화: 숨기거나 축소하기
TLA(Temporal Logic of Actions)+에 대해 이야기할 때, 저는 항상 '추상화'를 가장 중요한 학습 요소로 언급합니다. 그리고 이것은 배우기 가장 어려운 것이기도 합니다.
하지만 저에게 모순되는 부분이 계속 신경 쓰였습니다. 컴퓨터 과학자들은 이미 추상화에 능해야 하는 것 아닌가요? 추상화는 운영체제(OS), 네트워킹, 소프트웨어 공학의 근본적인 요소가 되어야 하지 않나요? 추상 자료형(ADTs)은 모든 CS 커리큘럼의 기본입니다. 그런데 왜 저(그리고 다른 모든 형식 방법론/모델링 전문가들)는 추상화에서 그렇게 큰 기술 격차를 발견하고, 이를 모델링을 위한 핵심이자 성패를 가르는 기술로 지적하는 걸까요?
저는 마침내 이 인지 부조화의 근원을 파악했다고 생각합니다. '추상화'라는 같은 우산 아래에 두 가지 종류가 혼동되어 있기 때문입니다.
모듈성 추상화(Modularity abstraction): 이것은 ADTs, API, 계층적 설계 등 CS 커리큘럼에서 가르치는 전통적인 추상화입니다. 이는 모두 캡슐화(encapsulation), 경계 설정, 내부 구현을 숨기는 것에 관한 것입니다.모델링 추상화(Modeling abstraction): 이것이 제가 모델링의 맥락에서 추상화를 이야기할 때 말하는 것입니다. 이는 수학자나 물리학자들이 생각하고 추론하기 위한 모델을 만들 때 사용하는 것과 같은 의미의 추상화입니다. 목표는 자신이 중요하게 여기는 속성을 보존하면서 최소하고 가장 우아한 설명을 찾는 것입니다. 이는 그 속성에 직교(orthogonal)하는 모든 것을 잘라내는 것에 관한 것입니다.
이 두 가지는 목표 면에서 더 이상 멀리 떨어져 있습니다! 다음 두 섹션에서 설명해 보겠습니다.
모듈성 추상화는 숨기고, 모델링 추상화는 축소한다.
모듈성 추상화는 내부 구현을 숨기는 인터페이스에 관한 것입니다. 모델링 추상화는 동작(behavior)에 관한 것이며, 중요하게 여기는 속성을 위해 시스템을 최소한의 행동적 골격으로 줄이는 것에 관한 것입니다.
모듈성 추상화와 모델링 추상화의 차이점
모듈성 추상화(Modularity abstraction)는 캡슐화하고, 수직 경계를 그리고, 아래 계층을 숨깁니다. 반면, 모델링 추상화(Modeling abstraction)는 가로지르는 성격입니다. 이는 시스템을 행위 평면(behavioral plane)을 따라 슬라이스하여 조사 중인 속성에 절대적으로 관련 있는 것만을 유지하며, 심지어 그것조차 "어떻게(how)"가 아닌 "무엇(what)"의 형태로만 남깁니다. 이 슬라이스는 보통 시스템의 조직 구조와는 전혀 다르게 보입니다.
모듈성 추상화는 동시성을 숨긴다. 모델링 추상화는 이를 노출한다.
모듈성 추상화는 누수를 막는 데 중점을 둡니다. 이것이 조엘 스폴스키(Joel Spolsky)가 "모든 추상화는 새어 나온다(all abstractions are leaky)"라고 탄식한 유명한 글의 배경입니다. [참고: 그의 목록은 모두 모듈성 추상화에 관한 것입니다: TCP (IP 숨기기), 문자열 라이브러리 (문자 배열 숨기기), 파일 시스템 (회전 디스크 숨기기), 가상 메모리/플랫 주소 공간 (MMU 및 페이징 숨기기), SQL (쿼리 계획 숨기기), NFS / SMB (네트워크 숨기기), C++ 문자열 클래스 (char* 숨기기).] 모듈성 추상화는 간섭(interleavings)을 숨기고 연산이 마치 원자적(atomic)인 것처럼 보이게 하는 것을 목표로 합니다. 그 목표는 모듈 사용을 쉽게 만드는 것이지만, 그렇게 함으로써 동시성이나 효율성의 기회를 노출하는 것을 포기합니다.
이에 극명하게 대비되는 모델링 추상화는 무엇이 새어 나와야 하는지 식별하고 이를 활용하는 것입니다! 이는 세밀한 동작(fine-grained actions)과 순서 지정(orderings)을 노출하며, 간섭에도 불구하고 불변성(invariants)이 유지됨을 증명합니다. 이 작업의 결과물은 시스템으로부터 최대의 안전한 동시성을 수확하는 것입니다.
모델링 추상화의 예시
분산 시스템 분야에는 모델링 추상화가 풍부하게 존재합니다. 거의 모든 프로토콜이 이런 방식으로 설계된 것처럼 느껴집니다.
- Lamport 논리 시계: 시스템 시간(wall-clock time)은 버리고 발생 순서(happens-before)만 유지
- 하이브리드 논리 시계: 시스템 시간과 인과 관계를 유지하고 나머지는 버림
- TrueTime: 시간을 유한 불확실성 간격으로 처리
- 합의(Consensus): 단일 결정에 동의하는 것. Lamport가 Paxos를 설계한 방식은 추상화의 교본입니다. 즉, 합의(Consensus)부터 투표(Voting), 그리고 최종 프로토콜까지 이어집니다.
- 선형성(Linearizability) (그리고 사실 모든 일관성 모델): 복제(replication), 캐싱(caching), 재시도(retries)는 버리고
- 로그는 데이터베이스 아이디어: 실현된 상태(materialized state)를 진실의 원천으로 보는 것은 버리고, 순서가 지정되고 추가 전용(append-only) 이벤트 시퀀스만 유지합니다.
- MapReduce/Spark: 오케스트레이션(orchestration), 병렬성(parallelism), 스케줄링(scheduling), 장애 허용성(fault tolerance)은 버립니다. 파티셔닝된 데이터에 대한 결정론적 변환의 방향 그래프(DAG)만 유지하고, 나머지 부분은 프레임워크가 이 골격으로부터 재구성하도록 합니다.
**아티팩트(artifact)**는 동시에 다음 역할을 수행할 수 있습니다.
정제해야 할 명세서 (spec to refine against) (모듈성)
그리고
추론의 뼈대 (skeleton to reason from) (모델링). 이는 단지 두 가지 역할이 하나의 아티팩트에서 공존한다는 의미일 뿐이며, 추상화 역할 자체는 여전히 구별됩니다.