람다 계산과 튜링 머신에서 출발한 프로그래밍 패러다임과 앨런 케이가 강조한 ‘진짜 OOP’의 핵심(메시지 전달, 캡슐화, 동적 바인딩)을 살피고, 클래스 상속 중심의 오해를 비판하며 자바스크립트 예시와 함께 메시지 지향 구성(MOP)을 제안한다.
참고: 이것은 JavaScript ES6+로 함수형 프로그래밍과 합성 소프트웨어 기법을 기초부터 배우는 “Composing Software” 시리즈(이제는 책!)의 일부입니다. 계속 지켜봐 주세요. 앞으로 더 많은 글이 이어집니다!
오늘날 우리가 사용하는 함수형 및 명령형 프로그래밍 패러다임은 1930년대 람다 계산과 튜링 머신으로 수학적으로 처음 탐구되었습니다. 이들은 보편 계산(일반적인 계산을 수행할 수 있는 형식화된 체계)의 서로 다른 정식화입니다. 처치-튜링 명제는 람다 계산과 튜링 머신이 기능적으로 동등하다는 것을 보여주었습니다. 즉, 튜링 머신으로 계산 가능한 것은 람다 계산으로도 계산 가능하고, 그 반대도 마찬가지입니다.
참고: 튜링 머신이 계산 가능한 것은 무엇이든 계산할 수 있다는 흔한 오해가 있습니다. 어떤 문제 부류(예: 정지 문제)는 일부 경우에 계산 가능하더라도, 튜링 머신으로 모든 경우에 일반적으로 계산 가능하지는 않습니다. 이 글에서 “계산 가능하다”라는 말은 “튜링 머신으로 계산 가능하다”라는 뜻입니다.
람다 계산은 계산에 대한 탑다운의 함수 적용 접근을, 튜링 머신의 티커 테이프/레지스터 머신 정식화는 바텀업의 명령형(단계별) 접근을 나타냅니다.
기계어와 어셈블리 같은 저수준 언어는 1940년대에 등장했고, 1950년대 말에는 최초의 대중적인 고수준 언어들이 나타났습니다. 리스프 계열은 오늘날에도 널리 사용되며, Clojure, Scheme, AutoLISP 등이 있습니다. FORTRAN과 COBOL은 모두 1950년대에 등장했으며, 오늘날에도 사용되는 명령형 고수준 언어의 예입니다. 다만 C 계열 언어들이 대부분의 용도에서 COBOL과 FORTRAN을 대체했습니다.
명령형 프로그래밍과 함수형 프로그래밍은 모두 계산 이론의 수학에 뿌리를 두고 있으며, 디지털 컴퓨터 이전에 등장했습니다. “객체지향 프로그래밍”(OOP)이라는 용어는 앨런 케이가 대학원 시절인 1966년경 또는 1967년에 만들었습니다.
아이반 서덜랜드(Ivan Sutherland)의 기념비적 애플리케이션인 Sketchpad는 OOP의 초기 영감이었습니다. 1961년에서 1962년 사이에 만들어졌고, 1963년에 그의 Sketchpad 박사 논문으로 출판되었습니다. 객체들은 오실로스코프 화면에 표시되는 그래픽 이미지를 나타내는 데이터 구조였고, 아이반 서덜랜드가 논문에서 “마스터”라고 부른 동적 위임을 통해 상속을 특징으로 했습니다. 어떤 객체든 “마스터”가 될 수 있었고, 그 객체의 추가 인스턴스는 “발생(occurrence)”라고 불렸습니다. Sketchpad의 마스터는 JavaScript의 프로토타입 상속과 많은 공통점을 공유합니다.
참고: MIT 링크컨 연구소의 TX-2는 라이트 펜을 사용해 화면과 직접 상호작용하는 그래픽 컴퓨터 모니터의 초기 사용 사례 중 하나였습니다. 1948–1958년에 가동된 EDSAC도 화면에 그래픽을 표시할 수 있었습니다. MIT의 Whirlwind는 1949년에 작동하는 오실로스코프 디스플레이를 갖추고 있었습니다. 프로젝트의 동기는 여러 항공기의 계기 피드백을 시뮬레이션할 수 있는 일반 비행 시뮬레이터를 만드는 것이었습니다. 이는 SAGE 컴퓨팅 시스템의 개발로 이어졌습니다. TX-2는 SAGE를 위한 시험 컴퓨터였습니다.
“객체지향”으로 널리 인정받는 최초의 프로그래밍 언어는 1965년에 명세된 Simula입니다. Sketchpad처럼 Simula도 객체를 특징으로 했고, 결국 클래스, 클래스 상속, 서브클래스, 가상 메서드를 도입했습니다.
참고: “가상 메서드”는 서브클래스에서 재정의를 염두에 두고 클래스에 정의된 메서드입니다. 가상 메서드는 동적 디스패치를 사용하여 런타임에 어떤 구체 메서드를 호출할지 결정함으로써, 코드가 컴파일될 당시 존재하지 않을 수도 있는 메서드를 호출할 수 있게 합니다. JavaScript는 동적 타입을 특징으로 하고 위임 체인을 사용해 호출할 메서드를 결정하기 때문에, 프로그래머에게 가상 메서드라는 개념을 노출할 필요가 없습니다. 달리 말해 JavaScript의 모든 메서드는 런타임 메서드 디스패치를 사용하므로, 이 기능을 지원하기 위해 메서드를 “virtual”로 선언할 필요가 없습니다.
“‘객체지향’이라는 용어는 내가 만든 말이지만, 분명한 건 C++를 염두에 둔 건 아니었다.” ~ Alan Kay, OOPSLA ‘97
앨런 케이는 1966년 또는 1967년 대학원에서 “객체지향 프로그래밍”이라는 용어를 만들었습니다. 핵심 아이디어는 직접적인 데이터 공유가 아니라 메시지 전달을 통해 통신하는, 캡슐화된 소형 미니 컴퓨터들을 소프트웨어로 사용하는 것이었습니다. 프로그램을 “데이터 구조”와 “프로시저”로 나누는 관행을 멈추자는 것이죠.
“재귀적 설계의 기본 원리는 부분들이 전체와 같은 힘을 갖게 만드는 것입니다.” ~ Bob Barton, Algol-60 실행에 최적화된 메인프레임 B5000의 주요 설계자
Smalltalk은 Xerox PARC에서 앨런 케이, Dan Ingalls, Adele Goldberg 등과 함께 개발되었습니다. Smalltalk는 Simula보다 더 객체지향적이었습니다. Smalltalk에서는 클래스, 정수, 블록(클로저)까지 모든 것이 객체입니다. 원래의 Smalltalk-72에는 서브클래싱이 없었습니다. 서브클래싱은 Dan Ingalls가 Smalltalk-76에 도입했습니다.
Smalltalk은 클래스와 결국 서브클래싱을 지원했지만, Smalltalk은 사물들을 클래스나 서브클래싱하는 것에 관한 언어가 아니었습니다. Smalltalk은 Simula뿐 아니라 Lisp에서 영감을 받은 함수형 언어였습니다. 앨런 케이는 업계가 서브클래싱에 초점을 맞추는 것을 객체지향 프로그래밍의 진정한 이점에서 주의를 돌리는 잡음으로 여깁니다.
“오래전에 이 주제를 위해 ‘객체’라는 용어를 만들어서 미안합니다. 그 때문에 많은 사람들이 덜 중요한 아이디어에 집중하게 되었거든요. 큰 아이디어는 메시징입니다.”
~ Alan Kay
2003년의 한 이메일 대화에서, 앨런 케이는 Smalltalk을 “객체지향적”이라고 부를 때 그가 무엇을 의미했는지 명확히 했습니다:
“나에게 OOP란 오직 메시징, 상태-프로세스의 로컬 보존·보호·은닉, 그리고 모든 것의 극단적 지연 바인딩을 의미합니다.”
~ Alan Kay
다시 말해, 앨런 케이에 따르면 OOP의 필수 재료는 다음과 같습니다:
주목할 점은, 상속과 서브클래스 다형성은 용어의 창시자이자 OOP를 대중화한 앨런 케이가 보기에 OOP의 필수 재료가 아니었다는 것입니다.
메시지 전달과 캡슐화의 조합은 중요한 목적을 수행합니다:
이러한 아이디어는 생물학적 세포와/또는 네트워크의 개별 컴퓨터에서 영감을 받았으며, 이는 앨런 케이의 생물학 배경과 Arpanet(인터넷의 초기 버전) 설계에서 받은 영향 때문입니다. 그때 이미 앨런 케이는 거대한 분산 컴퓨터(인터넷)에서 소프트웨어가 실행되는 모습을 상상했습니다. 개별 컴퓨터들이 생물학적 세포처럼, 자신들의 고립된 상태에서 독립적으로 동작하고 메시지 전달을 통해 통신하는 그림을요.
“나는 세포/전체-컴퓨터 은유가 데이터를 없앨 수 있으리라 깨달았습니다[…]”
~ Alan Kay
여기서 “데이터를 없앤다”는 말로 앨런 케이는 공유 변경 상태 문제와 공유 데이터가 야기하는 강한 결합을 충분히 인지하고 있었을 것입니다. 이는 오늘날에도 흔한 주제입니다.
하지만 1960년대 후반, ARPA 프로그래머들은 소프트웨어를 만들기 전에 프로그램을 위한 데이터 모델 표현을 선택해야 하는 필요에 좌절하고 있었습니다. 특정 데이터 구조에 지나치게 강하게 결합된 프로시저는 변화에 탄력적이지 못했습니다. 그들은 데이터에 대한 더 균질한 처리를 원했습니다.
“[…] OOP의 핵심은 객체 내부에 무엇이 있는지 걱정할 필요가 없게 만드는 것입니다. 서로 다른 머신과 서로 다른 언어로 만든 객체들이 서로 대화할 수 있어야 합니다 […]” ~ Alan Kay
객체는 데이터 구조 구현을 추상화하고 감출 수 있습니다. 객체의 내부 구현은 소프트웨어 시스템의 다른 부분을 망가뜨리지 않고도 변경될 수 있습니다. 사실, 극단적 지연 바인딩을 사용하면 전혀 다른 컴퓨터 시스템이 어떤 객체의 책임을 떠맡아도 소프트웨어가 계속 동작할 수 있습니다. 한편, 객체는 내부적으로 어떤 데이터 구조를 사용하든 동작하는 표준 인터페이스를 노출할 수 있습니다. 동일한 인터페이스가 연결 리스트, 트리, 스트림 등에 대해서도 똑같이 동작할 수 있습니다.
앨런 케이는 또한 객체를 대수적 구조로 보았습니다. 대수적 구조는 그 동작에 대해 특정한 수학적으로 증명 가능한 보장을 제공합니다:
“수학 배경 덕분에 나는 각 객체가 여러 개의 대수를 가질 수 있고, 그 대수들이 가족(family)을 이룰 수 있으며, 그것들이 매우 매우 유용하다는 것을 깨달았습니다.”
~ Alan Kay
이는 사실로 입증되었고, 범주론에서 영감을 받은 프라미스와 렌즈 같은 객체의 기반이 됩니다.
앨런 케이가 그린 객체의 대수적 특성은 객체에 형식적 검증, 결정적 동작, 향상된 테스트 용이성을 부여합니다. 대수는 본질적으로 몇 가지 방정식 형태의 규칙을 따르는 연산이기 때문입니다.
프로그래머의 언어로 말하면, 대수는 함수(연산)들로 이루어진 추상화와, 그 함수들이 통과해야 하는 단위 테스트로 집행되는 특정한 법칙(공리/방정식)들의 집합과 같습니다.
이러한 아이디어는 수십 년 동안 C 계열 OO 언어(C++, Java, C# 등)에서 잊혀졌지만, 최근 널리 사용되는 OO 언어의 새 버전들에서 다시 자리 잡기 시작했습니다.
말하자면, 프로그래밍 세계는 OO 언어의 맥락에서 함수형 프로그래밍과 합리적 사고의 이점을 재발견하고 있습니다.
JavaScript와 Smalltalk이 그랬듯, 대부분의 현대 OO 언어들은 점점 더 “멀티 패러다임 언어”가 되어가고 있습니다. 함수형 프로그래밍과 OOP 사이에서 선택할 필요가 없습니다. 각자의 역사적 본질을 보면, 두 개념은 호환될 뿐 아니라 상호 보완적입니다.
이렇게 공통점이 많기 때문에, 저는 “JavaScript는 OOP에 대한 세계의 오해에 맞선 Smalltalk의 복수”라고 말하곤 합니다. Smalltalk과 JavaScript는 모두 다음을 지원합니다:
앨런 케이에 따르면, OOP에 본질적인 것은 무엇일까요?
비본질적인 것은 무엇일까요?
new 키워드만약 Java나 C# 배경이라면 정적 타입과 다형성이 필수 재료라고 생각할지 모르지만, 앨런 케이는 대수적 형태의 일반적 동작을 선호했습니다. 예를 들어, Haskell에서:
fmap :: (a -> b) -> f a -> f b
이는 펑터의 map 서명으로, 지정되지 않은 타입 a와 b에 대해 일반적으로 동작하여, a에서 b로 가는 함수를 a의 펑터 문맥에서 적용해 b의 펑터를 만들어냅니다. 펑터는 본질적으로 “map 연산을 지원한다”는 뜻의 수학 용어입니다. JavaScript의 [].map()에 익숙하다면 이미 그 의미를 알고 있는 셈입니다.
이 작가의 업데이트를 받으려면 Medium에 무료로 가입하세요.
다음은 JavaScript의 두 가지 예시입니다:
// isEven = Number => Boolean
const isEven = n => n % 2 === 0;const nums = [1, 2, 3, 4, 5, 6];// map은 함수 a => b와 a들의 배열(여기서는 this)을 받아
// b들의 배열을 반환합니다.
// 이 경우, a는 Number, b는 Boolean입니다.
const results = nums.map(isEven);console.log(results);
// [false, true, false, true, false, true]
.map() 메서드는 a와 b가 어떤 타입이든 상관없이 일반적으로 동작한다는 점에서 제네릭합니다. 이는 배열이 대수적 functor 법칙을 구현하는 데이터 구조이기 때문입니다. .map()은 타입들을 직접 조작하려 하지 않고, 애플리케이션에 맞는 올바른 타입을 기대하고 반환하는 함수를 적용하므로 타입은 .map()에게 중요하지 않습니다.
// matches = a => Boolean
// 여기서 a는 어떤 비교 가능한 타입이어도 됩니다.
const matches = control => input => input === control;const strings = ['foo', 'bar', 'baz'];const results = strings.map(matches('bar'));console.log(results);
// [false, true, false]
이러한 제네릭한 타입 관계는 TypeScript 같은 언어에서 정확하고 철저하게 표현하기 어렵지만, 고차 종류 타입(Higher-Kinded Types)을 지원하는 Haskell의 Hindley–Milner 타입에서는 비교적 쉽게 표현되었습니다.
대부분의 타입 시스템은 함수 합성, 자유로운 객체 합성, 런타임 객체 확장, 콤비네이터, 렌즈 등 동적이고 함수형적인 아이디어의 자유로운 표현을 허락하기에는 지나치게 제한적이었습니다. 다시 말해, 정적 타입은 종종 합성 가능한 소프트웨어를 작성하는 일을 더 어렵게 만듭니다.
만약 여러분의 타입 시스템이 너무 제한적이라면(예: TypeScript, Java), 같은 목표를 이루기 위해 더 복잡한 코드를 작성해야 할 수도 있습니다. 그렇다고 정적 타입이 나쁜 아이디어라는 뜻은 아니며, 모든 정적 타입 구현이 똑같이 제한적이라는 뜻도 아닙니다. 저는 Haskell의 타입 시스템에서는 훨씬 적은 문제에 부딪혔습니다.
정적 타입의 팬이고 그 제약이 불편하지 않다면, 좋습니다! 하지만 이 글의 일부 조언을 따르기 어렵다면, 그 이유가 합성된 함수와 합성된 대수적 구조에 타입을 붙이기 어렵기 때문이라면, 아이디어를 탓하지 말고 타입 시스템을 탓하세요. 사람들은 SUV의 편안함을 사랑하지만, SUV가 날지 못한다고 불평하진 않습니다. 날려면 더 많은 자유도를 가진 탈것이 필요합니다.
제약이 코드를 단순하게 해준다면 훌륭합니다! 하지만 제약 때문에 더 복잡한 코드를 써야 한다면, 아마 그 제약이 잘못된 것일지도 모릅니다.
오랜 세월에 걸쳐 객체는 많은 함의를 떠안았습니다. JavaScript에서 우리가 “객체”라고 부르는 것은 단순히 합성 데이터 타입일 뿐이며, 클래스 기반 프로그래밍이나 앨런 케이의 메시지 전달에서 비롯된 함의를 필수적으로 담고 있지 않습니다.
JavaScript에서 그러한 객체들은 캡슐화, 메시지 전달, 메서드를 통한 행위 공유, 심지어 서브클래스 다형성(비록 타입 기반 디스패치가 아닌 위임 체인을 통해)이 가능하고 자주 그렇게 합니다. 어떤 함수든 어떤 프로퍼티에든 할당할 수 있습니다. 객체의 행위를 동적으로 구축할 수 있고, 런타임에 객체의 의미를 바꿀 수도 있습니다. JavaScript는 또한 구현 프라이버시를 위한 클로저를 사용한 캡슐화를 지원합니다. 하지만 이 모든 것은 옵트인(선택적) 행위입니다.
오늘날 우리가 생각하는 객체는 단지 합성 데이터 구조일 뿐이며, 객체로 간주되기 위해 그 이상을 요구하지 않습니다. 하지만 이러한 종류의 객체로 프로그래밍한다고 해서, 함수로 프로그래밍한다고 해서 코드가 “함수형”이 되는 것이 아닌 것처럼, 코드가 “객체지향적”이 되는 것은 아닙니다.
현대 프로그래밍 언어에서 “객체”가 앨런 케이가 의도한 것보다 훨씬 적은 의미를 갖게 되었기 때문에, 저는 진짜 OOP의 규칙을 설명하기 위해 “객체” 대신 “컴포넌트”라는 말을 사용하겠습니다. JavaScript에서는 많은 “객체”가 다른 코드에 의해 직접 소유되고 조작되지만, “컴포넌트”는 자신의 상태를 캡슐화하고 스스로 제어해야 합니다.
진짜 OOP란 다음을 의미합니다:
대부분의 컴포넌트 행위는 대수적 구조로 일반적으로 명세할 수 있습니다. 이때 상속은 필요하지 않습니다. 컴포넌트는 데이터를 공유하지 않고도 공유 함수와 모듈 임포트에서 행위를 재사용할 수 있습니다.
JavaScript에서 “객체”를 조작하거나 “클래스 상속”을 사용한다고 해서 “OOP를 하고 있다”는 뜻은 아닙니다. 위와 같은 방식의 컴포넌트를 사용하는 것이 진짜 OOP입니다. 하지만 대중적 사용이 결국 단어의 의미를 결정하기 때문에, OOP를 버리고 이것을 “객체지향 프로그래밍(OOP)”이 아니라 “메시지 지향 프로그래밍(MOP)”이라고 불러야 할지도 모르겠습니다.
걸레(mop)가 엉망진창을 치우는 데 쓰인다는 것이 우연일까요?
대부분의 현대 소프트웨어에는 사용자 상호작용을 관리하는 일부 UI, 애플리케이션 상태(사용자 데이터)를 관리하는 코드, 시스템 또는 네트워크 I/O를 관리하는 코드가 있습니다.
이 시스템들 각각은 이벤트 리스너 같은 장수 프로세스를 요구할 수 있고, 네트워크 연결, UI 요소 상태, 애플리케이션 상태 자체 같은 것을 추적하기 위한 상태를 필요로 할 수 있습니다.
좋은 MOP란 이 모든 시스템이 서로의 상태를 직접 끄집어내어 조작하는 대신, 메시지 디스패치를 통해 다른 컴포넌트와 소통하는 것을 의미합니다. 사용자가 저장 버튼을 클릭하면 "SAVE" 메시지가 디스패치되고, 애플리케이션 상태 컴포넌트가 이를 해석한 다음 상태 업데이트 핸들러(예: 순수 리듀서 함수)로 전달할 수 있습니다. 아마 상태가 업데이트된 뒤 상태 컴포넌트는 UI 컴포넌트로 "STATE_UPDATED" 메시지를 디스패치할 것이고, UI 컴포넌트는 상태를 해석하여 어떤 부분의 UI를 업데이트해야 하는지 정리하고, 해당 UI 부분을 다루는 서브컴포넌트들에게 업데이트된 상태를 전달할 것입니다.
한편, 네트워크 연결 컴포넌트는 네트워크상의 다른 머신에 대한 사용자의 연결을 모니터링하면서 메시지를 청취하고, 원격 머신에 데이터를 저장하기 위해 업데이트된 상태 표현을 디스패치할 수 있습니다. 내부적으로는 네트워크 하트비트 타이머, 현재 연결이 온라인인지 오프라인인지 등을 추적합니다.
이 시스템들은 시스템의 다른 부분에 대한 세부 정보를 알 필요가 없습니다. 각자의 개별적이며 모듈식 관심사만 알면 됩니다. 시스템 컴포넌트는 분해 가능하고 재조합 가능해야 합니다. 상호 운용이 가능하도록 표준화된 인터페이스를 구현합니다. 인터페이스만 만족하면, 같은 일을 다른 방식으로 수행하는 대체재나, 같은 메시지로 전혀 다른 일을 수행하는 대체재로 바꿔 끼울 수 있습니다. 심지어 런타임에도 그렇게 할 수 있고, 모든 것이 제대로 계속 작동해야 합니다.
같은 소프트웨어 시스템의 컴포넌트들이 반드시 같은 머신에 있을 필요도 없습니다. 시스템은 분산될 수 있습니다. 네트워크 스토리지는 데이터를 IPFS 같은 분산 스토리지 시스템에 샤딩해, 특정 머신의 상태에 의존하지 않고도 사용자의 데이터가 안전하게 백업되고, 그것을 훔치려는 해커들로부터 안전하도록 할 수 있습니다.
OOP는 부분적으로 아르파넷에서 영감을 받았고, 아르파넷의 목표 중 하나는 핵폭탄 같은 공격에도 탄력적인 분산 네트워크를 구축하는 것이었습니다. 아르파넷 개발 당시 DARPA의 디렉터였던 Stephen J. Lukasik에 따르면(“Why the Arpanet Was Built”):
“목표는 핵 위협에 맞선 군 지휘통제를 위해, 미국의 핵전력을 생존 가능하게 통제하고, 군의 전술적·관리적 의사결정을 개선하기 위해 새로운 컴퓨터 기술을 활용하는 것이었다.”
참고: 아르파넷의 주된 동인은 핵 위협이 아니라 편의성이었고, 명백한 방어상의 이점은 나중에 부각되었습니다. ARPA는 세 개의 별도 컴퓨터 터미널로 세 개의 별도 컴퓨터 연구 프로젝트와 소통하고 있었습니다. Bob Taylor는 각 프로젝트를 서로 연결하는 단일 컴퓨터 네트워크를 원했습니다.
좋은 MOP 시스템은 애플리케이션이 실행 중인 동안에도 컴포넌트를 핫스왑할 수 있게 하여, 인터넷의 견고함을 공유할 수 있습니다. 사용자가 휴대전화로 터널에 들어가 오프라인이 되더라도 계속 작동할 수 있습니다. 허리케인이 서버가 위치한 데이터 센터 중 하나의 전력을 끊더라도 계속 기능할 수 있습니다.
이제 소프트웨어 세계는 실패한 클래스 상속 실험을 놓아주고, 원래 OOP 정신을 정의했던 수학과 과학의 원칙을 받아들여야 합니다.
이제 우리는 MOP와 함수형 프로그래밍이 조화를 이루는, 더 유연하고, 더 탄력적이며, 더 잘 합성된 소프트웨어를 만들기 시작해야 합니다.
참고: MOP라는 약어는 이미 “monitoring-oriented programming(모니터링 지향 프로그래밍)”을 지칭하는 데 사용되고 있고, OOP가 조용히 사라질 가능성은 낮습니다.
MOP가 프로그래밍 은어로 자리 잡지 못하더라도 화내지 마세요.
여러분의 OOP 실수들을 MOP으로 닦아내세요.
EricElliottJS.com 회원을 위한 함수형 프로그래밍 비디오 강좌가 준비되어 있습니다. 아직 회원이 아니라면, 지금 가입하세요.