Smalltalk
Smalltalk은 1970년대 Xerox PARC의 Learning Research Group에서 개발된 동적 타입의 객체 지향 프로그래밍 언어이자 통합 프로그래밍 환경이다. 앨런 케이가 제시한 객체와 메시지 중심의 계산 모델을 바탕으로 댄 잉걸스, 아델 골드버그, Ted Kaehle...
| 분류 | 범용 프로그래밍 언어, 객체 지향 프로그래밍 언어 |
|---|---|
| 패러다임 | 객체 지향 프로그래밍, 반영 프로그래밍, 명령형 프로그래밍 |
| 설계자 | 앨런 케이, 댄 잉걸스, 아델 골드버그 외 |
| 개발 기관 | Xerox PARC |
| 최초 구현 | 1972년 |
| 대표 계통 | Smalltalk-72, Smalltalk-76, Smalltalk-80 |
| 타입 체계 | 동적 타입, 강한 타입 검사 |
| 실행 방식 | 가상 머신, 바이트코드, 이미지 기반 실행 환경 |
| 주요 표준 | ANSI Smalltalk |
| 영향을 받은 언어 | Simula, Lisp, Logo |
| 영향을 준 언어 | Objective-C, Self, Ruby, Python, Java |
| 주요 구현체 | Squeak, Pharo, VisualWorks, GemStone/S, GNU Smalltalk |
| 파일 확장자 | .st |
Smalltalk은 1970년대 Xerox PARC의 Learning Research Group에서 개발된 동적 타입의 객체 지향 프로그래밍 언어이자 통합 프로그래밍 환경이다. 앨런 케이가 제시한 객체와 메시지 중심의 계산 모델을 바탕으로 댄 잉걸스, 아델 골드버그, Ted Kaehler, Diana Merry를 비롯한 연구자들이 초기 언어와 실행 환경을 설계하고 구현하였다. 최초의 실질적인 구현인 Smalltalk-72는 1972년에 만들어졌으며, 이후 Smalltalk-76과 Smalltalk-80을 거치면서 현대 Smalltalk 계열의 기본 구조가 확립되었다.[1]
Smalltalk에서는 정수, 문자열, 클래스, 코드 블록과 개발 도구를 포함한 거의 모든 구성 요소를 객체로 표현하며, 객체가 다른 객체에 메시지를 보내는 방식으로 계산을 수행한다. 연산자나 제어 구조처럼 보이는 표현도 객체에 대한 메시지 전송으로 구성되며, 클래스와 메서드는 실행 중인 시스템 안에서 조회하고 변경할 수 있다. 이러한 구조는 객체 지향 프로그래밍을 단순히 자료와 함수를 묶는 기법이 아니라, 독립된 객체들이 메시지를 주고받으며 시스템을 구성하는 계산 모델로 발전시켰다.[2]
Smalltalk은 소스 파일을 편집한 뒤 프로그램 전체를 다시 빌드하는 방식보다, 실행 중인 객체 공간을 직접 탐색하고 수정하는 라이브 프로그래밍 환경을 중심으로 사용된다. 전통적인 Smalltalk 시스템은 객체와 클래스, 실행 상태, 개발 도구를 하나의 이미지에 저장하며, 브라우저, 작업 공간, 검사기, 디버거와 같은 도구 역시 같은 객체 시스템 안에서 동작한다. 개발자는 실행 중 발생한 오류에서 디버거를 열고 메서드를 수정한 뒤, 프로그램을 종료하지 않고 계산을 이어갈 수 있다.
Smalltalk은 언어만을 가리키는 이름인 동시에 Smalltalk-72, Smalltalk-76, Smalltalk-80과 그 후속 구현들을 포함하는 언어 계열을 가리킨다. Smalltalk-80은 PARC 외부에 널리 공개된 계통의 기반이 되었으며, 이후 상용 환경인 VisualWorks와 GemStone/S, 공개 소프트웨어 환경인 Squeak, Pharo, GNU Smalltalk 등으로 분화하였다. Squeak은 Smalltalk-80에서 이어진 공개 구현체이고, Pharo는 Smalltalk 전통을 바탕으로 현대적인 개발과 연구를 지향하는 독립적인 언어·환경으로 발전하였다.[3][4][5]
Smalltalk은 개인용 컴퓨팅과 그래픽 사용자 인터페이스의 발전에도 중요한 역할을 했다. Xerox Alto에서 실행된 Smalltalk 환경은 겹치는 창, 팝업 메뉴, 마우스 입력, 그래픽 편집 도구와 통합된 프로그래밍 환경을 구현했으며, 언어와 사용자 인터페이스, 개발 도구를 하나의 동적인 시스템으로 결합하였다. 이러한 환경은 이후 개인용 컴퓨터의 그래픽 인터페이스와 통합 개발 환경에 영향을 주었다.[6]
현대 소프트웨어 산업에서 Smalltalk 계열의 사용 규모는 주류 범용 언어보다 작지만, 기존 기업 시스템의 운영, 객체 데이터베이스, 웹 애플리케이션, 교육, 프로그래밍 언어 연구와 라이브 프로그래밍 환경 개발에서 계속 사용되고 있다. Smalltalk이 확립한 메시지 전송, 동적 바인딩, 반영, 통합 브라우저와 디버거, 실행 중 코드 수정 방식은 Objective-C, Self, Ruby를 비롯한 여러 언어와 현대 개발 도구의 설계에 이어졌다.
역사
Smalltalk의 역사는 하나의 프로그래밍 언어가 점진적으로 개정된 과정만을 뜻하지 않는다. 객체와 메시지를 중심으로 한 계산 모델, 그래픽 사용자 인터페이스, 라이브 프로그래밍 환경, 개인용 컴퓨터에 대한 구상이 함께 발전한 과정이다. 1970년대 Xerox PARC의 Learning Research Group은 Smalltalk-72, Smalltalk-74, Smalltalk-76, Smalltalk-78과 Smalltalk-80을 차례로 만들었으며, 각 버전은 언어 구조뿐 아니라 가상 머신, 객체 저장 방식, 그래픽 시스템과 개발 도구를 함께 바꾸었다.[7]
사상적 배경
Smalltalk의 기원은 앨런 케이가 1960년대 후반부터 구상한 개인용 컴퓨팅과 객체 중심 계산 모델에 있다. 케이는 사용자가 단순히 완성된 프로그램을 실행하는 데 그치지 않고, 컴퓨터라는 매체 자체를 직접 이해하고 변경할 수 있어야 한다고 보았다. 그는 책보다 동적이고 상호작용이 가능한 개인용 컴퓨터인 Dynabook을 구상했으며, 어린이도 읽고 쓰고 그림을 그리듯 프로그램을 만들 수 있는 환경을 목표로 삼았다.
Smalltalk의 객체 개념에는 여러 선행 기술이 결합되었다. Simula에서는 객체와 클래스, 독립된 실행 주체로 시스템을 모델링하는 방식을 받아들였고, Lisp에서는 실행 중인 프로그램을 동적으로 구성하고 변경하는 성질을 받아들였다. Sketchpad의 그래픽 객체와 제약 기반 상호작용, Logo의 교육적 프로그래밍 방식, ARPANET의 독립된 컴퓨터가 메시지를 교환하는 구조도 Smalltalk의 설계에 영향을 주었다.
케이가 강조한 핵심은 객체 내부의 자료 구조보다 객체 사이의 메시지 교환이었다. 각 객체는 내부 상태를 감추고, 다른 객체가 보낸 메시지에 응답한다. 이러한 관점에서 객체는 단순한 자료와 함수의 묶음이 아니라, 외부와 정해진 방식으로 통신하는 독립된 계산 단위였다. 케이는 이 구상을 바탕으로 객체 지향 프로그래밍이라는 표현을 사용했으며, Smalltalk은 이를 언어와 실행 환경 전체에 일관되게 적용하려는 시도였다.[8]
1970년 케이는 새로 설립된 Xerox PARC에 합류하여 Learning Research Group을 이끌었다. 이 그룹에는 댄 잉걸스, 아델 골드버그, Ted Kaehler, Diana Merry, Scott Wallace 등이 참여했다. 케이는 언어와 개인용 컴퓨팅의 방향을 제시했고, 잉걸스는 초기 언어와 가상 머신의 주요 구현을 담당했다. 골드버그는 교육 연구와 문서화, 이후 Smalltalk-80의 외부 공개를 주도했다.
Smalltalk-72
1972년 만들어진 Smalltalk-72는 실제로 실행된 최초의 Smalltalk 시스템이다. 케이는 메시지 전송을 중심으로 한 언어의 핵심을 매우 작은 인터프리터로 구현할 수 있다고 주장했고, 잉걸스와 함께 초기 인터프리터를 완성하였다. 이 시스템은 1972년 가을부터 사용되기 시작했으며, 이후 Xerox Alto에서 실행되었다.[9]
Smalltalk-72는 현대 Smalltalk과 문법과 실행 방식이 상당히 달랐다. 메시지를 받은 객체가 입력 스트림에서 뒤따르는 토큰을 직접 읽고 해석할 수 있었으며, 언어의 문법이 객체의 동작에 따라 확장될 수 있었다. 이러한 방식은 매우 유연했지만, 프로그램의 의미를 정적으로 파악하거나 효율적으로 컴파일하기 어려웠다. 현대 Smalltalk에서 일반적인 클래스 기반 상속 체계도 아직 완성되지 않았다.
Smalltalk-72는 어린이를 대상으로 한 교육 실험과 그래픽 프로그램 제작에 사용되었다. 사용자는 화면에 그림을 그리고 객체를 움직이거나 간단한 애니메이션을 만들 수 있었으며, 명령을 실행한 결과를 즉시 확인할 수 있었다. 언어와 그래픽 환경은 별개의 프로그램이 아니라 하나의 상호작용 시스템으로 제공되었다.
이 시기에 Smalltalk은 비트맵 화면, 키보드와 마우스를 갖춘 Alto와 결합되었다. 당시 대부분의 컴퓨터 사용 환경이 텍스트 터미널과 일괄 처리에 의존했던 것과 달리, Smalltalk 사용자는 화면 위의 객체와 창을 직접 조작하며 프로그램을 만들 수 있었다. 이는 이후 Smalltalk이 언어뿐 아니라 그래픽 개발 환경으로 발전하는 기반이 되었다.
Smalltalk-74와 Smalltalk-76
Smalltalk-74는 Smalltalk-72의 언어 구조를 전면적으로 바꾸기보다 실행 환경과 그래픽 성능을 개선한 버전이었다. Ted Kaehler는 Alto의 제한된 주기억장치보다 많은 객체를 다룰 수 있도록 OOZE라는 객체 지향 가상 메모리 시스템을 개발했다. OOZE는 사용 중인 객체를 주기억장치에 두고 나머지 객체를 보조 저장 장치와 교환하여, 하나의 지속적인 객체 공간을 구성했다.
댄 잉걸스는 비트맵의 사각형 영역을 다른 위치로 빠르게 복사하고 조합하는 BitBLT를 개발했다. 이전에는 문자와 일반 그래픽이 서로 다른 복사 루틴을 사용했지만, BitBLT는 이를 하나의 그래픽 기본 연산으로 통합했다. 이 연산은 창 이동, 문자 표시, 스크롤과 애니메이션을 빠르게 처리하는 기반이 되었으며, 이후 다양한 그래픽 시스템에서 사용된 비트 블리팅 방식에 영향을 주었다.[10]
Smalltalk-76은 초기 Smalltalk과 현대 Smalltalk 사이의 가장 중요한 전환점이었다. 개발팀은 Smalltalk-72의 해석 중심 구조를 대신하여, 메서드를 바이트코드로 컴파일하고 가상 머신에서 실행하는 체계를 도입했다. 이로써 실행 성능과 예측 가능성이 개선되었고, 소스 코드의 문법도 더욱 규칙적인 형태로 정리되었다.
Smalltalk-76에서는 객체를 클래스로 분류하고 클래스 사이에 상속 관계를 구성하는 체계가 본격적으로 확립되었다. 클래스는 인스턴스의 상태와 메시지에 대한 응답 방법을 정의했으며, 하위 클래스는 상위 클래스의 동작을 물려받아 일부만 변경할 수 있었다. 단항 메시지, 이항 메시지와 키워드 메시지로 이어지는 현대 Smalltalk의 메시지 문법도 이 시기에 기본 형태를 갖추었다.
Smalltalk-76은 언어, 클래스 라이브러리, 가상 머신과 개발 도구가 서로를 구성하는 하나의 자기 기술적 시스템으로 발전했다. 시스템 브라우저에서 클래스를 찾고 메서드를 편집하며, 변경한 코드를 실행 중인 환경에 즉시 반영하는 개발 방식도 정착되었다. 이후 Smalltalk 계열에서 유지된 객체 모델과 프로그래밍 경험의 대부분은 Smalltalk-76에서 확립되었다.[11]
Smalltalk-78
Smalltalk-78은 Smalltalk-76을 Xerox NoteTaker로 이식한 버전이다. NoteTaker는 Intel 8086 계열 프로세서, 비트맵 화면과 휴대 가능한 본체를 갖춘 실험적 개인용 컴퓨터였다. Alto 전용 하드웨어와 마이크로코드에 크게 의존하던 Smalltalk 시스템을 다른 프로세서에서 실행하면서, 구현체의 이식성이 중요한 과제로 떠올랐다.
Smalltalk-78은 Smalltalk-76의 언어와 사용자 환경을 대체로 유지했지만, Alto의 저장 장치와 밀접하게 결합된 OOZE 가상 메모리를 제거했다. 또한 언어의 실행 환경을 특정 하드웨어에서 분리하고, 가상 머신과 객체 이미지 사이의 경계를 더욱 명확하게 정리했다.
이 작업은 Smalltalk을 Xerox 내부의 특정 컴퓨터에서만 실행되는 시스템이 아니라, 여러 종류의 하드웨어로 옮길 수 있는 언어 환경으로 만드는 과정이었다. Smalltalk-78에서 얻은 이식 경험은 이후 Smalltalk-80의 가상 이미지와 가상 머신 명세를 분리하는 설계로 이어졌다.[12]
Smalltalk-80
Smalltalk-80은 Smalltalk을 Xerox PARC 외부에 공개하고 여러 하드웨어에서 구현할 수 있도록 정리한 버전이다. 당시 Learning Research Group의 관리자가 된 아델 골드버그는 Smalltalk의 개념과 환경을 외부에 알리는 작업을 이끌었고, 댄 잉걸스는 공개 버전의 개발을 주도했다.
Smalltalk-80은 언어의 문법, 객체 모델, 핵심 클래스, 바이트코드와 가상 머신 인터페이스를 비교적 명확하게 규정했다. Xerox 하드웨어에서만 사용할 수 있었던 특수 문자는 ASCII로 표현할 수 있는 기호로 교체되었다. 시스템은 플랫폼과 독립적인 객체 이미지와 각 컴퓨터에 맞게 구현하는 가상 머신으로 나뉘었다. 같은 이미지를 서로 다른 기계에서 실행할 수 있다는 구조는 Smalltalk 계열의 높은 이식성을 가능하게 했다.
1980년 Xerox는 Smalltalk-80을 외부 기관에 제공하기 위한 작업에 착수했다. Apple, Digital Equipment Corporation, Hewlett-Packard와 Tektronix가 Smalltalk-80을 각자의 하드웨어에 이식하고 검토하는 협력 기관으로 선정되었다. 이 과정에서 Smalltalk은 Xerox 내부 연구 시스템을 넘어 외부 개발자와 기업이 사용할 수 있는 프로그래밍 환경으로 확장되었다.[13]
1981년 8월에는 《BYTE》 잡지가 Smalltalk 특집호를 발행했다. 이 특집에는 아델 골드버그, 댄 잉걸스, Ted Kaehler와 Larry Tesler 등이 Smalltalk-80의 언어 설계, 실행 환경, 그래픽 시스템과 개발 방식을 설명한 글을 실었다. 이를 통해 객체 지향 프로그래밍과 통합 그래픽 개발 환경이라는 Smalltalk의 개념이 컴퓨터 산업과 프로그래밍 언어 연구자들에게 널리 알려졌다.[14]
Smalltalk-80의 언어와 시스템은 여러 권의 공식 서적으로 정리되었다. 아델 골드버그와 David Robson의 《Smalltalk-80: The Language and Its Implementation》은 객체 모델, 언어 문법, 핵심 클래스와 가상 머신을 설명했으며, 표지 색상에서 유래해 Blue Book이라고도 불렸다. 이와 함께 프로그래밍 환경을 다룬 Orange Book, 구현 경험과 역사를 다룬 Green Book 등이 출판되었다.[15]
Smalltalk-80은 이후 등장한 대부분의 Smalltalk 구현체가 공통 기반으로 삼은 계통이 되었다. 오늘날 별도의 수식어 없이 Smalltalk이라고 할 때는 일반적으로 Smalltalk-80에서 확립된 문법과 객체 모델을 따르는 언어 계열을 의미한다.
상용화와 표준화
1980년대에는 Smalltalk-80을 기반으로 한 상용 구현체가 등장했다. Xerox에서 분리된 ParcPlace Systems는 Smalltalk-80과 ObjectWorks를 개발했으며, 이 계통은 이후 VisualWorks로 이어졌다. Digitalk는 개인용 컴퓨터를 대상으로 Smalltalk/V를 공급하여 DOS, Windows와 OS/2 환경에서 Smalltalk을 사용할 수 있게 했다.
ParcPlace 계열은 주로 UNIX 워크스테이션과 대규모 기업 개발 환경을 대상으로 했고, Digitalk 계열은 Intel 기반 개인용 컴퓨터 시장에 집중했다. 이 밖에도 IBM의 VisualAge for Smalltalk, Enfin, GemStone/S, Dolphin Smalltalk와 여러 Smalltalk 구현체가 개발되었다. Smalltalk은 금융, 보험, 물류와 통신 분야의 대규모 업무 시스템과 객체 데이터베이스를 구축하는 데 사용되었다.
Smalltalk 환경은 실행 중인 프로그램을 직접 수정하고 빠르게 기능을 추가할 수 있었기 때문에, 복잡한 사용자 인터페이스와 변화가 잦은 업무 규칙을 다루는 프로젝트에서 강점을 보였다. 1980년대 후반부터 1990년대 중반까지 Smalltalk은 상용 객체 지향 개발을 대표하는 언어 가운데 하나로 성장했다.
구현체마다 클래스 라이브러리와 부가 기능이 달라지면서 공통 언어 규격의 필요성도 커졌다. 이에 여러 Smalltalk 개발사와 사용자가 표준화 작업에 참여했고, 1998년 ANSI INCITS 319-1998이 승인되었다. 이 표준은 Smalltalk 언어의 핵심 문법과 의미, 공통 프로토콜을 정의했지만, 그래픽 사용자 인터페이스와 개발 환경 전체를 하나의 방식으로 통일하지는 않았다.[16]
상용 Smalltalk 시장은 1990년대 후반부터 Java와 C++ 기반 개발 환경이 확산되면서 이전보다 축소되었다. 그러나 VisualWorks, GemStone/S, VA Smalltalk와 ObjectStudio 같은 구현체는 기존 기업 시스템의 유지·확장과 새로운 응용 프로그램 개발에 계속 사용되었다.
Squeak과 공개 소프트웨어 계통
1990년대 중반 앨런 케이와 댄 잉걸스를 비롯한 개발자들은 Apple에서 교육과 멀티미디어 실험에 사용할 수 있는 새로운 Smalltalk 환경을 만들기 시작했다. 이들은 Apple Smalltalk에서 이어진 Smalltalk-80 계통의 이미지를 바탕으로, 다양한 플랫폼에 쉽게 이식할 수 있는 Squeak을 개발했다.
Squeak의 가상 머신은 Smalltalk로 작성한 뒤 이를 C 코드로 변환하는 방식으로 구성되었다. 개발자는 Smalltalk 환경 안에서 가상 머신의 구현을 읽고 수정하고 시험할 수 있었으며, 변환된 C 코드를 사용해 새로운 운영체제와 하드웨어로 이식할 수 있었다. 이는 언어와 실행기를 같은 환경에서 개발한다는 Smalltalk의 자기 기술적 성격을 확장한 것이었다.[17]
Squeak은 컬러 그래픽, 소리와 멀티미디어 기능을 강화했고, Self에서 시작된 Morphic 사용자 인터페이스 체계를 받아들였다. 또한 어린이를 위한 시각적 교육 환경인 Etoys의 기반이 되었다. 개발은 1995년에 시작되었고, 1996년 공개 소프트웨어로 배포되면서 Smalltalk-80 계통의 언어와 개발 환경을 개인과 연구기관이 자유롭게 실험할 수 있는 기반을 제공했다.[18]
Squeak에서는 여러 후속 프로젝트가 파생되었다. Cuis는 Squeak의 기본 구조를 간결하게 정리하는 방향으로 발전했고, Croquet과 Open Cobalt는 협업형 3차원 환경을 연구하는 데 사용되었다. Scratch의 초기 버전도 Squeak을 기반으로 개발되었으며, Smalltalk의 라이브 객체 환경과 교육용 프로그래밍 전통을 시각적 블록 프로그래밍으로 이어갔다.
현대 Smalltalk 계열
2008년에는 Squeak에서 분리된 Pharo가 시작되었다. Pharo는 교육용 멀티미디어 환경까지 포함하던 Squeak 이미지에서 오래된 구성 요소를 정리하고, 소프트웨어 개발과 연구에 적합한 작고 현대적인 Smalltalk 환경을 만드는 방향으로 발전했다. 프로젝트는 MIT 라이선스를 중심으로 공개되었으며, 기업과 대학, 공개 소프트웨어 공동체가 함께 개발하고 있다.[19]
Pharo는 Smalltalk-80의 객체와 메시지 중심 문법, 이미지 기반 실행 환경과 라이브 프로그래밍 방식을 유지하면서도 패키지 관리, 테스트, 코드 분석, Git 연동, 네트워크와 서버 개발 기능을 지속적으로 현대화했다. 가상 머신 계통도 인터프리터 중심 구조에서 Cog와 Spur 같은 JIT 컴파일 및 객체 메모리 기술을 사용하는 방향으로 발전했다.
Squeak과 Pharo 외에도 여러 계통이 계속 유지되고 있다. VisualWorks와 VA Smalltalk는 기업용 개발 환경으로 사용되며, GemStone/S는 다수 사용자가 공유하는 지속 객체 공간과 객체 데이터베이스 기능을 제공한다. Dolphin Smalltalk는 Windows 환경과의 통합을 중심으로 발전했고, GNU Smalltalk는 전통적인 명령줄과 소스 파일 중심의 사용 방식을 제공했다.
현대 Smalltalk 공동체는 단일한 구현체로 통합되지 않고 서로 다른 목적을 가진 여러 방언과 실행 환경으로 구성되어 있다. 구현체별 클래스 라이브러리와 개발 도구에는 차이가 있지만, 객체에 메시지를 보내 계산하고 실행 중인 시스템을 직접 탐색·수정한다는 Smalltalk의 핵심 모델은 유지되고 있다. Smalltalk은 주류 산업 언어에서 차지하는 비중이 과거보다 작아졌지만, 라이브 프로그래밍, 가상 머신, 객체 데이터베이스, 소프트웨어 분석, 교육과 프로그래밍 환경 연구에서 지속적으로 발전하고 있다.
언어 설계와 객체 모델
Smalltalk의 언어 설계는 객체가 메시지를 주고받아 계산한다는 원칙을 중심으로 구성된다. 객체는 내부 상태와 동작을 가지며, 다른 객체는 그 내부 구현을 직접 지정하는 대신 수행할 작업을 메시지로 요청한다. 메시지를 받은 객체는 자신의 클래스와 상속 계층에서 메시지에 대응하는 메서드를 찾아 실행한다.
Smalltalk에서 프로그램은 객체 바깥에 존재하는 절차가 객체를 조작하는 구조보다, 각 객체가 자신에게 주어진 책임을 수행하고 필요한 작업을 다른 객체에 다시 요청하는 구조로 작성된다. 이 설계는 자료와 절차를 하나의 단위로 묶는 것에 그치지 않고, 시스템 전체를 협력하는 객체들의 네트워크로 표현한다.
정수, 문자열, 배열, 코드 블록과 클래스는 모두 객체이다. 조건 분기와 반복도 대부분 특별한 언어 명령이 아니라 Boolean 객체와 코드 블록 사이의 메시지 교환으로 표현된다. 다만 변수 선언, 변수 접근, 대입, 메서드 반환처럼 실행 환경이 직접 처리하는 일부 언어 요소는 메시지 전송이 아니다.[20]
객체와 메시지
객체는 Smalltalk 프로그램을 구성하는 기본 실행 단위이다. 각 객체는 고유한 정체성, 내부 상태와 메시지에 응답하는 동작을 가진다. 두 객체가 같은 값을 나타내더라도 서로 다른 인스턴스라면 객체 정체성은 구분될 수 있으며, 객체의 상태는 인스턴스 변수에 다른 객체를 참조하는 방식으로 표현된다.
객체에 작업을 요청하는 행위를 메시지 전송이라고 한다. 메시지는 메시지를 받는 수신자, 요청할 동작을 나타내는 선택자, 그리고 필요한 경우 전달되는 인자로 구성된다.
account deposit: 100
위 표현에서 account는 수신자이고, deposit:은 선택자이며, 100은 인자이다. 메시지 자체가 실행할 메서드를 직접 지정하지는 않는다. 실제로 어떤 메서드가 실행되는지는 메시지를 받은 객체의 클래스와 상속 계층에 따라 실행 중에 결정된다.
메시지를 보내는 과정은 개념적으로 다음 두 단계로 나뉜다.
- 수신자의 클래스에서 선택자와 같은 이름을 가진 메서드를 찾는다.
- 찾은 메서드를 수신자와 인자를 대상으로 실행한다.
메서드를 실행할 때 self는 메시지를 받은 원래 수신자를 가리킨다. 메서드가 상위 클래스에서 발견되더라도 self가 상위 클래스나 메서드를 정의한 객체로 바뀌지는 않는다.[21]
Smalltalk에서는 메시지를 보내는 측이 수신자의 구체적인 클래스나 메서드 구현을 알 필요가 없다. 수신자가 해당 메시지를 이해하고 약속된 동작을 수행할 수 있으면 된다. 이 구조는 객체의 내부 표현과 호출 측의 코드를 분리하며, 서로 다른 클래스의 객체를 동일한 메시지 형식으로 사용할 수 있게 한다.
예를 들어 여러 종류의 도형 객체가 area 메시지에 응답한다면 호출 측은 각 객체의 구체적인 클래스를 검사하지 않고 같은 메시지를 보낼 수 있다.
shapes collect: [ :shape | shape area ]
각 객체는 자신의 클래스에 정의된 방식으로 넓이를 계산한다. 원은 원의 메서드를 실행하고 사각형은 사각형의 메서드를 실행하지만, 메시지를 보내는 코드는 동일하게 유지된다.
메시지 전송은 단순한 함수 호출과 비슷하게 보일 수 있지만, Smalltalk의 설계에서는 객체 사이의 결합 방식을 결정하는 중심 개념이다. 발신자는 수행할 작업의 이름만 제시하고, 실제 구현의 선택은 수신자와 그 클래스가 담당한다.
클래스와 인스턴스
Smalltalk-80 계열에서 모든 일반 객체는 어떤 클래스의 인스턴스이다. 클래스는 인스턴스가 저장할 상태의 구조와 이해할 메시지의 구현을 정의한다. 객체의 클래스는 그 객체가 어떤 종류의 상태를 가질 수 있고, 특정 메시지를 받았을 때 어떤 메서드를 실행하는지를 결정한다.
클래스는 주로 다음 정보를 가진다.
- 클래스의 이름
- 상위 클래스
- 인스턴스 변수의 정의
- 클래스 변수와 공유 변수
- 선택자와 컴파일된 메서드의 대응 관계
- 클래스가 속한 패키지 또는 시스템 분류
예를 들어 BankAccount 클래스는 계좌 객체가 잔액을 저장하도록 balance 인스턴스 변수를 선언하고, 입금과 출금을 처리하는 메서드를 정의할 수 있다.
Object subclass: #BankAccount
instanceVariableNames: 'balance'
classVariableNames: ''
package: 'Banking'
클래스로부터 생성된 각 인스턴스는 같은 메서드 정의를 공유하지만 인스턴스 변수의 값은 각각 따로 가진다. 따라서 여러 BankAccount 객체는 동일한 deposit: 메서드를 사용하면서 서로 다른 잔액을 유지할 수 있다.
firstAccount := BankAccount new.
secondAccount := BankAccount new.
firstAccount deposit: 100.
secondAccount deposit: 250.
메서드는 객체 안에 개별적으로 복제되어 저장되는 것이 아니라 일반적으로 클래스의 메서드 사전에 저장된다. 인스턴스가 메시지를 받으면 가상 머신은 그 인스턴스의 클래스를 통해 메서드를 찾는다. 반면 인스턴스 변수의 실제 값은 각 인스턴스에 속한다.
Smalltalk의 클래스는 단순한 소스 코드 선언이나 컴파일러용 형식 정보가 아니다. 클래스 자체도 실행 중인 시스템 안에 존재하는 객체이며, 메시지를 받고 상태를 조회하거나 변경할 수 있다. 개발 도구는 별도의 외부 파일 해석기만으로 클래스를 다루는 것이 아니라, 실행 중인 클래스 객체에 질문하고 메시지를 보내 시스템을 탐색한다.
상속과 메서드 탐색
Smalltalk의 클래스는 일반적으로 하나의 상위 클래스를 가지는 단일 상속 계층을 구성한다. 하위 클래스는 상위 클래스가 정의한 인스턴스 변수 구조와 메서드를 물려받으며, 필요한 메서드를 새로 추가하거나 같은 선택자의 메서드를 다시 정의할 수 있다.
객체가 메시지를 받으면 메서드 탐색은 수신자의 클래스에서 시작한다. 해당 클래스의 메서드 사전에 선택자가 없으면 상위 클래스로 이동하며, 메서드를 찾거나 상속 계층의 끝에 도달할 때까지 이 과정을 반복한다.
SavingsAccount >> withdraw: anAmount
balance := balance - anAmount
SavingsAccount 인스턴스가 withdraw: 메시지를 받으면 먼저 SavingsAccount에서 메서드를 찾는다. 이 클래스에 메서드가 없다면 BankAccount와 그 위의 상위 클래스를 차례로 탐색한다.
하위 클래스가 상위 클래스와 같은 선택자의 메서드를 정의하는 것을 재정의라고 한다. 이때 메시지를 받은 객체의 실제 클래스에 더 가까운 메서드가 선택된다. 따라서 같은 메시지에 대한 동작을 객체의 종류에 따라 바꾸는 다형성을 구성할 수 있다.
Account >> monthlyFee
^ 0
PremiumAccount >> monthlyFee
^ 10
Account 인스턴스와 PremiumAccount 인스턴스에 같은 monthlyFee 메시지를 보내더라도 서로 다른 결과를 반환할 수 있다.
메서드 안에서 super를 사용하면 현재 메서드를 정의한 클래스의 상위 클래스부터 같은 선택자의 메서드를 탐색한다.
PremiumAccount >> initialize
super initialize.
rewardPoints := 0
super는 별도의 상위 클래스 객체를 수신자로 사용하는 표현이 아니다. 수신자는 계속 self가 가리키는 원래 객체이며, 메서드 탐색을 시작하는 위치만 상위 클래스로 이동한다. 따라서 상위 클래스의 메서드 안에서도 인스턴스 변수 접근과 self 메시지 전송은 원래 수신자를 대상으로 수행된다.
상속은 코드 재사용에 사용될 수 있지만, Smalltalk에서는 객체가 제공하는 메시지 집합과 역할을 세분화하는 수단이기도 하다. 하위 클래스는 상위 클래스가 약속한 기본 프로토콜을 유지하면서 일부 동작을 구체화한다.
메서드 탐색을 끝까지 수행해도 선택자와 일치하는 메서드를 찾지 못하면, 시스템은 일반적으로 수신자에게 doesNotUnderstand: 메시지를 보낸다. 이 동작은 오류를 보고하는 데 사용되지만, 프록시, 메시지 전달과 동적 인터페이스 같은 고급 기능을 구현하는 데에도 활용할 수 있다.
메타클래스
Smalltalk에서는 클래스 자체도 객체이므로 클래스 역시 어떤 클래스의 인스턴스여야 한다. 일반 객체의 동작을 클래스가 정의하듯이, 클래스 객체의 동작은 메타클래스가 정의한다.
예를 들어 다음 메시지에서 BankAccount는 클래스를 나타내는 이름인 동시에 메시지를 받는 객체이다.
BankAccount new
new 메시지는 BankAccount의 인스턴스에 보내는 메시지가 아니라 BankAccount 클래스 객체에 보내는 메시지다. 따라서 new와 같이 클래스가 직접 이해하는 메시지는 일반 인스턴스 쪽 메서드와 다른 위치에 정의되어야 한다.
전통적인 Smalltalk-80 객체 모델에서 각 클래스에는 대응하는 익명의 메타클래스가 존재한다. BankAccount 클래스 객체는 BankAccount class라고 표현되는 메타클래스의 인스턴스이다.
BankAccount class
일반적으로 다음 관계가 성립한다.
BankAccount의 일반 객체는BankAccount의 인스턴스이다.BankAccount클래스 객체는BankAccount class의 인스턴스이다.BankAccount의 인스턴스 메서드는BankAccount에 저장된다.BankAccount가 직접 이해하는 클래스 쪽 메서드는BankAccount class에 저장된다.
BankAccount class >> defaultCurrency
^ #KRW
위 메서드는 일반 계좌 객체가 아니라 BankAccount 클래스 객체에 보내는 메시지를 처리한다.
BankAccount defaultCurrency
메타클래스의 상속 관계는 대응하는 일반 클래스의 상속 관계와 연결된다. SavingsAccount가 BankAccount를 상속한다면, SavingsAccount class도 클래스 쪽 메서드 탐색에서 BankAccount class의 동작을 물려받을 수 있다.
메타클래스 구조를 통해 Smalltalk은 인스턴스 생성, 클래스 초기화, 클래스별 설정과 팩터리 메서드를 특별한 정적 함수 체계로 분리하지 않고 일반적인 객체와 메시지 모델 안에서 표현한다. 클래스 쪽 메시지 역시 일반 메시지와 같은 동적 탐색 규칙을 따른다.[22]
Smalltalk 구현체에 따라 최상위 메타클래스 계층의 구체적인 이름과 구조에는 차이가 있다. 그러나 클래스가 실행 중인 객체이고, 클래스의 동작도 메시지 전송과 메서드 탐색으로 처리된다는 원칙은 Smalltalk-80 계열 전반에서 유지된다.
동적 바인딩
Smalltalk의 메시지 전송은 동적 바인딩을 사용한다. 소스 코드에 메시지 선택자가 작성되어 있어도 어떤 메서드가 실행될지는 일반적으로 컴파일 시점에 하나로 고정되지 않는다. 실행 시점의 수신자와 그 클래스가 실제 메서드 선택을 결정한다.
anObject printString
위 표현만으로는 printString의 구체적인 구현을 확정할 수 없다. anObject가 정수라면 정수 클래스 계통의 메서드가 사용되고, 날짜나 사용자 정의 객체라면 그 객체의 클래스 계통에서 발견된 메서드가 사용된다.
이러한 구조에서는 같은 메시지를 이해하는 객체라면 공통 상위 클래스를 명시적으로 검사하지 않고도 같은 위치에서 사용할 수 있다.
items do: [ :each |
transcript show: each printString ]
컬렉션 안의 각 객체가 printString 메시지를 이해하면 서로 다른 클래스의 객체가 섞여 있어도 같은 코드로 처리할 수 있다.
동적 바인딩은 하위 클래스뿐 아니라 실행 중 추가되거나 수정된 메서드에도 적용된다. 클래스에 메서드를 새로 컴파일하거나 기존 메서드를 교체하면 이후 메시지 전송은 변경된 메서드를 사용할 수 있다. 전체 프로그램을 종료하고 다시 연결하지 않아도 시스템의 동작을 바꿀 수 있는 것은 Smalltalk의 라이브 프로그래밍 환경과 밀접하게 연결된다.
이 방식은 발신자와 구체적인 구현 사이의 결합을 줄인다. 발신자는 수신자가 어떤 클래스인지보다 어떤 메시지를 이해하는지에 의존한다. 그 결과 객체 교체, 하위 클래스 확장, 테스트 대역과 프록시 구성 같은 작업을 메시지 프로토콜을 유지하는 방식으로 수행할 수 있다.
동적 바인딩은 컴파일러와 가상 머신이 실행 성능을 높이기 위해 내부적으로 최적화할 수 있다. 인라인 캐시나 JIT 컴파일을 사용하는 구현체는 특정 호출 지점에서 관찰된 수신자 클래스를 기반으로 빠른 경로를 만들 수 있지만, 언어 수준의 의미는 수신자의 실제 클래스에 따라 메서드를 선택하는 동적 전송으로 유지된다.
반영과 메타프로그래밍
Smalltalk은 실행 중인 프로그램이 자신의 구조와 상태를 조회하고 변경할 수 있는 반영 기능을 객체 모델 안에 포함한다. 클래스, 메서드, 실행 문맥과 개발 도구가 객체로 표현되므로 일반적인 메시지 전송을 통해 프로그램 자체를 조사할 수 있다.
객체는 자신의 클래스를 반환할 수 있고, 클래스는 상위 클래스, 하위 클래스, 인스턴스 변수와 메서드 사전을 제공할 수 있다.
anObject class
BankAccount superclass
BankAccount methodDictionary
메서드 사전에서는 선택자와 컴파일된 메서드 사이의 대응 관계를 조사할 수 있다. 구현체가 제공하는 API를 사용하면 새로운 메서드를 실행 중에 컴파일하여 클래스에 추가하거나 기존 메서드를 교체할 수도 있다.
BankAccount
compile: 'balance ^ balance'
classified: 'accessing'
시스템 브라우저, 객체 검사기와 디버거는 이러한 반영 기능 위에서 동작한다. 브라우저는 실행 중인 클래스와 메서드 객체를 조회하고, 검사기는 객체의 인스턴스 변수가 참조하는 객체를 탐색한다. 디버거는 현재 실행 스택과 메서드 문맥을 객체로 표시하고, 메서드를 수정한 뒤 중단된 계산을 다시 진행할 수 있다.
Smalltalk의 반영은 프로그램의 구조를 읽는 내성에 그치지 않는다. 클래스 정의와 메서드, 실행 중인 객체의 상태를 변경하는 개입도 가능하다. 이 특성은 다음과 같은 기능의 기반이 된다.
- 실행 중인 클래스와 메서드 탐색
- 코드 자동 생성과 변환
- 객체 직렬화와 지속성
- 테스트 대역과 프록시
- 객체 관계 시각화
- 디버거와 코드 분석 도구
- 개발 환경 자체의 확장
- 실행 중인 시스템의 점진적 갱신
반영과 메타프로그래밍은 별도의 전처리 언어나 외부 도구에만 의존하지 않는다. Smalltalk 개발 도구 자체가 Smalltalk 객체와 메서드로 구현되기 때문에, 개발자는 응용 프로그램을 수정하는 것과 같은 방식으로 브라우저, 검사기와 디버거를 변경할 수 있다.
이러한 구조는 Smalltalk 시스템을 언어 실행기와 개발 도구가 분리된 고정된 제품이라기보다, 실행 중에 자신을 관찰하고 확장할 수 있는 하나의 객체 세계로 만든다. 다만 실제로 노출되는 반영 API와 실행 문맥의 표현, 클래스 변경 방식은 Smalltalk 구현체마다 차이가 있다.[23]
문법과 실행 의미
Smalltalk의 문법은 객체에 메시지를 보내는 표현을 중심으로 구성된다. 언어가 직접 제공하는 핵심 구문은 리터럴 객체의 생성, 변수 참조와 대입, 메시지 전송, 메서드 반환, 코드 블록 정의와 문장 구분 등으로 비교적 작다. 조건 분기, 반복, 컬렉션 순회와 같은 기능은 대부분 별도의 제어문이 아니라 Boolean 객체, 정수, 컬렉션과 코드 블록에 보내는 메시지로 표현된다.
Smalltalk 소스 코드는 하나 이상의 표현식으로 이루어진다. 표현식을 평가하면 객체가 결과로 생성되며, 그 결과에 다시 메시지를 연속해서 보낼 수 있다.
OrderedCollection new
add: 'Smalltalk';
add: 'Pharo';
yourself
위 표현식은 OrderedCollection 클래스에 new 메시지를 보내 객체를 생성하고, 생성된 객체에 문자열을 추가한 뒤 그 객체 자체를 결과로 반환한다.
여러 문장은 마침표(.)로 구분한다. 마침표는 문장을 끝내는 구분자이며 일반적으로 마지막 문장의 뒤에는 생략할 수 있다.
total := 10.
total := total + 5.
Transcript show: total printString
Smalltalk-80의 초기 문헌에서는 대입을 왼쪽 화살표로, 반환을 위쪽 화살표로 표시했다. 현대의 ASCII 기반 소스 코드에서는 일반적으로 대입에 :=, 반환에 ^를 사용한다.[24]
리터럴과 변수
리터럴은 소스 코드 안에 객체의 값을 직접 표현하는 구문이다. Smalltalk은 수, 문자, 문자열, Symbol, 배열과 일부 구현체가 제공하는 추가 객체 리터럴을 지원한다.
정수는 일반적인 십진수 표기로 작성한다.
0
42
-17
밑수를 지정한 정수는 밑수r값 형식으로 작성할 수 있다.
2r1010
8r755
16rFF
위 표현식은 각각 이진수 1010, 팔진수 755, 16진수 FF를 나타낸다. 밑수 표기의 구체적인 허용 범위는 구현체에 따라 차이가 있을 수 있다.
실수는 소수점이나 지수 표기를 사용한다.
3.14
-0.5
1.25e3
문자는 달러 기호($) 뒤에 문자를 작성하여 표현한다.
$A
$7
$+
문자열은 작은따옴표로 감싼다.
'Smalltalk'
'객체와 메시지'
문자열 안에 작은따옴표를 넣으려면 작은따옴표를 두 번 작성한다.
'Smalltalk''s object model'
Symbol은 이름이나 선택자처럼 변하지 않는 식별 값을 나타내는 객체이다. 일반적으로 # 뒤에 이름을 작성한다.
#Smalltalk
#balance
#deposit:
#between:and:
문자열이 문자들의 변경 가능한 또는 독립적인 나열을 나타내는 데 사용되는 반면, Symbol은 시스템 안에서 같은 내용을 가진 값이 하나의 공유된 식별 객체로 취급되는 경우가 일반적이다. 클래스 이름, 메서드 선택자, 사전의 키와 열거형에 가까운 값에 널리 사용된다.
리터럴 배열은 #() 안에 요소를 나열한다.
#(1 2 3 4)
#('red' 'green' 'blue')
#(Smalltalk Pharo Squeak)
리터럴 배열 안에서 따로 #를 붙이지 않은 이름은 Symbol로 해석된다. 리터럴 배열은 소스 코드가 컴파일될 때 만들어지는 고정된 리터럴 객체이므로, 실행 중 계산한 표현식을 요소로 넣는 용도로 사용하지 않는다.
실행 중 평가되는 표현식으로 배열을 만들 때는 중괄호를 사용하는 동적 배열 문법을 지원하는 구현체가 많다.
{ 1 + 2. Date today. Object new }
이 구문은 각 표현식을 왼쪽에서 오른쪽으로 평가한 뒤 그 결과를 배열에 담는다. 중괄호 기반 동적 배열은 현대 Smalltalk 구현체에서 널리 제공되지만, 초기 Smalltalk-80의 최소 문법과 구현체별 지원 범위를 구분할 필요가 있다.
Smalltalk의 변수는 값을 직접 포함하는 고정된 형식의 저장 공간이라기보다 객체를 가리키는 이름으로 사용된다. 변수 자체에 정적 타입을 선언하지 않으며, 같은 변수에 서로 다른 클래스의 객체를 차례로 대입할 수 있다.
value := 10.
value := 'ten'.
value := Date today
대입 표현식은 오른쪽 표현식을 먼저 평가한 뒤 그 결과 객체를 변수에 연결한다. 대입 자체도 대입된 객체를 결과로 가지므로 여러 변수에 같은 객체를 연속해서 대입할 수 있다.
first := second := OrderedCollection new
위 표현식에서 first와 second는 같은 컬렉션 객체를 참조한다.
변수의 종류는 선언 위치와 유효 범위에 따라 나뉜다.
- 인자 변수는 메시지와 함께 메서드나 블록에 전달된다.
- 임시 변수는 메서드나 블록이 실행되는 동안 사용된다.
- 인스턴스 변수는 각 객체의 상태를 저장한다.
- 클래스 인스턴스 변수는 클래스 객체마다 별도의 상태를 저장한다.
- 클래스 변수는 클래스와 그 하위 클래스가 공유한다.
- 전역 변수는 시스템의 전역 이름 공간이나 환경에 등록된다.
- 의사 변수는 언어가 특별한 의미를 부여하며 일반 대입 대상이 될 수 없다.
메서드의 임시 변수는 세로 막대 사이에 선언한다.
| subtotal tax total |
subtotal := 100.
tax := subtotal * 0.1.
total := subtotal + tax
블록 인자는 이름 앞에 콜론을 붙이며, 블록의 임시 변수 역시 세로 막대로 구분한다.
[ :price :rate |
| tax |
tax := price * rate.
price + tax
]
self, super, nil, true와 false는 특별한 의미를 가진 의사 변수이다.
self는 현재 메서드의 메시지를 받은 객체를 가리킨다.super는 같은 수신자를 유지하면서 메서드 탐색을 상위 클래스에서 시작하게 한다.nil은 값이 없거나 아직 객체가 연결되지 않은 상태를 나타내는 단일 객체이다.true와false는 Boolean 계층의 단일 객체이다.
일부 구현체는 현재 실행 문맥을 나타내는 thisContext를 추가로 제공한다. 이는 디버거, 실행 스택 조사와 고급 반영 기능에 사용되지만 모든 Smalltalk 표준과 구현체에서 같은 방식으로 제공되는 것은 아니다.
단항 메시지
단항 메시지는 인자를 받지 않는 메시지이다. 수신자 뒤에 선택자 하나를 작성한다.
42 factorial
'Smalltalk' size
Date today
Object new
각 표현식은 다음과 같이 해석된다.
42 factorial은 정수42에factorial메시지를 보낸다.'Smalltalk' size는 문자열에size메시지를 보낸다.Date today는Date클래스 객체에today메시지를 보낸다.Object new는Object클래스 객체에new메시지를 보낸다.
단항 메시지의 선택자는 문자로 시작하는 하나의 식별자이다. 괄호가 없기 때문에 함수 호출처럼 size() 또는 new()로 작성하지 않는다.
단항 메시지는 연속해서 보낼 수 있으며 왼쪽에서 오른쪽으로 평가된다.
Date today month name
평가 흐름은 다음과 같다.
((Date today) month) name
먼저 Date에 today를 보내 날짜 객체를 얻고, 그 결과에 month를 보내 월 객체를 얻은 뒤, 다시 name을 보내 이름을 얻는다.
수신자가 클래스 이름처럼 보이는 경우에도 별도의 정적 호출이 되는 것은 아니다. 클래스 역시 객체이므로 일반 객체와 동일한 메시지 전송 규칙을 따른다.
이항 메시지
이항 메시지는 하나의 인자를 받으며 기호로 된 선택자를 사용한다. 산술, 비교, 결합과 좌표 생성처럼 두 객체 사이의 연산을 표현하는 데 주로 사용된다.
3 + 4
10 - 2
6 * 7
20 / 5
3 < 8
name = 'Smalltalk'
100 @ 200
3 + 4에서 수신자는 3, 선택자는 +, 인자는 4이다. Smalltalk에서 +는 언어에 고정된 산술 연산자가 아니라 정수 객체에 보내는 메시지 선택자이다.
3 + 4
이 표현은 개념적으로 3이라는 객체에 + 메시지를 인자 4와 함께 보내는 것이다. 수신자의 클래스가 해당 선택자에 어떻게 응답하는지에 따라 실제 동작이 결정된다.
이항 선택자는 하나 이상의 특수 문자로 구성될 수 있다. 일반적으로 다음과 같은 선택자가 사용된다.
+ - * / // \\ = == ~= ~~ < > <= >= , @ -> |
각 선택자의 의미는 메서드를 정의한 클래스가 결정한다. 예를 들어 ,는 문자열과 컬렉션을 연결하는 데 사용될 수 있고, @는 두 수를 좌표 객체로 결합하는 데 사용된다.
'Small' , 'talk'
10 @ 20
#name -> 'Smalltalk'
이항 메시지 사이에는 일반적인 수학 연산자 우선순위가 없다. 모든 이항 메시지는 같은 우선순위를 가지며 왼쪽에서 오른쪽으로 평가된다.
2 + 3 * 4
위 표현식은 다음과 같이 해석된다.
(2 + 3) * 4
따라서 결과는 20이다. 일반적인 수학식처럼 14를 얻으려면 괄호로 계산 순서를 명시해야 한다.
2 + (3 * 4)
이 규칙은 *와 /가 +와 -보다 먼저 계산되는 언어에 익숙한 사용자가 특히 주의해야 하는 부분이다. Smalltalk의 문법은 연산자별 우선순위 표를 두는 대신, 메시지 종류와 왼쪽에서 오른쪽이라는 단순한 규칙을 사용한다.[25]
키워드 메시지
키워드 메시지는 하나 이상의 인자를 받으며, 각 인자 앞에 콜론으로 끝나는 키워드를 작성한다.
account deposit: 100
deposit:은 하나의 인자를 받는 전체 선택자이다.
인자가 여러 개이면 선택자를 여러 키워드 조각으로 나눈다.
collection
copyFrom: 2
to: 5
위 메시지의 전체 선택자는 copyFrom:to:이며 인자는 2와 5이다. 선택자의 일부는 copyFrom:과 to:이지만, 각각 독립된 메시지가 아니라 두 부분이 합쳐져 하나의 메시지 이름을 이룬다.
5 between: 1 and: 10
이 표현의 전체 선택자는 between:and:이다. 5가 수신자이고 1과 10이 두 인자로 전달된다.
키워드 메시지는 인자의 역할을 선택자 안에 표현할 수 있으므로 여러 인자를 사용하는 동작을 비교적 자연스럽게 읽을 수 있다.
canvas
drawString: 'Smalltalk'
at: 100 @ 50
font: defaultFont
color: Color black
이 메시지의 전체 선택자는 drawString:at:font:color:이다.
객체 생성도 생성자 전용 문법 대신 클래스 객체에 보내는 키워드 메시지로 구성할 수 있다.
Point x: 10 y: 20
Color red: 0.5 green: 0.2 blue: 0.8
Array new: 10
메시지 키워드의 이름과 개수는 메서드의 인자 이름과 대응하지만, 호출 측에서 작성하는 키워드와 메서드 내부의 인자 변수 이름은 같은 개념이 아니다.
Rectangle class >> origin: aPoint extent: anExtent
^ self new
origin: aPoint;
extent: anExtent;
yourself
이 메서드의 선택자는 origin:extent:이고 aPoint와 anExtent는 메서드 내부에서 사용하는 인자 변수이다.
메시지 우선순위
Smalltalk은 괄호가 없는 메시지 연쇄를 해석하기 위해 메시지 종류별 우선순위를 사용한다.
평가 순서는 다음과 같다.
- 괄호 안의 표현식
- 단항 메시지
- 이항 메시지
- 키워드 메시지
- 같은 종류 안에서는 왼쪽에서 오른쪽
예를 들어 다음 표현식을 살펴볼 수 있다.
Pen new
color: Color red;
go: 100 + 20
첫 번째 메시지 연쇄만 분해하면 다음 순서로 평가된다.
Pen에 단항 메시지new를 보낸다.Color에 단항 메시지red를 보낸다.100에 이항 메시지+를 인자20과 함께 보낸다.- 만들어진 Pen 객체에 키워드 메시지
color:와go:를 보낸다.
단항 메시지는 이항 메시지보다 먼저 평가된다.
3 factorial + 2
위 표현식은 다음과 같다.
(3 factorial) + 2
이항 메시지는 키워드 메시지보다 먼저 평가된다.
collection at: index + 1
위 표현식은 다음과 같이 해석된다.
collection at: (index + 1)
키워드 메시지는 다른 키워드 메시지의 인자를 자동으로 묶어 주지 않는다. 다음 표현식은 전체가 하나의 키워드 메시지 선택자로 결합될 수 있으므로 의도에 따라 괄호가 필요하다.
receiver
first: (other second: value)
third: result
괄호가 없으면 first:second:third:처럼 다른 선택자로 해석될 수 있다. 따라서 키워드 메시지의 인자로 또 다른 키워드 메시지를 전달할 때는 일반적으로 괄호를 사용한다.
복잡한 메시지 연쇄는 괄호를 사용해 평가 순서를 명시할 수 있다. 다만 단순한 표현까지 과도하게 괄호로 감싸기보다 메시지 우선순위에 맞춰 줄을 나누는 방식이 Smalltalk 코드에서 널리 사용된다.[26]
같은 수신자에게 여러 메시지를 연속해서 보낼 때는 캐스케이드 문법을 사용할 수 있다. 세미콜론(;) 뒤의 메시지는 직전 메시지의 결과가 아니라 최초 수신자에게 전송된다.
stream := WriteStream on: String new.
stream
nextPutAll: 'Smalltalk';
space;
nextPutAll: 'language'
위 코드는 stream에 nextPutAll:, space, nextPutAll: 메시지를 각각 보낸다.
캐스케이드에서 표현식 전체의 결과는 기본적으로 마지막 메시지의 결과이다. 최초 수신자 자체가 필요할 때는 관례적으로 yourself 메시지를 마지막에 보낸다.
OrderedCollection new
add: 10;
add: 20;
add: 30;
yourself
add:가 추가한 요소를 반환하는 구현체에서도 마지막의 yourself는 최초 수신자인 컬렉션을 반환한다.
세미콜론과 마침표의 역할은 서로 다르다.
- 마침표는 현재 문장을 종료한다.
- 세미콜론은 같은 수신자에 대한 메시지 전송을 이어 간다.
코드 블록과 클로저
코드 블록은 나중에 실행할 표현식의 묶음을 객체로 만든 것이다. 대괄호 안에 코드를 작성한다.
[ 1 + 2 ]
블록을 작성하는 것만으로 내부 코드가 즉시 실행되지는 않는다. 블록에 value 메시지를 보내야 평가된다.
[ 1 + 2 ] value
위 표현식은 3을 반환한다.
블록은 인자를 받을 수 있다. 각 인자는 콜론으로 시작하며, 마지막 인자 뒤의 세로 막대가 인자 목록과 본문을 구분한다.
[ :number | number squared ]
인자가 하나인 블록은 value: 메시지로 실행한다.
[ :number | number squared ] value: 5
인자가 여러 개이면 인자 수에 맞는 메시지를 사용한다.
[ :left :right | left + right ]
value: 10
value: 20
블록 내부에 임시 변수를 선언할 수도 있다.
[ :price |
| tax total |
tax := price * 0.1.
total := price + tax.
total
]
블록의 결과는 일반적으로 마지막으로 평가된 표현식의 결과이다.
코드 블록은 자신이 생성된 위치의 어휘적 환경을 보존하는 클로저로 동작한다. 블록은 외부 메서드나 블록의 인자와 임시 변수를 참조할 수 있으며, 블록이 생성된 실행 범위를 벗어난 뒤에도 필요한 값을 유지할 수 있다.
makeCounter
| count |
count := 0.
^ [ count := count + 1 ]
makeCounter가 반환한 블록은 메서드 실행이 끝난 뒤에도 count를 기억한다.
counter := self makeCounter.
counter value.
counter value.
counter value
각 실행은 같은 포획 변수 count를 갱신한다.
블록은 제어 구조와 컬렉션 처리에 널리 사용된다.
numbers select: [ :each | each even ]
numbers collect: [ :each | each squared ]
numbers do: [ :each |
Transcript show: each printString; cr
]
여기서 블록은 익명 함수와 비슷한 역할을 하지만, Smalltalk에서는 일반 객체이므로 변수에 저장하고 인자로 전달하며 컬렉션에 넣거나 메시지를 보낼 수 있다.
Smalltalk-80의 전통적인 블록 의미와 현대 구현체의 완전한 어휘적 클로저 구현 사이에는 세부 차이가 있다. 현대의 Squeak, Pharo와 여러 Smalltalk 구현체는 일반적으로 블록이 외부 변수를 포획하는 완전한 클로저 의미를 제공한다.
조건문과 반복
Smalltalk의 조건문은 언어에 내장된 if 문이 아니라 Boolean 객체에 보내는 메시지로 표현된다. 조건식은 true 또는 false 객체를 반환하며, 선택된 Boolean 객체가 적절한 블록을 실행한다.
balance > 0
ifTrue: [
Transcript show: '잔액이 남아 있다'
]
두 갈래 조건은 ifTrue:ifFalse: 메시지를 사용한다.
balance >= amount
ifTrue: [
balance := balance - amount
]
ifFalse: [
self error: '잔액이 부족하다'
]
이 표현의 전체 선택자는 ifTrue:ifFalse:이다. 조건이 참이면 첫 번째 블록을, 거짓이면 두 번째 블록을 평가한다.
반대 방향의 메시지도 사용할 수 있다.
collection isEmpty
ifFalse: [
self process: collection first
]
Boolean 연산에서도 블록을 사용하면 필요한 경우에만 오른쪽 표현식을 평가할 수 있다.
object notNil and: [ object isValid ]
cachedValue notNil or: [ self loadValue ]
and:와 or:는 인자로 받은 블록의 실행을 지연하므로 단락 평가를 구성할 수 있다. &와 | 같은 이항 메시지는 구현체와 클래스 라이브러리의 정의에 따라 양쪽 표현식이 이미 평가된 값을 대상으로 동작할 수 있으므로 목적에 따라 구분한다.
조건을 만족하는 동안 반복하려면 조건 블록에 whileTrue:를 보낸다.
[ index <= items size ] whileTrue: [
self process: (items at: index).
index := index + 1
]
조건이 거짓인 동안 반복하려면 whileFalse:를 사용한다.
[ stream atEnd ] whileFalse: [
self process: stream next
]
횟수 기반 반복은 정수에 메시지를 보내 표현할 수 있다.
10 timesRepeat: [
Transcript show: 'Smalltalk'; cr
]
범위 반복 역시 정수에 보내는 키워드 메시지로 구성된다.
1 to: 10 do: [ :index |
Transcript show: index printString; cr
]
증가 폭을 지정할 수도 있다.
0 to: 100 by: 10 do: [ :value |
Transcript show: value printString; cr
]
컬렉션 순회는 인덱스를 직접 관리하는 반복문보다 컬렉션 프로토콜을 사용하는 방식이 일반적이다.
users do: [ :user |
user update
]
activeUsers := users select: [ :user |
user isActive
]
total := prices
inject: 0
into: [ :sum :price | sum + price ]
조건과 반복이 메시지로 구현되기 때문에 사용자 정의 객체도 같은 형태의 제어 프로토콜을 제공할 수 있다. 가상 머신은 성능을 위해 일반적인 Boolean 및 블록 메시지를 특별하게 최적화할 수 있지만, 언어 수준에서는 객체에 보내는 메시지라는 의미를 유지한다.[27]
메서드 정의
메서드는 객체가 특정 메시지를 받았을 때 실행할 동작을 정의한다. 메서드는 클래스의 인스턴스 쪽 또는 클래스 쪽에 속하며, 헤더와 본문으로 구성된다.
인자가 없는 단항 메서드는 선택자 하나로 헤더를 작성한다.
BankAccount >> balance
^ balance
위 표기에서 BankAccount >> balance는 BankAccount의 인스턴스 쪽에 balance 메서드를 정의한다는 뜻으로 널리 사용되는 현대적인 표현이다. 브라우저 기반 전통적 Smalltalk 환경에서는 클래스와 프로토콜을 먼저 선택한 뒤 메서드 본문만 입력하기도 한다.
이항 메서드는 이항 선택자와 하나의 인자 변수로 헤더를 구성한다.
Vector >> + anotherVector
^ self class
x: x + anotherVector x
y: y + anotherVector y
키워드 메서드는 각 키워드 뒤에 인자 변수를 작성한다.
BankAccount >> deposit: anAmount
balance := balance + anAmount.
^ anAmount
인자가 여러 개인 경우 키워드와 인자 변수를 이어 쓴다.
BankAccount >> transfer: anAmount to: anotherAccount
self withdraw: anAmount.
anotherAccount deposit: anAmount.
^ anAmount
이 메서드의 선택자는 transfer:to:이고 인자 변수는 anAmount와 anotherAccount이다.
메서드의 임시 변수는 본문 앞의 세로 막대 사이에 선언한다.
Invoice >> totalWithTaxRate: aRate
| subtotal tax |
subtotal := self subtotal.
tax := subtotal * aRate.
^ subtotal + tax
큰따옴표로 감싼 텍스트는 주석이다.
BankAccount >> withdraw: anAmount
"계좌에서 지정된 금액을 출금한다."
balance := balance - anAmount
작은따옴표 문자열과 큰따옴표 주석은 서로 다른 구문이다.
'이것은 문자열이다'
"이것은 주석이다"
캐럿(^)은 메서드의 명시적 반환을 나타낸다.
BankAccount >> isEmpty
^ balance = 0
캐럿을 만나면 현재 메서드의 실행을 끝내고 뒤의 표현식 결과를 호출자에게 반환한다.
메서드에서 명시적인 반환을 작성하지 않으면 Smalltalk-80 계열에서는 일반적으로 self, 즉 메시지를 받은 수신자를 반환한다.
BankAccount >> reset
balance := 0
reset의 마지막 표현식은 대입된 0이지만, 명시적인 ^가 없으므로 메서드 호출 결과는 일반적으로 수신자인 계좌 객체이다. 이는 작업을 수행하는 메서드를 연쇄하거나 객체 초기화 프로토콜을 구성할 때 활용된다.
반면 코드 블록은 명시적인 반환이 없을 때 마지막 표현식의 값을 결과로 반환한다.
[ 10 + 20 ] value
따라서 메서드와 블록의 기본 반환 규칙을 구분해야 한다.
블록 안의 캐럿은 전통적인 Smalltalk에서 비지역 반환을 수행한다. 블록 자체에서만 빠져나오는 것이 아니라, 그 블록을 정의한 메서드의 실행을 종료한다.
Collection >> firstPositive
self do: [ :each |
each positive ifTrue: [ ^ each ]
].
^ nil
컬렉션을 순회하다 양수를 발견하면 블록을 호출한 do: 메서드가 아니라 firstPositive 메서드 전체에서 반환한다.
탈출하려는 메서드의 실행이 이미 끝난 뒤 저장된 블록에서 비지역 반환을 시도하면 실행 문맥이 존재하지 않으므로 오류가 발생할 수 있다. 구현체는 이를 블록 반환 오류나 유효하지 않은 반환으로 보고한다.
메서드는 자신의 클래스에 속한 인스턴스 변수와 전달된 인자, 임시 변수, 공유 변수와 전역 이름을 참조할 수 있다. 인스턴스 변수를 외부 객체에서 직접 선택하는 필드 접근 문법은 일반적으로 제공하지 않는다. 다른 객체의 상태는 해당 객체가 제공하는 메시지를 통해 읽거나 변경한다.
메서드 본문에서 self로 보내는 메시지는 실제 수신자의 클래스부터 동적으로 탐색한다.
BankAccount >> description
^ self currencySymbol , balance printString
super로 보내는 메시지는 현재 메서드를 정의한 클래스의 상위 클래스부터 탐색하지만, 메시지를 받는 객체는 계속 원래의 self이다.
SavingsAccount >> initialize
super initialize.
interestRate := 0
메서드의 선택자와 구현은 클래스의 메서드 사전에 등록된다. 객체가 메시지를 받으면 가상 머신은 수신자의 클래스에서 선택자에 대응하는 메서드를 찾고, 찾지 못하면 상위 클래스 방향으로 탐색한다. 메서드를 찾으면 인자를 매개 변수에 연결하고 수신자를 self에 연결한 뒤 본문을 실행한다.[28]
Smalltalk 환경에서는 메서드를 실행 중인 이미지에 개별적으로 컴파일할 수 있다. 하나의 메서드를 수정해 저장하면 해당 클래스의 메서드 사전이 갱신되며, 이후의 메시지 전송부터 새로운 구현이 사용된다. 이 구조는 문법과 실행 의미를 라이브 프로그래밍 환경에 직접 연결한다.
타입 체계
Smalltalk은 일반적으로 동적 타입과 강한 타입 검사를 사용하는 언어로 분류된다. 변수, 메서드 인자와 반환값에 정적 타입을 선언하지 않으며, 객체가 특정 메시지에 응답할 수 있는지는 프로그램이 실행되면서 확인된다. 그러나 서로 관련 없는 값을 암시적으로 같은 표현으로 변환하거나 객체의 내부 표현을 임의로 해석하지는 않는다.
Smalltalk에서 타입은 변수에 고정된 속성이라기보다 실행 중인 객체의 클래스와 그 객체가 이해하는 메시지 집합에 연결된다. 같은 변수는 실행 과정에서 서로 다른 클래스의 객체를 참조할 수 있다.
value := 10.
value := 'Smalltalk'.
value := Date today
위 코드에서 value에는 차례로 정수, 문자열과 날짜 객체가 대입된다. 변수 선언을 변경하거나 명시적인 형 변환을 수행할 필요는 없다. 각 객체는 자신의 클래스를 유지하며, 이후 어떤 메시지를 받을 수 있는지는 현재 연결된 객체가 결정한다.
value printString
printString 메시지를 받은 객체의 실제 클래스와 상속 계층에서 대응하는 메서드가 탐색된다. 컴파일러는 일반적으로 value가 어느 클래스의 객체인지 하나로 확정하지 않고 메시지 전송 코드를 생성한다.
Smalltalk의 동적 타입은 모든 값이 구분 없이 취급된다는 의미가 아니다. 정수는 정수 클래스의 인스턴스이고 문자열은 문자열 클래스의 인스턴스이며, 각 객체는 자신의 클래스와 상태, 동작을 유지한다. 타입 검사가 사라진 것이 아니라 대부분 실행 시점의 메시지 탐색으로 이동한 것이다.[29]
동적 타입
Smalltalk의 변수는 특정 자료형의 값을 담도록 고정된 저장 공간이라기보다 객체를 참조하는 이름이다. 메서드 인자, 임시 변수, 인스턴스 변수와 공유 변수에도 일반적으로 타입 이름을 붙이지 않는다.
Invoice >> addItem: anItem
items add: anItem
anItem이 어느 클래스의 인스턴스인지는 메서드 선언에 나타나지 않는다. addItem:은 전달된 객체를 컬렉션에 추가하며, 이후 해당 객체에 필요한 메시지를 보낼 때 실제 호환성이 확인된다.
Invoice >> total
^ items
inject: 0
into: [ :sum :each | sum + each price ]
이 메서드에서 items의 각 요소는 최소한 price 메시지를 이해해야 한다. 요소들이 모두 같은 클래스에 속할 필요는 없다. 서로 다른 클래스의 객체라도 price에 적절히 응답하면 함께 사용할 수 있다.
동적 타입은 객체의 실제 클래스가 실행 중에도 유지된다는 점과 구분해야 한다.
value := 42.
value class
위 표현식은 정수 객체의 클래스를 반환한다. 변수가 타입 선언을 갖지 않는 것과 객체가 클래스에 속하지 않는 것은 서로 다른 개념이다. Smalltalk-80 계열에서 일반 객체는 클래스의 인스턴스이며, 클래스가 객체의 상태 구조와 메서드를 정의한다.
동적 타입은 메시지 전송과 함께 작동한다. 다음 표현식이 컴파일될 때 컴파일러는 anObject의 구체적인 클래스를 알지 못해도 선택자와 메시지 전송 구조를 기록할 수 있다.
anObject displayOn: aCanvas
실행 시점에는 다음 과정이 수행된다.
anObject가 참조하는 실제 객체를 확인한다.- 그 객체의 클래스에서
displayOn:메서드를 찾는다. - 메서드가 없으면 상위 클래스 방향으로 탐색한다.
- 발견한 메서드를 원래 객체를 수신자로 하여 실행한다.
- 계층 끝까지 메서드가 없으면 메시지 이해 실패 절차로 넘어간다.
따라서 동일한 소스 코드가 실행될 때마다 수신자의 클래스에 따라 다른 메서드를 선택할 수 있다. 이 성질은 메서드 재정의, 프록시, 실행 중 클래스 수정과 라이브 프로그래밍을 자연스럽게 지원한다.
documents do: [ :document |
document renderOn: canvas
]
documents에는 텍스트 문서, 이미지 문서와 벡터 문서처럼 서로 다른 클래스의 객체를 넣을 수 있다. 각 객체가 renderOn: 메시지를 이해하면 호출 코드는 객체별 분기 없이 유지된다.
동적 타입은 컴파일 시점의 타입 선언을 요구하지 않으므로 새로운 종류의 객체를 기존 코드에 연결하기 쉽다. 반면 특정 메시지가 실제로 지원되는지는 해당 실행 경로가 수행될 때까지 발견되지 않을 수 있다. Smalltalk 개발 환경은 테스트, 코드 브라우저, 송신자와 구현자 검색, 실행 중 디버거를 통해 이러한 동적 구조를 다룬다.
강한 타입 검사
Smalltalk을 강한 타입 언어라고 부르는 것은 객체의 종류와 동작 규칙을 무시한 암시적 해석을 일반적으로 허용하지 않는다는 뜻이다. 동적 타입은 타입 검사의 시점을 나타내고, 강한 타입 검사는 서로 다른 종류의 값이 어떤 규칙으로 결합되는지를 나타낸다. 두 개념은 서로 반대되지 않는다.
예를 들어 정수와 문자열에 + 메시지를 사용하는 코드를 생각할 수 있다.
10 + '20'
Smalltalk은 문자열 '20'을 자동으로 정수 20으로 변환하여 더하지 않는다. 10이 속한 정수 클래스의 + 메서드는 전달된 객체와 산술 연산을 수행하려고 하며, 호환되지 않는 객체라면 실행 중 오류가 발생한다.
명시적인 변환이 필요하면 변환을 나타내는 메시지를 직접 보내야 한다.
10 + '20' asInteger
마찬가지로 정수를 문자열과 연결하려면 정수 객체를 문자열 표현으로 변환해야 한다.
'Total: ' , 20 printString
강한 타입 검사는 모든 클래스가 같은 메시지 이름을 같은 의미로 사용하도록 강제한다는 뜻은 아니다. 이항 선택자와 키워드 선택자의 동작은 수신자 클래스가 결정한다.
'Small' , 'talk'
#(1 2) , #(3 4)
두 표현식은 모두 , 메시지를 사용하지만, 문자열과 배열은 자신의 클래스에 맞는 방식으로 결합된다. 메시지 선택자의 의미가 수신자의 프로토콜 안에서 결정되므로, 연산의 유효성 역시 수신자와 인자의 조합에 따라 달라진다.
Smalltalk 객체의 인스턴스 변수는 일반적으로 외부 코드에서 메모리 위치나 다른 원시 타입으로 직접 재해석할 수 없다. 객체 외부에서는 해당 객체가 공개한 메시지를 통해 상태에 접근한다. 네이티브 코드 연동이나 가상 머신 내부의 원시 연산은 별도의 명시적인 인터페이스를 사용한다.
Smalltalk의 강한 타입 성질은 메모리 표현을 직접 노출하는 저수준 언어와 구분되는 객체 캡슐화의 일부이다. 객체는 자신의 표현을 유지하고, 외부 객체는 허용된 메시지를 통해서만 동작을 요청한다.
객체의 클래스와 프로토콜
Smalltalk에서 객체의 클래스는 그 객체의 상태 구조와 기본 메서드 집합을 정의한다. 그러나 실제 프로그램에서 객체를 사용할 수 있는지는 클래스 이름만큼이나 그 객체가 제공하는 프로토콜에 의해 결정된다.
프로토콜은 객체가 이해하는 관련 메시지의 집합과 그 메시지가 따라야 할 동작상의 약속을 뜻한다. 예를 들어 컬렉션 프로토콜에는 다음과 같은 메시지가 포함될 수 있다.
do:collect:select:includes:sizeisEmpty
배열, 순서 있는 컬렉션, 집합과 사전은 서로 다른 클래스 계층과 저장 방식을 가질 수 있지만, 공통 컬렉션 프로토콜의 일부에 응답한다.
aCollection do: [ :each |
Transcript show: each printString; cr
]
호출 측은 aCollection이 정확히 Array인지 OrderedCollection인지 검사하지 않는다. 객체가 do: 메시지를 이해하고 규정된 방식으로 요소를 블록에 전달하면 된다.
클래스 상속과 프로토콜 적합성은 서로 관련되지만 동일하지 않다. 상속받은 메서드는 하위 클래스 객체가 프로토콜을 제공하도록 만들 수 있다. 그러나 서로 공통 상위 클래스를 직접 공유하지 않거나 서로 다른 계층에 속한 객체도 같은 선택자 집합을 구현하여 동일한 역할을 수행할 수 있다.
예를 들어 파일 스트림과 메모리 스트림은 내부 구조가 다르더라도 다음과 같은 스트림 프로토콜에 응답할 수 있다.
stream
nextPutAll: 'Smalltalk';
cr;
flush
이 구조에서는 정적인 인터페이스 선언보다 메시지의 의미와 사용 관례가 중요하다. Smalltalk 브라우저는 메서드를 accessing, enumerating, testing, printing과 같은 프로토콜 분류에 배치하여 클래스가 제공하는 역할을 탐색하기 쉽게 한다. 이러한 프로토콜 분류는 소스 코드를 정리하는 기능이며, 일반적으로 컴파일러가 강제하는 정적 타입 선언은 아니다.
객체가 특정 메시지를 이해하는지 실행 중 확인할 수도 있다.
anObject respondsTo: #renderOn:
respondsTo:는 선택자가 현재 객체의 클래스 계층에서 처리될 수 있는지를 조사한다. 그러나 메시지를 보내기 전에 항상 이 검사를 수행하는 방식은 일반적이지 않다. 객체가 필요한 프로토콜을 따른다는 전제 아래 메시지를 보내고, 프로그램 구조와 테스트를 통해 그 약속을 유지하는 방식이 주로 사용된다.
메시지 존재 여부만으로 프로토콜 전체의 의미가 보장되는 것은 아니다. 두 객체가 같은 선택자를 구현하더라도 인자의 해석, 반환값, 상태 변화와 오류 조건이 다르면 상호 교체하기 어려울 수 있다. 따라서 Smalltalk의 프로토콜은 메서드 이름의 집합뿐 아니라 동작상의 계약까지 포함한다.
덕 타이핑
Smalltalk의 다형성은 흔히 덕 타이핑으로 설명된다. 객체의 클래스 이름이나 명시적으로 선언된 인터페이스보다, 필요한 메시지에 실제로 응답하는지를 기준으로 객체를 사용한다.
objects collect: [ :each |
each area
]
위 코드에서 각 객체는 area 메시지를 이해해야 한다. 객체가 Circle, Rectangle 또는 다른 클래스인지 호출 측에서 확인하지 않는다.
Circle >> area
^ Float pi * radius squared
Rectangle >> area
^ width * height
두 클래스의 내부 상태와 상속 계층은 다르지만 같은 메시지 프로토콜을 제공한다. 따라서 같은 컬렉션 처리 코드에서 사용할 수 있다.
덕 타이핑은 단순히 메서드 이름이 우연히 같으면 된다는 의미보다 필요한 역할의 프로토콜을 제공하면 된다는 의미에 가깝다. 다음 메서드는 인자로 받은 객체가 next 메시지를 이해한다는 사실만 사용한다.
Parser >> readFrom: aStream
| token |
token := aStream next.
^ self parse: token
aStream은 파일 스트림, 문자열 스트림, 네트워크 스트림 또는 테스트용 가짜 스트림일 수 있다. 각각이 next의 계약을 제공하면 readFrom:을 수정하지 않고 교체할 수 있다.
이 방식은 테스트 대역을 만드는 데에도 활용된다.
FakeClock >> now
^ DateAndTime
year: 2026
month: 7
day: 17
hour: 12
minute: 0
second: 0
실제 시계 객체와 FakeClock이 모두 now 메시지에 응답하면, 호출 측은 공통 상위 클래스나 별도의 인터페이스 선언 없이 테스트용 객체를 사용할 수 있다.
덕 타이핑은 상속보다 객체 합성과 위임을 활용하는 설계와도 잘 맞는다. 특정 객체가 필요한 메시지를 직접 처리하거나 내부 협력 객체에 전달하면 기존 프로토콜 안에서 사용할 수 있다.
LoggingStream >> nextPutAll: aString
logger record: aString.
^ wrappedStream nextPutAll: aString
LoggingStream이 필요한 스트림 메시지를 구현하면 실제 저장 기능을 wrappedStream에 위임하면서도 다른 코드에는 스트림처럼 사용될 수 있다.
Smalltalk에서 덕 타이핑이라는 용어가 언어 문법의 공식 요소인 것은 아니다. 이는 동적 메시지 전송과 프로토콜 중심 설계에서 나타나는 사용 방식을 설명하는 표현이다.
타입 오류와 메시지 이해 실패
동적 타입을 사용하는 Smalltalk에서 가장 대표적인 실행 오류는 객체가 받은 메시지를 처리할 메서드를 찾지 못하는 경우이다.
42 first
정수 객체의 클래스와 상위 클래스 계층에 first 메서드가 없다면 정상적인 메서드 탐색은 실패한다. Smalltalk 시스템은 즉시 고정된 저수준 오류로 종료하는 대신, 실패한 메시지를 객체로 구성하여 수신자에게 일반적으로 doesNotUnderstand: 메시지를 보낸다.
개념적인 흐름은 다음과 같다.
- 객체에 선택자와 인자를 포함한 메시지를 보낸다.
- 수신자의 클래스에서 메서드 탐색을 시작한다.
- 상위 클래스까지 탐색해도 메서드를 찾지 못한다.
- 실패한 선택자와 인자를 나타내는 메시지 객체를 만든다.
- 원래 수신자에게
doesNotUnderstand:를 보낸다. - 기본 구현은 오류 또는 예외를 발생시켜 디버거를 연다.
Object >> doesNotUnderstand: aMessage
"구현체의 기본 동작은 일반적으로 메시지 이해 실패를 보고한다."
실제 기본 구현과 예외 클래스의 이름은 Smalltalk 구현체마다 다를 수 있다. 라이브 개발 환경에서는 오류가 발생한 실행 문맥과 수신자, 선택자, 인자와 호출 스택을 디버거에서 조사할 수 있다.
메시지 이해 실패는 정적 타입 언어의 컴파일 오류와 발생 시점이 다르다. 해당 코드가 이미지 안에 존재하더라도 실행되지 않은 경로라면 오류가 드러나지 않을 수 있다.
condition ifTrue: [
42 first
]
condition이 거짓이면 잘못된 메시지 전송 경로는 실행되지 않는다. 이 때문에 테스트와 실제 사용 경로의 실행이 타입 관련 오류를 발견하는 데 중요한 역할을 한다.
doesNotUnderstand:는 오류 처리뿐 아니라 동적 메시지 전달을 구현하는 데에도 사용할 수 있다. 프록시 객체는 자신이 직접 구현하지 않은 메시지를 내부 대상 객체로 전달할 수 있다.
Proxy >> doesNotUnderstand: aMessage
^ aMessage sendTo: target
이러한 구조는 원격 객체, 지연 로딩, 추적 프록시와 투명한 래퍼를 구성하는 데 활용된다. 다만 모든 알 수 없는 메시지를 가로채므로 실제 오타나 구현 누락까지 숨기지 않도록 전달 범위와 오류 처리를 명확하게 구성해야 한다.
타입 오류는 메시지를 찾지 못하는 경우에만 발생하지 않는다. 메서드는 존재하지만 전달된 인자의 프로토콜이 맞지 않아 메서드 내부에서 다른 메시지 전송이 실패할 수도 있다.
BankAccount >> deposit: anAmount
balance := balance + anAmount
deposit:은 어떤 객체든 인자로 받을 수 있지만, 실제 실행에서는 balance가 받은 + 메시지의 인자로 anAmount를 처리할 수 있어야 한다. 문자열이나 호환되지 않는 객체를 전달하면 오류는 deposit: 호출 자체가 아니라 메서드 내부의 산술 메시지 전송에서 나타날 수 있다.
이처럼 Smalltalk의 타입 호환성은 하나의 메서드 경계에서 끝나지 않고 객체 사이에 이어지는 메시지 프로토콜 전체에서 결정된다.
타입 검사와 개발 도구
전통적인 Smalltalk 환경은 정적 타입 선언 대신 실행 중 객체와 코드를 조사하는 개발 도구를 중심으로 타입 관련 문제를 다룬다.
시스템 브라우저에서는 특정 선택자의 구현자와 송신자를 검색할 수 있다.
#deposit:
선택자를 기준으로 검색하면 다음 정보를 조사할 수 있다.
- 어떤 클래스가 해당 메시지를 구현하는가
- 어느 메서드가 해당 메시지를 보내는가
- 선택자가 어떤 프로토콜에서 사용되는가
- 서로 다른 구현이 어떤 인자와 반환 관례를 따르는가
객체 검사기는 실행 중 변수에 연결된 객체의 실제 클래스와 상태를 보여 준다. 디버거는 실패한 메시지의 수신자와 인자, 호출 스택을 보존하므로 예상과 다른 객체가 전달된 위치를 추적할 수 있다.
단위 테스트는 프로토콜의 동작을 검증하는 주요 수단이다.
ShapeTest >> testEveryShapeComputesArea
shapes do: [ :shape |
self assert: shape area >= 0
]
이 테스트는 모든 도형이 같은 클래스에 속하는지를 검사하지 않고, area 프로토콜의 결과가 기대한 조건을 만족하는지를 확인한다.
현대 Smalltalk 환경은 전통적인 동적 실행 도구에 더해 코드 분석, 린트, 타입 추론과 자동 완성 기능을 제공할 수 있다. 이러한 도구는 변수의 사용, 메시지 전송과 관찰된 실행 정보를 분석하여 가능한 클래스나 선택자를 제안하지만, 언어의 기본 실행 의미를 정적 타입으로 바꾸지는 않는다.
선택적 타입 체계와 변형
Smalltalk 계열에서는 동적 타입의 실행 모델을 유지하면서 정적 분석이나 선택적 타입 선언을 추가하려는 연구가 지속되었다. 대표적인 사례인 Strongtalk은 기존 Smalltalk 코드와의 호환성을 유지하면서 구조적 타입 체계를 이용해 선택적인 정적 타입 검사를 제공하도록 설계되었다.[30]
Strongtalk의 타입 체계는 클래스와 타입을 구분한다. 객체의 클래스가 구현과 상속 구조를 나타내는 반면, 타입은 객체가 제공해야 하는 메시지 프로토콜을 구조적으로 기술한다. 따라서 서로 상속 관계가 없는 클래스도 필요한 프로토콜을 제공하면 같은 타입에 적합할 수 있다.
선택적 타입 선언은 코드 전체에 반드시 적용할 필요가 없으며, 타입이 선언된 영역과 선언되지 않은 기존 Smalltalk 코드를 함께 사용할 수 있도록 설계되었다. 이는 Smalltalk의 유연성과 기존 이미지의 호환성을 유지하면서 대규모 코드에서 일부 오류를 실행 전에 발견하려는 방향이었다. Strongtalk은 Smalltalk-80을 하위 호환 방식으로 확장하고 구조적 타입 체계를 제공하는 타입 검사기로 소개되었다.
Smalltalk을 대상으로 한 다른 연구와 도구도 다음과 같은 방식으로 타입 정보를 얻는다.
- 변수와 메시지 사용에서 타입을 추론한다.
- 실행 중 관찰된 수신자 클래스를 수집한다.
- 메서드 프로토콜을 구조적으로 비교한다.
- 주석이나 별도의 메타데이터에 타입을 기록한다.
- 정적 분석 결과를 자동 완성과 리팩터링에 사용한다.
이러한 확장은 Smalltalk의 표준 타입 체계를 하나로 교체한 것이 아니라 구현체나 개발 도구가 선택적으로 제공하는 기능이다. 전통적인 Smalltalk-80, Squeak, Pharo와 GNU Smalltalk의 일반적인 코드는 여전히 명시적인 정적 타입 선언 없이 실행되는 동적 메시지 모델을 중심으로 한다.
Smalltalk의 타입 체계를 이해할 때는 클래스, 타입, 프로토콜을 구분하는 것이 중요하다.
- 클래스는 객체의 구현, 상태 구조와 메서드 탐색 위치를 결정한다.
- 실행 시 타입 호환성은 객체가 필요한 메시지를 처리할 수 있는지에 따라 드러난다.
- 프로토콜은 객체가 제공하는 메시지와 그 동작상의 계약을 나타낸다.
- 선택적 정적 타입 시스템에서는 프로토콜을 별도의 타입으로 기술할 수 있다.
따라서 Smalltalk은 타입이 없는 언어가 아니다. 객체는 구체적인 클래스와 동작을 가지며 잘못된 메시지와 호환되지 않는 연산은 오류를 일으킨다. Smalltalk의 특징은 이러한 제약을 변수 선언에 미리 고정하기보다 객체와 메시지의 실제 상호작용을 통해 동적으로 판정한다는 데 있다.
실행 환경
Smalltalk의 실행 환경은 일반적으로 객체 이미지와 가상 머신으로 구성된다. 객체 이미지에는 클래스, 메서드, 전역 변수, 개발 도구와 사용자가 만든 객체가 하나의 객체 그래프로 저장된다. 가상 머신은 이 객체 그래프를 메모리에 적재하고, 컴파일된 메서드의 바이트코드를 실행하며, 메모리 관리와 운영체제 연동을 담당한다.
이 구조에서는 프로그램이 소스 파일과 실행 파일로만 나뉘지 않는다. 실행 중인 시스템 자체가 객체들의 집합으로 존재하고, 개발자는 그 안에서 클래스를 추가하거나 메서드를 수정하고 객체의 상태를 조사한다. 변경된 상태는 다시 이미지로 저장하여 다음 실행에서 이어갈 수 있다.
전통적인 Smalltalk-80은 하드웨어와 독립적인 객체 이미지와 플랫폼별 가상 머신의 결합으로 배포되었다. 같은 형식의 이미지가 서로 다른 컴퓨터에서 실행될 수 있도록 가상 머신이 실제 프로세서, 메모리와 입출력 장치의 차이를 추상화했다. 현대의 Squeak과 Pharo도 이러한 기본 구조를 계승하지만, JIT 컴파일, 세대별 가비지 컬렉션과 현대적인 객체 메모리 형식 등을 추가하고 있다.[31][32]
객체 이미지
객체 이미지는 Smalltalk 시스템을 구성하는 객체 메모리의 저장된 형태이다. 여기에는 응용 프로그램의 객체뿐 아니라 클래스, 컴파일된 메서드, Symbol, 전역 환경, 프로세스, 개발 도구와 사용자 인터페이스 상태까지 포함될 수 있다.
이미지는 단순한 소스 코드 묶음이나 실행 파일이 아니다. 객체 사이의 참조 관계와 각 객체의 상태를 보존한 객체 그래프이다. 가상 머신이 이미지 파일을 읽으면 저장된 객체 그래프를 다시 메모리에 구성하고 Smalltalk 시스템의 실행을 재개한다.
Smalltalk globals at: #BankAccount
위 표현식으로 얻는 클래스 역시 이미지 안에 존재하는 객체이다. 클래스가 가진 메서드 사전과 컴파일된 메서드, 상위 클래스 관계도 같은 객체 그래프의 일부이다.
이미지에는 일반적으로 다음과 같은 요소가 포함된다.
- 클래스와 메타클래스
- 컴파일된 메서드와 리터럴
- 전역 변수와 패키지 정보
- 사용자가 생성한 객체
- 열린 개발 도구와 사용자 인터페이스 상태
- 일부 실행 프로세스와 실행 문맥
- 시스템 설정과 개발 기록
이미지 기반 구조에서는 개발 환경과 실행 대상 프로그램이 물리적으로 완전히 분리되지 않는다. 시스템 브라우저와 디버거도 이미지 안의 객체와 클래스들로 구현되며, 응용 프로그램과 같은 메시지 전송 규칙으로 동작한다.
이미지를 저장할 때는 메모리에 존재하는 객체와 그 참조 관계가 이미지 파일에 기록된다. 이후 같은 이미지 파일을 열면 저장 당시 존재하던 클래스와 객체를 다시 사용할 수 있다.
Smalltalk snapshot: true andQuit: false
이미지를 저장하는 실제 메시지와 시스템 객체의 이름은 구현체마다 다르다. Pharo에서는 Smalltalk 전역이 SmalltalkImage current와 같은 환경 객체로 대체되거나 별도의 세션 관리 API를 사용하는 등 버전에 따른 차이가 있다.
이미지가 모든 외부 상태를 그대로 보존하는 것은 아니다. 파일 디스크립터, 네트워크 소켓, 창 시스템 자원, 운영체제 스레드와 같은 자원은 이미지 파일에 객체 형태로 기록되더라도, 다음 실행에서 동일한 운영체제 자원과 자동으로 연결될 수 없다. 이러한 객체는 이미지가 다시 시작될 때 외부 자원을 재구성하거나 무효 상태를 정리해야 한다.
Smalltalk 시스템은 이를 위해 시작과 종료 시점에 특정 객체에 알림을 보내거나 세션 관리자와 의존성 체계를 사용한다. 구현체에 따라 클래스 초기화 메서드, 시작 목록, 종료 목록 또는 세션 핸들러가 제공된다.
객체 이미지의 장점은 개발 중인 시스템 전체를 하나의 상태로 저장하고 이동할 수 있다는 점이다. 반면 이미지 안의 변경 사항이 전통적인 텍스트 파일 중심 개발 흐름에서는 보이지 않을 수 있으므로, 현대 Smalltalk 환경은 패키지, 변경 집합과 Git 기반 소스 관리 도구를 함께 사용한다.[33]
가상 머신과 바이트코드
Smalltalk 소스 코드는 일반적으로 메서드 단위로 컴파일된다. 컴파일러는 메서드의 문법을 분석하여 컴파일된 메서드 객체를 만들고, 그 안에 바이트코드와 리터럴, 인자 수와 임시 변수 등의 정보를 저장한다.
compiledMethod := BankAccount >> #deposit:
현대 Smalltalk 환경에서 위와 같은 표현은 클래스의 메서드 사전에서 deposit: 선택자에 대응하는 컴파일된 메서드 객체를 가져온다. 구체적인 조회 문법은 구현체에 따라 달라질 수 있지만, 컴파일된 메서드가 실행 중 조사 가능한 객체라는 원칙은 공통적이다.
바이트코드는 실제 CPU 명령어가 아니라 Smalltalk 가상 머신이 이해하는 중간 명령이다. 바이트코드는 다음과 같은 작업을 표현한다.
- 수신자와 변수의 값을 스택에 올린다.
- 리터럴 객체를 읽는다.
- 메시지를 전송한다.
- 인스턴스 변수와 임시 변수에 값을 저장한다.
- 조건에 따라 다른 위치로 이동한다.
- 메서드나 블록에서 값을 반환한다.
- 가상 머신의 원시 연산을 호출한다.
예를 들어 다음 메서드는 소스 코드 그대로 반복 해석되지 않고, 컴파일된 바이트코드로 저장된다.
BankAccount >> deposit: anAmount
balance := balance + anAmount.
^ balance
개념적으로는 다음과 같은 실행 흐름으로 변환된다.
- 수신자의
balance인스턴스 변수를 읽는다. - 인자
anAmount를 읽는다. +메시지를 전송한다.- 결과를
balance에 저장한다. - 결과 객체를 반환한다.
가상 머신은 바이트코드를 실행하면서 객체 생성, 메시지 전송, 메서드 탐색, 블록 실행, 프로세스 전환과 메모리 관리를 처리한다. 파일, 네트워크, 화면, 키보드와 같은 운영체제 기능도 가상 머신이나 외부 기능 인터페이스를 통해 Smalltalk 객체에 제공된다.
Smalltalk-80의 전통적인 가상 머신은 바이트코드를 순서대로 해석하는 인터프리터로 설명되었다. 가상 머신 명세는 특정 프로세서 명령어보다 객체 표현, 바이트코드 의미, 메서드 실행, 원시 연산과 저장 형식을 정의하는 데 초점을 두었다.
현대 Squeak과 Pharo에서 사용되는 OpenSmalltalk VM 계열은 바이트코드 인터프리터뿐 아니라 Cog JIT 컴파일러를 제공한다. Cog는 실행되는 Smalltalk 바이트코드를 네이티브 기계어로 변환하고, 메시지 전송 지점에 인라인 캐시를 적용하여 반복적인 메서드 탐색 비용을 줄인다.[34]
JIT 컴파일을 사용해도 언어 수준의 의미는 바뀌지 않는다. 수신자의 실제 클래스에 따라 메서드가 동적으로 선택되고, 실행 중 메서드가 교체되면 이후 메시지 전송은 새 메서드를 사용할 수 있어야 한다. 가상 머신은 이러한 동적 성질을 유지하면서 관찰된 수신자 클래스와 호출 패턴을 바탕으로 실행 경로를 최적화한다.
메시지 전송과 메서드 실행
Smalltalk의 바이트코드 실행에서 가장 중심적인 연산은 메시지 전송이다. 메시지 전송 바이트코드가 실행되면 가상 머신은 수신자, 선택자와 인자를 확인한 뒤 수신자의 클래스에서 메서드를 탐색한다.
개념적인 실행 순서는 다음과 같다.
- 수신자와 인자를 평가한다.
- 수신자의 클래스를 얻는다.
- 클래스의 메서드 사전에서 선택자를 찾는다.
- 찾지 못하면 상위 클래스로 이동한다.
- 컴파일된 메서드를 발견하면 실행 문맥을 만든다.
- 수신자를
self에, 인자를 메서드 인자 변수에 연결한다. - 메서드의 바이트코드를 실행한다.
- 반환값을 메시지를 보낸 실행 문맥으로 전달한다.
메서드 탐색이 상위 클래스에서 끝나더라도 self는 메시지를 받은 원래 객체를 유지한다. super 메시지는 수신자를 바꾸지 않고 메서드 탐색을 시작하는 클래스만 변경한다.
메서드를 찾지 못하면 가상 머신과 기본 객체 모델은 실패한 메시지를 나타내는 객체를 구성하고 수신자에게 doesNotUnderstand: 메시지를 보내는 절차로 연결한다.
메시지 전송은 동적으로 수행되므로 반복적으로 같은 클래스의 객체가 나타나는 호출 지점에서도 매번 전체 상속 계층을 탐색하면 비용이 커질 수 있다. 현대 가상 머신은 인라인 캐시를 사용하여 최근에 관찰한 수신자 클래스와 메서드를 기억한다.
단일 클래스가 주로 나타나는 호출 지점은 단형적 캐시로 처리할 수 있고, 여러 클래스가 나타나는 지점은 다형적 캐시나 다른 최적화 전략을 사용할 수 있다. 수신자 클래스나 메서드가 변경되면 캐시를 무효화하거나 갱신하여 Smalltalk의 동적 실행 의미를 유지한다.
일부 연산은 일반 Smalltalk 메서드보다 가상 머신의 원시 연산으로 구현된다. 정수 산술, 객체 생성, 메모리 접근, 파일 입출력과 화면 처리가 대표적이다.
SmallInteger >> + aNumber
<primitive: 1>
^ super + aNumber
위 코드는 원시 연산 번호를 표시하는 전통적인 형태의 예이다. 실제 원시 연산 번호와 실패 처리 코드는 구현체마다 다르다.
가상 머신이 원시 연산을 성공적으로 수행하면 결과를 즉시 반환한다. 인자가 지원되지 않거나 오버플로가 발생하는 등 원시 연산이 실패하면 메서드의 Smalltalk 코드로 실행을 이어가 다른 방식으로 처리할 수 있다. 이를 통해 자주 사용되는 저수준 연산은 빠르게 실행하면서도 객체 모델과 예외적 상황을 Smalltalk 코드로 확장할 수 있다.
가비지 컬렉션
Smalltalk의 객체는 일반적으로 가상 머신이 관리하는 객체 메모리에 할당된다. 개발자는 객체를 생성할 수 있지만, 더 이상 사용하지 않는 객체의 메모리를 직접 해제하지 않는다. 가상 머신의 가비지 컬렉터가 도달할 수 없는 객체를 찾아 메모리를 회수한다.
account := BankAccount new
new 메시지를 통해 생성된 객체는 객체 메모리에 배치된다. 이후 전역 변수, 실행 스택, 다른 객체의 인스턴스 변수와 같은 루트에서 해당 객체까지 참조 경로가 남아 있는 동안 객체는 살아 있는 것으로 간주된다.
account := nil
위 대입 이후 계좌 객체를 참조하는 다른 경로가 없다면 그 객체는 이후 가비지 컬렉션에서 회수될 수 있다. nil을 대입한 시점에 반드시 즉시 메모리가 반환되는 것은 아니다.
전통적인 Smalltalk 구현체부터 자동 메모리 관리는 객체 중심 환경의 핵심 요소였다. 모든 값이 객체로 표현되고 메시지 전송 과정에서 많은 단기 객체가 만들어질 수 있으므로, 객체 할당과 회수 성능은 가상 머신의 중요한 역할이다.
현대 Smalltalk 가상 머신은 일반적으로 객체의 생존 특성에 따라 메모리 영역을 나누는 세대별 가비지 컬렉션을 사용한다. 새 객체는 젊은 세대에 배치하고, 여러 번의 수집에서도 살아남은 객체는 오래된 세대로 이동시킨다.
젊은 객체는 대체로 짧은 시간 안에 사용되지 않게 된다는 세대 가설을 활용하면 전체 객체 메모리를 매번 검사하지 않고 작은 영역을 자주 수집할 수 있다. 오래된 객체가 젊은 객체를 참조하는 경우를 추적하기 위해 쓰기 장벽과 기억 집합 등의 기법을 사용할 수 있다.
Squeak과 Pharo 계열에서 사용되는 Spur 객체 메모리는 32비트와 64비트 실행 환경을 지원하고, 객체 표현과 가비지 컬렉션을 현대화한 메모리 관리자이다.[35]
가비지 컬렉션은 객체의 메모리를 회수하지만 운영체제 자원을 자동으로 안전하게 종료하는 일반적인 대체 수단은 아니다. 파일과 소켓, 그래픽 자원처럼 명시적인 종료가 필요한 자원은 close, flush 또는 보장 실행 블록을 사용해 정리한다.
stream := FileStream open: aFileName mode: #read.
[
self process: stream contents
] ensure: [
stream close
]
ensure:는 앞의 블록이 정상적으로 끝나거나 오류와 비지역 반환으로 중단되더라도 정리 블록이 실행되도록 한다. 객체 메모리의 자동 회수와 외부 자원의 수명 관리는 서로 구분된다.
일부 구현체는 약한 참조, 종료 처리와 Ephemeron 같은 고급 메모리 기능을 제공한다. 이러한 기능은 캐시, 객체 등록소와 외부 자원 래퍼를 만들 때 사용되며 구체적인 클래스와 동작은 구현체마다 다르다.
실행 중 코드 변경
Smalltalk 환경에서는 실행 중인 이미지에 메서드를 개별적으로 컴파일하고 교체할 수 있다. 메서드를 수정하기 위해 응용 프로그램 전체를 종료하거나 하나의 실행 파일을 다시 연결할 필요가 없다.
BankAccount compile: '
deposit: anAmount
balance := balance + anAmount.
^ balance'
위와 같은 동적 컴파일 API의 정확한 형태는 구현체마다 다르지만, 컴파일 결과가 클래스의 메서드 사전에 즉시 등록된다는 실행 모델은 Smalltalk의 핵심적인 특성이다.
실행 중 코드 변경은 대체로 다음 과정으로 이루어진다.
- 개발자가 메서드 소스를 수정한다.
- 컴파일러가 새 컴파일된 메서드 객체를 만든다.
- 클래스의 메서드 사전에서 기존 선택자의 메서드를 교체한다.
- 가상 머신이 관련된 메시지 전송 캐시를 무효화한다.
- 이후 발생하는 메시지 전송부터 새 메서드를 사용한다.
이미 실행을 시작한 메서드의 실행 문맥은 일반적으로 기존 컴파일된 메서드를 계속 사용할 수 있다. 새로 호출되는 메시지는 갱신된 메서드를 선택한다. 구현체와 개발 도구에 따라 디버거에서 현재 실행 문맥을 다시 시작하거나 수정된 메서드로 계산을 재개할 수도 있다.
오류가 발생하면 Smalltalk 환경은 프로세스를 즉시 종료하기보다 실행 문맥을 보존한 상태로 디버거를 열 수 있다. 개발자는 호출 스택을 탐색하고 변수의 실제 객체를 검사하며, 문제가 있는 메서드를 수정한 뒤 실패한 메시지를 다시 실행하거나 이전 실행 지점부터 계속할 수 있다.
클래스의 구조도 실행 중 바꿀 수 있다. 클래스에 인스턴스 변수를 추가하거나 상위 클래스를 변경하면 기존 인스턴스를 새로운 구조로 변환해야 한다. Smalltalk 환경은 이러한 변경을 처리하기 위해 기존 객체를 새 클래스 형식으로 이주시키거나 클래스 객체를 교체하고 참조를 갱신하는 기능을 제공한다.
Object subclass: #BankAccount
instanceVariableNames: 'balance owner'
classVariableNames: ''
package: 'Banking'
이미 BankAccount 인스턴스가 존재하는 상태에서 owner 변수를 추가하면 개발 환경은 기존 객체의 balance 값을 유지하면서 새로운 슬롯을 포함한 구조로 변환할 수 있다. 구체적인 마이그레이션 방식과 허용되는 클래스 변경의 범위는 구현체마다 다르다.
실행 중 수정 기능은 언어의 동적 바인딩, 반영 가능한 클래스와 컴파일된 메서드, 이미지 기반 객체 메모리가 결합되어 가능하다. 이는 별도의 핫 리로드 계층을 추가한 기능이라기보다 Smalltalk 시스템의 기본 객체 모델에서 직접 나온다.
프로세스와 실행 문맥
Smalltalk에서 메서드 실행 상태는 개념적으로 실행 문맥으로 표현된다. 실행 문맥에는 현재 실행 중인 메서드, 수신자, 인자와 임시 변수, 평가 스택, 다음 바이트코드 위치와 호출자 문맥에 대한 정보가 포함된다.
전통적인 Smalltalk-80 객체 모델에서는 메서드 실행 문맥과 블록 실행 문맥을 객체로 접근할 수 있었다. 현대 가상 머신은 성능을 위해 실행 문맥을 네이티브 스택이나 내부 프레임으로 최적화하고, 프로그램이 반영 기능으로 문맥을 요구할 때 객체 형태로 물질화할 수 있다.
thisContext sender
thisContext를 제공하는 구현체에서는 현재 실행 문맥을 조사하고 호출자 문맥으로 이동할 수 있다. 이 기능은 디버거와 예외 처리, 실행 스택 분석의 기반이 되지만 일반적인 응용 프로그램 코드에서 직접 사용하는 경우는 제한적이다.
Smalltalk의 경량 실행 단위는 일반적으로 Process 객체로 표현된다. 코드 블록에 fork 메시지를 보내 새로운 프로세스로 실행할 수 있다.
[
self runBackgroundTask
] fork
Smalltalk 프로세스는 대개 운영체제 스레드와 일대일로 대응하지 않고 가상 머신 안에서 스케줄링되는 경량 프로세스이다. 전통적인 Smalltalk 시스템은 우선순위 기반 협력적 스케줄링을 사용하며, 세마포어와 지연 객체를 통해 프로세스를 대기시키거나 깨운다.
semaphore := Semaphore new.
[
self prepareData.
semaphore signal
] fork.
semaphore wait.
self consumeData
원시 연산, 타이머와 입출력 이벤트는 가상 머신이 관리하는 외부 신호를 통해 Smalltalk 프로세스와 연결된다. 현대 구현체는 운영체제 스레드, 비동기 입출력과 FFI 호출을 추가로 지원할 수 있지만, 구체적인 동시성 모델은 구현체마다 다르다.
디버거는 중단된 프로세스의 실행 문맥을 객체 형태로 보여 준다. 사용자는 각 스택 프레임의 수신자, 인자와 임시 변수를 검사하고, 메서드 소스로 이동하여 코드를 수정할 수 있다.
시스템 저장과 재개
Smalltalk 시스템은 실행 중인 객체 메모리를 스냅샷으로 저장하고 이후 다시 불러와 실행을 이어갈 수 있다. 저장된 스냅샷이 일반적으로 이미지 파일이다.
시스템 저장은 개념적으로 다음 과정으로 이루어진다.
- 이미지 저장을 요청한다.
- 저장 전 처리가 필요한 객체에 알림을 보낸다.
- 가상 머신이 객체 그래프와 메모리 메타데이터를 파일에 기록한다.
- 필요에 따라 현재 프로세스를 계속 실행하거나 가상 머신을 종료한다.
- 다음 실행에서 가상 머신이 이미지 파일을 적재한다.
- 저장 후 재개 처리를 실행하고 운영체제 자원을 다시 연결한다.
이미지를 저장한 호출은 저장을 수행한 현재 실행에서는 저장 결과를 나타내는 한 값을 반환하고, 저장된 이미지를 다시 시작했을 때는 재개 상태를 나타내는 다른 값을 반환할 수 있다. 이 구조를 통해 같은 실행 지점에서 저장 직후와 이미지 재시작 이후의 동작을 구분할 수 있다.
resumed := Smalltalk snapshot: true andQuit: false.
resumed
ifTrue: [
Transcript show: '이미지를 저장했다'
]
ifFalse: [
Transcript show: '저장된 이미지에서 재개했다'
]
위 API와 반환 규칙은 전통적인 Smalltalk 계열을 설명하기 위한 예이며 현대 구현체마다 메시지 이름과 의미가 다를 수 있다.
이미지 재개는 프로세스의 주소 공간을 운영체제 수준에서 그대로 복원하는 것과는 다르다. 가상 머신은 저장된 Smalltalk 객체 그래프를 다시 적재하지만 외부 파일, 네트워크 연결, 시스템 시간, 화면 해상도와 플러그인 상태는 달라질 수 있다.
따라서 재개 과정에서는 다음과 같은 작업이 필요할 수 있다.
- 화면과 입력 장치를 다시 초기화한다.
- 파일과 네트워크 자원의 유효성을 확인한다.
- 타이머와 비동기 이벤트를 다시 등록한다.
- 운영체제별 경로와 환경 변수를 갱신한다.
- 외부 라이브러리와 플러그인을 다시 연결한다.
- 저장 시점 이후 변경된 시간을 반영한다.
Smalltalk 이미지는 다른 컴퓨터로 이동할 수 있지만, 이미지 형식과 가상 머신의 객체 메모리 형식이 호환되어야 한다. 32비트와 64비트 형식, 바이트 순서, 객체 헤더 구조와 가상 머신 세대가 달라지면 이미지 변환이나 중간 버전에서의 마이그레이션이 필요할 수 있다.
현대 Smalltalk 프로젝트에서는 이미지가 유일한 배포·버전 관리 단위로 사용되지는 않는다. 소스 코드는 패키지 저장소와 Git에서 관리하고, 자동화된 부트스트랩이나 패키지 로딩을 통해 새 이미지를 재구성할 수 있다. 이미지는 실행 가능한 개발 상태와 빠른 시작점을 제공하고, 텍스트 기반 저장소는 변경 이력, 협업과 재현 가능한 빌드를 담당한다.
이러한 방식은 전통적인 이미지 중심 개발을 폐기하지 않고, 이미지의 지속적인 객체 환경과 현대 소스 관리의 장점을 결합한다.
운영체제와의 연결
객체 이미지 자체는 플랫폼 독립적인 객체와 바이트코드를 중심으로 구성되지만, 실제 실행에는 운영체제와 하드웨어의 기능이 필요하다. 가상 머신은 Smalltalk 시스템과 운영체제 사이의 경계를 제공한다.
가상 머신이 담당하거나 중개하는 기능에는 다음이 포함된다.
- 메모리 할당과 가비지 컬렉션
- 파일과 디렉터리 접근
- 네트워크 통신
- 창, 화면과 입력 장치
- 타이머와 시스템 시계
- 클립보드와 문자 입력
- 동적 라이브러리와 외부 함수 호출
- 운영체제 이벤트와 프로세스 연동
Smalltalk 코드는 이러한 기능을 클래스와 객체의 메시지 인터페이스로 사용한다.
file := aFileReference readStream.
[
self process: file contents
] ensure: [
file close
]
파일 객체의 실제 동작은 Smalltalk 클래스 라이브러리와 가상 머신의 파일 원시 연산 또는 외부 함수 인터페이스가 협력하여 수행한다.
플랫폼별 가상 머신은 동일한 객체 이미지가 Windows, macOS, Linux와 다른 운영체제에서 실행될 수 있게 한다. 다만 사용자 인터페이스 프레임워크, 문자 인코딩, 파일 경로와 외부 라이브러리 의존성에 따라 응용 프로그램 수준의 추가 이식 작업이 필요할 수 있다.
Squeak, Pharo와 Cuis는 공통 계통의 OpenSmalltalk VM을 사용할 수 있다. OpenSmalltalk VM은 인터프리터와 JIT 컴파일러, 객체 메모리, 플랫폼 추상화와 플러그인 체계를 제공한다. Squeak의 가상 머신 개발 방식은 가상 머신의 상당 부분을 Smalltalk로 기술한 뒤 C 코드로 변환하여 플랫폼별 실행기를 만드는 전통에서 발전했다.[36][37]
이 구조에서 Smalltalk 실행 환경은 고정된 네이티브 런타임 위에 언어만 올린 형태가 아니다. 객체 이미지 안의 클래스와 개발 도구, 가상 머신의 실행·메모리 관리, 운영체제 추상화가 결합되어 하나의 지속적이고 반영 가능한 프로그래밍 시스템을 이룬다.
라이브 프로그래밍과 개발 도구
Smalltalk의 개발 환경은 실행 중인 프로그램과 개발 도구를 엄격하게 분리하지 않는다. 클래스, 메서드, 객체, 실행 문맥과 개발 도구가 모두 같은 객체 이미지 안에 존재하며, 개발자는 실행 중인 객체를 직접 조사하고 메서드를 수정한 뒤 즉시 변경된 동작을 확인할 수 있다. 이러한 방식을 라이브 프로그래밍이라고 한다.
전통적인 컴파일 중심 환경에서는 일반적으로 소스 파일을 편집하고 프로그램을 빌드한 뒤 새 실행 파일을 시작한다. Smalltalk에서는 메서드 하나를 이미지 안에 컴파일하여 이미 존재하는 클래스에 즉시 설치할 수 있다. 이후 해당 선택자의 메시지를 보내면 변경된 메서드가 사용된다.
Counter >> increment
count := count + 1.
^ count
개발자가 위 메서드를 수정해 저장하면 실행 중인 Counter 인스턴스를 새로 만들지 않아도 다음 increment 메시지부터 변경된 구현을 사용할 수 있다. 객체의 상태는 그대로 유지되고 동작만 교체될 수 있다.
Smalltalk 환경에서 라이브 프로그래밍은 단순한 자동 재실행이나 소스 파일 감시 기능이 아니다. 개발자는 다음 작업을 실행 중인 시스템을 대상으로 수행한다.
- 임의의 표현식을 즉시 평가한다.
- 평가 결과 객체를 검사한다.
- 클래스와 메서드를 검색하고 수정한다.
- 실행 중인 객체의 상태를 탐색한다.
- 오류가 발생한 실행 문맥을 보존한다.
- 디버거 안에서 메서드를 작성하거나 수정한다.
- 수정된 메서드로 실패한 실행을 다시 시도한다.
- 객체 이미지 전체를 저장하고 나중에 재개한다.
Pharo는 이러한 환경을 개발, 테스트와 디버깅 과정에서 지속적으로 즉각적인 피드백을 제공하는 몰입형 환경으로 설명한다.[38]
시스템 브라우저
시스템 브라우저는 이미지 안의 클래스와 메서드를 탐색하고 편집하는 중심 도구이다. 전통적인 Smalltalk 브라우저는 패키지 또는 시스템 분류, 클래스, 프로토콜과 메서드를 계층적으로 선택하는 여러 영역으로 구성된다.
브라우저에서는 일반적으로 다음 순서로 코드를 탐색한다.
- 패키지나 시스템 분류를 선택한다.
- 그 안의 클래스를 선택한다.
- 인스턴스 쪽 또는 클래스 쪽을 선택한다.
- 메서드 프로토콜을 선택한다.
- 선택자를 선택하여 메서드 소스를 연다.
- 소스를 수정하고 컴파일한다.
클래스 정의도 브라우저에서 편집할 수 있다.
Object subclass: #Counter
instanceVariableNames: 'count'
classVariableNames: ''
package: 'Examples-Counter'
위 정의를 평가하면 Counter 클래스 객체가 실행 중인 이미지에 생성된다. 이후 브라우저에서 해당 클래스를 선택하고 메서드를 추가할 수 있다.
Counter >> initialize
super initialize.
count := 0
브라우저에서 메서드를 저장하면 소스가 단순히 파일에 기록되는 것이 아니라 컴파일러가 컴파일된 메서드 객체를 만들고 클래스의 메서드 사전에 설치한다. 따라서 별도의 전체 빌드 단계 없이 새 메서드를 즉시 사용할 수 있다.
counter := Counter new.
counter increment
브라우저는 클래스 계층을 탐색하는 기능도 제공한다. 선택한 클래스의 상위 클래스와 하위 클래스를 확인하고, 상속받은 메서드와 재정의된 메서드를 구분할 수 있다.
Smalltalk에서는 소스 코드 탐색이 파일 경로보다 클래스와 선택자를 중심으로 이루어진다. 개발자는 특정 메서드 이름을 기준으로 다음 정보를 검색할 수 있다.
- 해당 선택자를 구현하는 모든 클래스
- 해당 선택자의 메시지를 보내는 모든 메서드
- 현재 클래스가 상속받은 구현
- 하위 클래스에서 재정의한 구현
- 선택자와 이름이 비슷한 메서드
- 특정 문자열이나 리터럴을 참조하는 메서드
예를 들어 #deposit: 선택자의 구현자를 검색하면 이미지 안에서 이 메시지를 처리하는 모든 클래스를 찾을 수 있다.
#deposit:
구현자 검색은 선택자에 대응하는 메서드를 정의한 클래스를 찾고, 송신자 검색은 해당 선택자를 메시지로 보내는 메서드를 찾는다. 동적 바인딩 때문에 하나의 메시지 전송 지점에서 실제로 실행될 메서드를 정적으로 하나로 확정할 수는 없지만, 선택자를 중심으로 가능한 구현과 사용 위치를 탐색할 수 있다.
현대 Smalltalk 브라우저는 다음 기능도 제공할 수 있다.
- 클래스와 메서드 생성
- 이름 변경과 메서드 이동
- 인스턴스 변수 추가와 제거
- 메서드 추출
- 상속 계층 변경
- 코드 포맷과 정적 분석
- 테스트 실행
- 버전 비교
- 패키지와 저장소 탐색
브라우저 자체도 Smalltalk로 작성된 객체 집합이므로, 개발자는 필요에 따라 브라우저의 메뉴, 탐색 방식과 표시 내용을 수정할 수 있다. Smalltalk 환경에서 개발 도구는 고정된 외부 프로그램이 아니라 이미지 안에서 확장 가능한 응용 프로그램이다.
작업 공간
작업 공간은 Smalltalk 표현식을 자유롭게 작성하고 즉시 실행할 수 있는 텍스트 편집 영역이다. Squeak에서는 Workspace라고 부르며, Pharo에서는 같은 역할을 하는 도구가 Playground로 발전하였다.
account := BankAccount new.
account deposit: 100.
account balance
작업 공간에서는 소스 전체를 하나의 프로그램으로 빌드할 필요가 없다. 원하는 표현식만 선택해 실행하고 결과를 출력하거나 객체 검사기로 열 수 있다.
전통적인 Smalltalk 환경은 텍스트 선택 영역에 대해 다음과 같은 평가 동작을 제공한다.
- Do it은 선택한 표현식을 실행한다.
- Print it은 표현식을 실행하고 결과의 출력 표현을 텍스트에 표시한다.
- Inspect it은 표현식을 실행하고 결과 객체를 검사기로 연다.
Squeak은 Workspace뿐 아니라 Browser, Inspector와 Debugger의 코드 입력 영역에서도 이러한 방식으로 Smalltalk 표현식을 평가할 수 있다.[39]
예를 들어 다음 표현식을 선택하고 Print it을 실행할 수 있다.
(1 to: 10) select: [ :each | each even ]
결과는 작업 공간에 다음과 비슷한 형태로 표시된다.
#(2 4 6 8 10)
Inspect it을 사용하면 결과 컬렉션을 문자열 표현으로만 보는 대신, 실제 배열 또는 컬렉션 객체를 검사기에서 열어 각 요소와 내부 상태를 탐색할 수 있다.
작업 공간의 변수는 환경에 따라 임시 작업 공간 변수로 유지될 수 있다. 개발자는 여러 표현식 사이에서 객체를 보존하며 실험할 수 있다.
counter := Counter new.
counter increment.
counter increment.
counter inspect
이러한 환경은 API를 탐색하거나 객체가 이해하는 메시지를 시험하고, 작은 알고리즘을 작성하며, 실행 중인 응용 프로그램 객체에 직접 접근하는 데 사용된다.
작업 공간은 셸이나 REPL과 비슷한 역할을 하지만, 텍스트 출력만을 중심으로 하지 않는다. 평가 결과가 실제 객체로 유지되고 검사기, 디버거와 브라우저로 바로 연결된다는 점이 특징이다.
검사기
검사기는 실행 중인 객체의 구조와 상태를 탐색하는 도구이다. 문자열 출력이나 로그에 의존하지 않고, 객체가 가진 인스턴스 변수와 참조 객체를 직접 조사할 수 있다.
account inspect
위 메시지를 실행하면 account가 참조하는 실제 객체를 대상으로 검사기가 열린다.
검사기는 일반적으로 다음 정보를 표시한다.
- 객체의 클래스
- 인스턴스 변수와 현재 값
- 컬렉션의 요소
- 키와 값의 관계
- 객체가 참조하는 다른 객체
- 객체에 적용할 수 있는 사용자 정의 보기
- 객체를 대상으로 평가할 수 있는 코드 입력 영역
예를 들어 BankAccount 객체가 balance, owner와 transactions 인스턴스 변수를 가진다면 검사기에서 각 변수의 실제 객체를 확인할 수 있다.
BankAccount >> initialize
super initialize.
balance := 0.
transactions := OrderedCollection new
transactions 항목을 선택하면 그 컬렉션 객체로 이동하고, 컬렉션의 요소를 다시 선택하여 거래 객체를 탐색할 수 있다. 객체 참조를 따라가면서 실행 중인 객체 그래프를 이동하는 방식이다.
검사기의 코드 영역에서 현재 검사 중인 객체를 수신자로 표현식을 평가할 수도 있다.
self balance
self transactions collect: [ :each | each amount ]
Smalltalk 구현체에 따라 검사기의 현재 대상 객체를 self 또는 별도의 의사 변수로 참조하는 방식에는 차이가 있을 수 있다.
검사기는 단순히 객체의 메모리 슬롯을 나열하는 기능을 넘어설 수 있다. 현대적인 Smalltalk 환경에서는 객체의 종류에 맞는 전용 표현을 제공한다.
- 문자열은 텍스트로 표시한다.
- 색상 객체는 실제 색상 견본으로 표시한다.
- 이미지는 비트맵으로 표시한다.
- 파일 객체는 경로와 내용을 표시한다.
- 컬렉션은 요소 목록과 표 형태로 표시한다.
- 구문 트리는 트리 구조로 표시한다.
- 그래픽 객체는 실제 화면 표현을 제공한다.
이러한 보기는 객체나 확장 패키지가 제공하는 메시지를 통해 추가할 수 있다. 따라서 응용 프로그램 개발자는 자신의 도메인 객체를 위한 전용 검사 화면을 구성할 수 있다.
검사기에서 객체의 상태를 직접 변경할 수도 있지만, 이는 객체가 유지해야 하는 불변 조건을 우회할 수 있다. 일반적인 동작 시험에는 객체가 제공하는 메시지를 사용하고, 직접적인 슬롯 변경은 디버깅과 복구처럼 필요한 상황에서 선택적으로 사용한다.
탐색기
일부 Smalltalk 환경은 검사기와 별도로 탐색기를 제공한다. 검사기가 선택한 객체의 주요 상태를 구조화해 보여 준다면, 탐색기는 객체가 참조하는 전체 구조를 트리 형태로 따라가는 데 중점을 둔다.
account explore
탐색기에서는 객체의 인스턴스 변수를 펼쳐 하위 객체를 확인하고, 그 객체가 다시 참조하는 객체를 계속 확장할 수 있다.
객체 그래프에는 순환 참조가 존재할 수 있으므로 탐색기는 이미 방문한 객체를 표시하거나 무한한 확장을 방지해야 한다. 대규모 컬렉션과 복잡한 그래프를 탐색할 때는 일부 요소만 지연해서 표시할 수 있다.
검사기와 탐색기의 명칭과 기능 경계는 구현체에 따라 다르다. 일부 현대 환경에서는 강력한 검사기가 트리 탐색, 전용 시각화와 코드 평가를 모두 제공하여 별도의 탐색기를 대체한다.
디버거
Smalltalk의 디버거는 오류가 발생한 실행을 단순히 종료한 뒤 사후 분석하는 도구가 아니다. 오류 시점의 실행 문맥과 객체 상태를 보존하고, 개발자가 그 상태 안에서 메서드를 수정한 뒤 실행을 다시 이어갈 수 있는 라이브 개발 도구이다.
account withdraw: 1000
출금 과정에서 오류나 처리되지 않은 예외가 발생하면 Smalltalk 시스템은 일반적으로 해당 프로세스를 중단하고 디버거를 연다.
디버거는 호출 스택을 구성하는 실행 문맥을 표시한다. 각 문맥에는 다음과 같은 정보가 포함된다.
- 실행 중인 메서드
- 메시지를 받은 객체
- 메서드 인자
- 임시 변수
- 현재 실행 위치
- 호출자 문맥
- 평가 중인 표현식
개발자는 스택의 각 프레임을 선택하여 해당 메서드의 소스와 당시 변수 값을 확인할 수 있다.
BankAccount >> withdraw: anAmount
anAmount > balance
ifTrue: [ self error: '잔액이 부족하다' ].
balance := balance - anAmount
balance가 예상과 다른 객체를 참조하거나 anAmount가 잘못된 값이라면 디버거에서 두 변수의 실제 객체를 즉시 검사할 수 있다.
디버거의 코드 영역에서도 표현식을 평가할 수 있다.
balance
anAmount > balance
디버거에서 문제가 있는 메서드를 직접 수정하고 컴파일할 수도 있다.
BankAccount >> withdraw: anAmount
anAmount negative
ifTrue: [ self error: '출금액은 음수일 수 없다' ].
anAmount > balance
ifTrue: [ self error: '잔액이 부족하다' ].
balance := balance - anAmount.
^ anAmount
수정된 메서드는 클래스의 메서드 사전에 즉시 설치된다. 그 뒤 디버거는 구현체에 따라 다음과 같은 실행 제어를 제공한다.
- 현재 메시지를 다시 실행한다.
- 선택한 실행 문맥을 재시작한다.
- 다음 표현식으로 진행한다.
- 메시지 안으로 들어간다.
- 현재 메서드를 끝내고 호출자로 돌아간다.
- 특정 값을 반환하도록 실행을 종료한다.
- 프로세스를 중단한다.
이 흐름에서는 오류가 프로그램의 종료점이 아니라 실행 중인 구체적인 객체 상태에서 코드를 성장시키는 시작점이 될 수 있다. Pharo의 테스트 문서는 테스트 실패로 열린 디버거에서 프로그램을 작성하고 수정하는 개발 흐름을 설명한다.[40]
디버거에서 메서드를 재시작할 때는 이미 수행된 외부 효과를 고려해야 한다. 파일 쓰기, 네트워크 요청, 데이터베이스 변경과 같은 작업이 메서드 재실행으로 중복될 수 있기 때문이다. 순수한 계산이나 이미지 내부 상태의 변경은 비교적 쉽게 되돌리거나 다시 실행할 수 있지만, 외부 시스템과 연결된 작업은 별도의 트랜잭션과 복구 설계가 필요할 수 있다.
오류에서 코드 작성하기
Smalltalk의 라이브 환경에서는 아직 구현되지 않은 메시지를 의도적으로 보내고, 발생한 오류에서 필요한 메서드를 작성하는 방식으로 개발할 수 있다.
counter incrementBy: 5
Counter가 incrementBy:를 구현하지 않았다면 메시지 탐색은 실패하고 doesNotUnderstand: 오류가 발생한다. 디버거에서 새 메서드를 작성할 수 있다.
Counter >> incrementBy: anAmount
count := count + anAmount.
^ count
메서드를 컴파일한 뒤 실패한 메시지를 재실행하면 기존 counter 객체와 그 상태를 유지한 채 작업을 계속할 수 있다.
이 방식은 테스트 주도 개발과 결합된다.
CounterTest >> testIncrementBy
| counter |
counter := Counter new.
counter incrementBy: 5.
self assert: counter count equals: 5
처음에는 테스트가 메시지 이해 실패나 예상값 불일치로 실패할 수 있다. 개발자는 실패 상태에서 필요한 메서드를 추가하고 테스트를 다시 실행한다. Smalltalk 환경에서는 테스트, 디버거, 브라우저와 실행 객체 사이의 이동이 하나의 연속적인 작업 흐름으로 구성된다.
변경 기록과 코드 관리
이미지 안에서 직접 메서드를 수정할 수 있으므로 변경 내용을 이미지 파일에만 보존하면 손상이나 실수로 작업을 잃을 수 있다. 전통적인 Smalltalk 환경은 이를 보완하기 위해 변경 기록을 유지했다.
Smalltalk 시스템은 평가하거나 컴파일한 코드와 클래스 변경을 변경 로그 파일에 순차적으로 기록할 수 있다. 이미지가 저장되기 전에 종료되거나 손상되더라도 변경 로그에서 최근 코드를 찾아 다시 적용할 수 있다.
변경 기록에는 다음과 같은 작업이 포함될 수 있다.
- 새 클래스 생성
- 클래스 정의 변경
- 메서드 추가와 수정
- 메서드 삭제
- 코드 평가
- 클래스 이름 변경
- 패키지 이동
Squeak과 전통적인 Smalltalk 환경에서는 .changes 파일이 이미지에서 수행한 소스 변경을 기록하고, .sources 파일이 기본 시스템 클래스의 소스를 제공하는 구조가 널리 사용되었다. 이미지 안의 컴파일된 메서드는 원래 소스 위치를 가리키는 정보를 통해 소스 파일의 내용을 브라우저에 표시할 수 있다.
변경 집합은 서로 관련된 클래스와 메서드 변경을 한 묶음으로 내보내고 다른 이미지에 적용하는 전통적인 배포 단위이다. 파일 아웃과 파일 인 기능을 사용하면 클래스나 메서드를 텍스트 표현으로 저장하고 다른 Smalltalk 환경에서 불러올 수 있다.
현대 Smalltalk 프로젝트에서는 패키지 단위의 소스 관리와 Git 연동을 함께 사용한다. Pharo의 Iceberg는 이미지 안의 패키지와 Git 저장소를 연결하며, 브랜치, 커밋, 병합과 변경 비교 작업을 Smalltalk 환경 안에서 수행할 수 있다. Pharo by Example은 Browser, Inspector, Debugger와 함께 Iceberg를 핵심 개발 도구로 다룬다.[41]
이미지와 텍스트 저장소는 서로 다른 역할을 담당한다.
- 이미지는 현재 실행 가능한 객체 상태와 개발 세션을 보존한다.
- 변경 로그는 이미지 안에서 수행된 작업을 순차적으로 기록한다.
- 패키지는 관련 클래스와 메서드를 논리적으로 묶는다.
- Git 저장소는 공유 가능한 소스와 변경 이력을 관리한다.
- 자동화 스크립트는 빈 이미지나 기본 이미지에서 개발 환경을 재구성한다.
현대적인 Smalltalk 개발에서는 특정 개발자의 이미지 파일 자체를 공동 저장소의 유일한 원본으로 사용하기보다, 패키지 소스를 버전 관리하고 이미지를 실행·시험 환경으로 사용하는 방식이 일반적이다.
코드 탐색과 리팩터링
Smalltalk 환경은 클래스 이름과 파일 경로만으로 코드를 탐색하지 않는다. 객체에 전송되는 선택자와 클래스 계층, 프로토콜, 패키지와 실행 중인 참조 관계를 함께 활용한다.
대표적인 탐색 기능은 다음과 같다.
- 클래스 이름 검색
- 선택자 검색
- 구현자 검색
- 송신자 검색
- 클래스 참조 검색
- 인스턴스 변수 참조 검색
- 하위 클래스 탐색
- 메서드 계층 보기
- 리터럴과 문자열 검색
- 최근 변경 메서드 보기
메서드 계층 보기는 같은 선택자가 상속 계층에서 어떻게 구현되고 재정의되는지를 보여 준다. 예를 들어 printOn:의 구현을 탐색하면 Object에서 시작해 문자열, 컬렉션과 사용자 정의 클래스가 이 메시지를 어떻게 구체화하는지 비교할 수 있다.
리팩터링 도구는 반영 가능한 클래스와 메서드 모델을 사용해 코드를 구조적으로 변경한다. 단순한 문자열 치환이 아니라 선택자, 변수, 클래스와 구문 트리의 관계를 분석할 수 있다.
대표적인 리팩터링은 다음과 같다.
- 클래스 이름 변경
- 메서드 이름 변경
- 인스턴스 변수 이름 변경
- 메서드 추출
- 임시 변수 추출
- 메서드를 다른 클래스로 이동
- 상위 클래스로 메서드 이동
- 하위 클래스로 메서드 이동
- 접근자 생성
- 사용하지 않는 클래스와 메서드 탐지
메서드 선택자를 변경할 때 리팩터링 도구는 해당 선택자의 송신자와 구현자를 찾아 함께 수정할 수 있다. 그러나 동적으로 생성된 선택자, 문자열에서 변환한 Symbol, perform:와 doesNotUnderstand:를 사용하는 코드는 정적 탐색만으로 완전히 찾기 어려울 수 있다.
테스트 도구
Smalltalk의 테스트 도구는 일반적으로 SUnit 계열의 단위 테스트 프레임워크를 중심으로 한다. 테스트 클래스와 테스트 메서드도 일반 클래스와 메서드이므로 브라우저에서 편집하고 실행할 수 있다.
CounterTest >> testIncrement
| counter |
counter := Counter new.
counter increment.
self assert: counter count equals: 1
테스트 실행기는 패키지, 클래스 또는 개별 메서드 단위로 테스트를 실행하고 성공, 실패와 오류를 구분한다.
- 성공은 모든 검증이 통과한 경우이다.
- 실패는
assert:와 같은 검증이 만족되지 않은 경우이다. - 오류는 예상하지 못한 예외가 발생한 경우이다.
실패한 테스트는 디버거에서 다시 실행할 수 있다. 개발자는 테스트가 구성한 객체와 실행 문맥을 그대로 조사하면서 구현을 수정한다.
self
should: [ account withdraw: 200 ]
raise: InsufficientFunds
테스트 도구는 라이브 개발 환경과 결합되어 다음 흐름을 만든다.
- 브라우저에서 테스트를 작성한다.
- 개별 테스트를 실행한다.
- 실패한 상태로 디버거를 연다.
- 실제 객체와 호출 스택을 조사한다.
- 구현 메서드를 수정한다.
- 해당 실행 문맥이나 테스트를 다시 실행한다.
- 관련 테스트 전체를 실행한다.
Smalltalk에서는 테스트 코드와 응용 프로그램 코드가 같은 이미지 안에서 실행되므로 테스트가 만든 객체를 검사기와 디버거에서 직접 다룰 수 있다.
프로파일러와 실행 분석
Smalltalk 구현체는 실행 시간과 메시지 전송을 분석하기 위한 프로파일러를 제공한다. 프로파일러는 일정 시간 동안 실행 스택을 표본화하거나 메서드 호출을 계측하여 비용이 큰 메서드와 호출 관계를 찾는다.
TimeProfiler spyOn: [
application processAll
]
실제 프로파일러 클래스와 API는 구현체마다 다르다.
프로파일 결과는 다음 정보를 제공할 수 있다.
- 메서드별 실행 시간
- 호출 횟수
- 전체 시간과 자체 실행 시간
- 호출자와 피호출자 관계
- 프로세스별 활동
- 객체 할당량
- 가비지 컬렉션 시간
객체 할당과 메모리 분석 도구는 특정 클래스의 인스턴스 수와 객체가 메모리에 남아 있는 이유를 조사할 수 있다. 이미지 안의 객체는 다른 객체의 참조로 인해 생존하므로, 참조 경로 탐색은 메모리 누수에 가까운 문제를 찾는 데 중요하다.
BankAccount allInstances
allInstances와 같은 반영 기능은 특정 클래스의 현재 인스턴스를 찾는 데 사용할 수 있다. 다만 전체 객체 메모리를 탐색하는 작업은 비용이 클 수 있고, 구현체와 객체 수에 따라 시스템 실행에 영향을 줄 수 있다.
이미지 기반 개발 방식
Smalltalk의 라이브 개발 도구는 객체 이미지와 결합되어 있다. 브라우저에서 수정한 클래스, 작업 공간에서 생성한 객체, 검사기에서 보고 있던 상태와 열려 있는 도구가 이미지의 일부로 저장될 수 있다.
이 구조에서는 개발 세션 자체가 지속된다. 이미지를 다시 시작하면 저장 당시의 클래스와 객체, 작업 중이던 도구를 이어서 사용할 수 있다.
이미지 기반 개발은 다음과 같은 작업 흐름을 가능하게 한다.
application := Application new.
application loadData.
application open
개발자는 실행 중인 application 객체를 검사하고, 메서드를 수정하며, 상태를 유지한 채 새 동작을 시험한다. 작업이 끝나면 객체 세계 전체를 이미지로 저장할 수 있다.
이미지 안에는 실험용 객체, 디버깅 상태와 불필요한 개발 도구도 남을 수 있다. 배포 이미지를 만들 때는 사용하지 않는 패키지와 개발 도구를 제거하거나, 깨끗한 기본 이미지에서 필요한 패키지만 다시 로드하는 방식을 사용할 수 있다.
현대 Smalltalk 프로젝트에서는 다음 두 방향을 함께 사용한다.
- 장기간 유지되는 개발 이미지에서 객체 상태를 이어 가며 개발한다.
- 선언된 패키지 의존성과 자동화 스크립트로 깨끗한 이미지를 재구성한다.
첫 번째 방식은 즉각적인 실험과 지속적인 객체 환경에 유리하다. 두 번째 방식은 재현 가능한 빌드, 지속적 통합과 팀 협업에 유리하다.
Pharo Launcher와 같은 도구는 기본 이미지와 가상 머신을 내려받고 여러 개발 이미지를 관리한다. Pharo는 이미지를 핵심 라이브러리를 포함하는 객체 공간으로 설명하며, Launcher가 이미지와 적절한 가상 머신을 함께 관리하도록 한다.[42]
도구 자체의 확장
Smalltalk의 개발 도구 역시 Smalltalk로 구현된 객체이므로 일반 응용 프로그램과 같은 방법으로 조사하고 수정할 수 있다. 브라우저에서 브라우저의 클래스를 열고, 검사기로 검사기 모델을 살펴보며, 디버거에서 디버거 자체의 오류를 디버깅할 수 있다.
이러한 자기 적용 구조는 다음과 같은 확장을 가능하게 한다.
- 새로운 브라우저 보기 추가
- 도메인 객체용 검사 표현 추가
- 코드 분석 규칙 추가
- 리팩터링 기능 추가
- 디버거 명령 추가
- 테스트 결과 표시 변경
- 패키지 관리 도구 통합
- 원격 객체와 데이터베이스용 탐색 도구 구성
예를 들어 도메인 객체가 전용 검사 표현을 제공하면 일반 인스턴스 변수 목록 대신 의미 있는 시각화로 표시할 수 있다. 그래프 객체는 노드와 간선을 그릴 수 있고, 이미지 객체는 실제 그림을 표시하며, 구문 트리는 계층 구조로 나타낼 수 있다.
개발 환경을 구성하는 모델, 화면 요소와 명령도 객체와 메시지로 구현되므로, 도구 확장은 별도의 플러그인 언어나 네이티브 API에만 의존하지 않는다.
라이브 환경의 범위
Smalltalk의 라이브 프로그래밍은 이미지 안의 객체와 메서드를 즉시 바꾸는 데 특히 강하다. 그러나 실행 환경 밖의 모든 상태가 자동으로 되돌아가거나 수정되는 것은 아니다.
다음 요소는 이미지 내부에서 직접 조사하고 변경하기 쉽다.
- 클래스와 메서드
- 인스턴스와 인스턴스 변수
- 컬렉션과 객체 그래프
- 실행 문맥
- Smalltalk 프로세스
- 개발 도구 상태
다음 요소는 외부 시스템과의 연결을 별도로 고려해야 한다.
- 파일 시스템 변경
- 네트워크 요청
- 데이터베이스 트랜잭션
- 운영체제 프로세스
- 외부 라이브러리 상태
- 하드웨어 장치
- 원격 서비스
메서드를 디버거에서 다시 시작하더라도 이미 전송한 네트워크 요청이나 기록한 파일이 자동으로 취소되지는 않는다. 따라서 외부 효과가 있는 코드는 멱등성, 트랜잭션, 재시도와 보상 작업을 고려해 설계할 수 있다.
Smalltalk의 라이브 환경은 코드 변경과 객체 상태의 피드백 주기를 매우 짧게 만들지만, 소스 버전 관리와 재현 가능한 배포를 대신하지는 않는다. 이미지, 패키지 저장소, 테스트와 자동화된 빌드를 함께 사용할 때 라이브 개발의 장점을 팀 개발과 운영 환경까지 연결할 수 있다.
Smalltalk의 개발 방식에서 핵심은 프로그램을 완성된 소스 문서로만 바라보지 않는다는 데 있다. 프로그램은 현재 실행되고 있는 객체들의 시스템이며, 브라우저, 작업 공간, 검사기와 디버거는 그 시스템을 서로 다른 관점에서 보고 수정하는 창으로 동작한다.
표준 라이브러리와 프로그래밍 모델
Smalltalk의 표준 라이브러리는 객체를 기능별 유틸리티 함수에 전달하는 방식보다, 객체가 공통된 메시지 프로토콜에 응답하도록 구성된다. 수, 문자열, 컬렉션, 스트림, 예외와 프로세스는 모두 객체이며, 각각의 클래스가 자신의 상태와 동작을 관리한다.
예를 들어 컬렉션을 순회하는 기능은 외부 반복 함수가 아니라 컬렉션 객체에 보내는 do: 메시지로 표현된다.
items do: [ :each |
Transcript show: each printString; cr
]
문자열을 변환하고, 스트림에서 값을 읽고, 예외를 처리하며, 프로세스를 시작하는 작업도 같은 메시지 전송 모델을 따른다.
name asUppercase.
stream next.
[ self loadData ] on: Error do: [ :error | error return ].
[ self runWorker ] fork
Smalltalk 구현체들은 Smalltalk-80에서 이어진 핵심 클래스와 프로토콜을 공유하지만, 구체적인 클래스 계층과 메서드 이름, 파일·네트워크·그래픽 인터페이스에는 차이가 있다. ANSI Smalltalk 표준은 수, 컬렉션, 스트림, 파일과 예외 처리에 필요한 공통 프로토콜을 정의했지만, 현대 구현체가 제공하는 전체 라이브러리와 개발 환경을 모두 규정하지는 않는다.[43]
라이브러리와 프로토콜
Smalltalk에서 프로토콜은 객체가 이해하는 관련 메시지와 그 동작상의 계약을 뜻한다. 프로토콜은 반드시 별도의 정적 인터페이스 선언으로 존재하지 않으며, 여러 클래스가 같은 메시지 집합을 구현함으로써 공유할 수 있다.
예를 들어 다음 메서드는 인자가 정확히 어느 컬렉션 클래스인지 지정하지 않는다.
Report >> printItemsFrom: aCollection
aCollection do: [ :each |
self printItem: each
]
aCollection이 do: 메시지에 응답하여 요소를 블록에 전달할 수 있다면 배열, 집합, 순서 있는 컬렉션이나 다른 사용자 정의 객체도 사용할 수 있다.
ANSI Smalltalk 표준은 클래스의 구체적인 구현보다 객체가 따라야 할 프로토콜을 중심으로 라이브러리를 기술한다. 예를 들어 컬렉션 계통은 읽기 가능한 컬렉션, 확장 가능한 컬렉션, 순서 있는 컬렉션, 사전과 집합 등의 프로토콜로 나뉜다. 스트림도 읽기 가능한 스트림, 쓰기 가능한 스트림과 위치 이동이 가능한 스트림 같은 역할로 구분된다.[44]
프로토콜 중심 구조에서는 알고리즘을 하나의 구체적인 클래스에 묶지 않고, 필요한 메시지를 제공하는 여러 객체에 재사용할 수 있다.
Processor >> repeat: aValuableObject
10 timesRepeat: [
aValuableObject value
]
aValuableObject는 반드시 특정 블록 클래스의 인스턴스일 필요 없이 value 메시지를 이해하면 된다. 실제 Smalltalk 코드에서는 일반적으로 코드 블록을 전달하지만, 같은 프로토콜을 제공하는 다른 객체를 사용할 수도 있다.
이러한 방식은 라이브러리의 서로 다른 부분을 연결한다.
- 컬렉션은 반복 작업을 코드 블록에 위임한다.
- 스트림은 순차 접근 프로토콜을 제공한다.
- 예외 처리는 보호할 코드와 처리 코드를 블록으로 받는다.
- 프로세스는 실행할 블록에서 생성될 수 있다.
- 그래픽 객체는 그리기 대상과 입력 사건을 메시지로 처리한다.
따라서 코드 블록은 컬렉션, 제어 구조, 예외 처리와 동시성 전반을 연결하는 핵심 라이브러리 객체로 사용된다.
컬렉션
컬렉션은 여러 객체를 하나의 객체로 관리하는 자료구조이다. Smalltalk의 컬렉션 계층은 요소의 순서, 중복 허용 여부, 키 기반 접근, 크기 변경 가능 여부와 저장 표현에 따라 여러 클래스로 나뉜다.
대표적인 컬렉션에는 다음과 같은 종류가 있다.
| 종류 | 대표 클래스 | 특성 |
|---|---|---|
| 고정 길이 순차 컬렉션 | Array | 정수 인덱스로 접근하며 일반적으로 크기가 고정된다. |
| 가변 길이 순차 컬렉션 | OrderedCollection | 요소를 앞이나 뒤에 추가·제거할 수 있다. |
| 중복 없는 컬렉션 | Set | 같은 원소를 하나만 보관하며 순서를 보장하지 않는다. |
| 정렬 컬렉션 | SortedCollection | 정렬 규칙에 따라 요소를 유지한다. |
| 키와 값의 대응 | Dictionary | 키를 사용하여 값에 접근한다. |
| 개수 기반 집합 | Bag | 원소별 출현 횟수를 관리한다. |
| 정수 범위 | Interval | 시작값, 끝값과 증가 폭으로 범위를 표현한다. |
| 문자열 | String | 문자의 순차 컬렉션으로 동작한다. |
| 바이트 배열 | ByteArray | 바이트 값을 순서대로 저장한다. |
컬렉션은 모두 같은 저장 구조를 사용하지 않는다. Array는 위치별 슬롯을 가지며, Set과 Dictionary는 일반적으로 해시 기반 구조를 사용한다. Interval은 모든 정수를 각각 저장하지 않고 시작값, 종료값과 간격으로 범위를 표현할 수 있다.
array := #(10 20 30).
ordered := OrderedCollection new.
set := Set new.
dictionary := Dictionary new
가변 컬렉션에는 add:로 요소를 추가할 수 있다.
ordered
add: 'Smalltalk';
add: 'Squeak';
add: 'Pharo'
요소 제거에는 remove: 계열 메시지를 사용한다.
ordered remove: 'Squeak'
컬렉션의 크기와 비어 있는 상태는 공통 메시지로 조사할 수 있다.
ordered size.
ordered isEmpty.
ordered notEmpty
순서가 있는 컬렉션은 일반적으로 at: 메시지로 위치의 요소를 읽는다. Smalltalk 컬렉션의 인덱스는 전통적으로 1부터 시작한다.
array at: 1
array at: 2 put: 25
리터럴 배열은 컴파일된 메서드가 공유하는 리터럴 객체일 수 있으므로 변경 가능한 작업에는 실행 중 새 배열을 생성하는 편이 적합하다.
array := Array new: 3.
array
at: 1 put: 10;
at: 2 put: 20;
at: 3 put: 30
사전은 임의의 키를 사용해 값에 접근한다.
user := Dictionary new.
user
at: #name put: 'Alan';
at: #language put: 'Smalltalk'
user at: #name
키가 없을 때 실행할 블록을 전달할 수 있다.
user
at: #email
ifAbsent: [ '등록되지 않음' ]
키가 없으면 새 값을 계산하여 등록하는 형태도 구현체별 컬렉션 프로토콜에서 널리 사용된다.
cache
at: key
ifAbsentPut: [ self computeValueFor: key ]
집합은 요소의 중복을 제거하는 데 사용할 수 있다.
#(1 2 2 3 3 3) asSet
정렬 컬렉션은 기본 비교 또는 전달된 정렬 블록에 따라 요소를 유지한다.
users := SortedCollection
sortBlock: [ :left :right |
left name <= right name
]
컬렉션 순회
Smalltalk의 컬렉션 프로그래밍은 외부에서 인덱스를 증가시키는 반복문보다 컬렉션 객체에 블록을 전달하는 방식이 중심이다.
do:는 각 요소에 대해 블록을 실행한다.
users do: [ :user |
user update
]
collect:는 각 요소를 변환한 결과로 새 컬렉션을 만든다.
names := users collect: [ :user |
user name
]
select:는 조건을 만족하는 요소만 골라낸다.
activeUsers := users select: [ :user |
user isActive
]
reject:는 조건을 만족하지 않는 요소를 선택한다.
visibleUsers := users reject: [ :user |
user isHidden
]
detect:는 조건을 만족하는 첫 번째 요소를 찾는다.
administrator := users
detect: [ :user | user isAdministrator ]
ifNone: [ nil ]
anySatisfy:와 allSatisfy:는 조건을 만족하는 요소가 존재하는지 또는 모든 요소가 조건을 만족하는지 검사한다.
users anySatisfy: [ :user | user isAdministrator ].
users allSatisfy: [ :user | user isValid ]
inject:into:는 누적값과 각 요소를 결합하여 하나의 결과를 만든다.
total := prices
inject: 0
into: [ :sum :price |
sum + price
]
여러 고차 메시지를 연속해서 조합할 수도 있다.
total := orders
select: [ :order | order isPaid ];
collect: [ :order | order total ];
inject: 0
into: [ :sum :amount | sum + amount ]
다만 위 캐스케이드는 같은 수신자를 대상으로 하므로, 실제 변환 결과를 연속해서 넘기려면 괄호나 중간 변수를 사용해야 한다.
paidOrders := orders select: [ :order | order isPaid ].
amounts := paidOrders collect: [ :order | order total ].
total := amounts
inject: 0
into: [ :sum :amount | sum + amount ]
또는 메시지 중첩으로 작성할 수 있다.
total := ((orders select: [ :order | order isPaid ])
collect: [ :order | order total ])
inject: 0
into: [ :sum :amount | sum + amount ]
컬렉션 메시지는 결과 컬렉션의 구체적인 클래스를 가능한 범위에서 수신자와 비슷하게 유지할 수 있지만, 구현체와 연산의 의미에 따라 반환 종류가 달라질 수 있다. 예를 들어 문자열에 select:를 보내면 문자열을 반환할 수 있고, 사전에 적용한 연산은 키와 값의 관계 때문에 별도의 의미를 가질 수 있다.[45]
수와 산술
Smalltalk에서 수는 원시 자료형이 아니라 메시지를 받는 객체이다. 정수, 분수, 부동소수점 수와 고정 소수점 수는 공통 수 프로토콜을 제공하며, 산술 연산자는 이항 메시지로 구현된다.
10 + 5.
10 - 5.
10 * 5.
10 / 5
정수 나눗셈의 /는 나누어떨어지지 않을 때 정수 부분만 남기는 대신 정확한 분수 객체를 반환할 수 있다.
1 / 3
1 / 3은 일반적으로 Fraction 계열 객체가 되며, 이후 산술에서도 가능한 범위에서 정확한 유리수 값을 유지한다.
(1 / 3) + (1 / 6)
몫과 나머지가 필요한 경우 별도의 메시지를 사용한다.
17 // 5.
17 \\ 5
일반적으로 //는 바닥 나눗셈의 몫을, \\는 그 나눗셈에 대응하는 나머지를 반환한다.
숫자 객체는 크기 비교 프로토콜을 제공한다.
3 < 5.
10 between: 1 and: 20.
-4 negative.
0 isZero
수학 함수 역시 숫자 객체에 보내는 메시지로 표현된다.
16 sqrt.
5 factorial.
2 raisedTo: 10.
Float pi sin.
-10 abs
숫자는 반복 범위를 표현하는 역할도 한다.
5 timesRepeat: [
Transcript show: 'Smalltalk'; cr
]
1 to: 10 do: [ :index |
Transcript show: index printString; cr
]
0 to: 100 by: 10 do: [ :value |
Transcript show: value printString; cr
]
to:와 to:by:는 Interval 객체를 만들 수 있다.
range := 1 to: 10.
range collect: [ :each | each squared ]
Smalltalk의 수 계층은 서로 다른 수 표현 사이의 연산을 객체와 메시지 수준에서 처리한다. 일부 전통적인 구현에서는 이중 디스패치와 수 변환 프로토콜을 사용하여 정수와 분수, 부동소수점 수 사이의 연산을 구현한다.
3 + 2.5
이때 정수 객체의 비트 표현을 임의로 부동소수점 값으로 해석하는 것이 아니라, 해당 숫자 클래스들이 정의한 산술 프로토콜을 통해 결과 객체를 계산한다. ANSI Smalltalk 표준은 정수, 분수, 부동소수점 수와 ScaledDecimal을 포함한 숫자 프로토콜을 정의한다.[46]
문자와 문자열
Character는 하나의 문자를 나타내는 객체이며, String은 문자의 순차 컬렉션으로 동작한다.
$S
'Smalltalk'
문자열의 길이와 문자 접근에는 일반적인 컬렉션 메시지를 사용할 수 있다.
'Smalltalk' size.
'Smalltalk' at: 1
문자열 결합에는 , 메시지를 사용한다.
'Small' , 'talk'
문자열의 대소문자 변환, 검색과 분리는 구현체의 문자열 프로토콜로 제공된다.
'Smalltalk' asUppercase.
'SMALLTALK' asLowercase
'Smalltalk' includesSubstring: 'talk'
'Smalltalk is live' substrings
문자열을 다른 객체로 변환하거나 다른 객체의 문자열 표현을 얻을 수 있다.
'2026' asInteger.
42 printString.
#Smalltalk asString
printString은 객체의 개발자용 또는 저장 가능한 문자열 표현을 얻는 데 널리 사용된다. 실제 출력 과정은 객체가 스트림에 자신의 표현을 쓰는 printOn: 프로토콜을 통해 구현되는 경우가 일반적이다.
BankAccount >> printOn: aStream
super printOn: aStream.
aStream
nextPutAll: ' balance: ';
print: balance
이후 다음 표현식은 사용자 정의 표현을 반환할 수 있다.
account printString
사용자에게 표시할 문자열과 개발 도구에서 객체를 식별하기 위한 문자열은 구현체에 따라 별도의 프로토콜로 구분될 수 있다.
문자열은 컬렉션이므로 collect:, select:와 do:를 사용할 수 있다.
'Smalltalk' collect: [ :character |
character asUppercase
]
문자 인코딩과 문자열 내부 표현은 구현체와 버전에 따라 다르다. 초기 Smalltalk 시스템은 제한된 문자 집합을 사용했지만 현대 구현체는 유니코드 문자를 지원하고, 문자열 종류를 바이트 문자열과 넓은 문자 문자열 등으로 내부적으로 구분할 수 있다.
문자열은 변경 가능한 컬렉션으로 구현된 계통도 있지만, 코드에서는 문자열 리터럴을 직접 변경하기보다 필요한 결과를 새 문자열로 만드는 방식이 안전하다. Symbol은 문자열과 비슷한 이름 값을 나타내지만 일반적으로 변경할 수 없고 동일한 내용의 Symbol이 공유되는 식별 객체로 사용된다.
스트림
스트림은 요소의 저장 위치나 전체 자료구조를 직접 다루지 않고, 순차적으로 값을 읽거나 쓰는 객체이다. 문자열, 배열, 파일, 네트워크 연결과 메모리 버퍼 등 서로 다른 대상에 공통된 순차 접근 프로토콜을 제공한다.
읽기 스트림은 현재 위치에서 다음 요소를 반환한다.
stream := ReadStream on: #(10 20 30).
stream next.
stream next.
stream atEnd
일반적인 읽기 메시지는 다음과 같다.
next는 다음 요소 하나를 읽는다.next:는 지정한 수만큼 요소를 읽는다.peek는 위치를 이동하지 않고 다음 요소를 본다.atEnd는 더 읽을 요소가 없는지 확인한다.position은 현재 위치를 반환한다.position:은 지원되는 경우 현재 위치를 변경한다.upTo:는 특정 구분자를 만날 때까지 읽는다.contents는 스트림이 나타내는 전체 또는 작성된 내용을 반환한다.
[ stream atEnd ] whileFalse: [
Transcript show: stream next printString; cr
]
쓰기 스트림은 nextPut:와 nextPutAll:로 값을 기록한다.
stream := WriteStream on: String new.
stream
nextPutAll: 'Smalltalk';
space;
nextPutAll: 'stream'
작성한 결과는 contents로 얻을 수 있다.
stream contents
여러 문자열을 반복해서 ,로 연결하면 중간 문자열 객체가 계속 생성될 수 있다. 쓰기 스트림은 하나의 버퍼에 내용을 순차적으로 기록하여 긴 문자열이나 직렬화 결과를 구성하는 데 적합하다.
result := String streamContents: [ :output |
users
do: [ :user | output nextPutAll: user name ]
separatedBy: [ output nextPutAll: ', ' ]
]
streamContents:와 separatedBy:의 제공 여부와 구체적인 위치는 구현체에 따라 다르지만, 스트림과 블록을 결합해 결과를 만드는 방식은 현대 Smalltalk에서 널리 사용된다.
객체의 출력 프로토콜도 스트림을 사용한다.
stream
print: account;
cr;
print: Date today
파일 스트림은 같은 읽기·쓰기 프로토콜을 파일에 연결한다.
stream := fileReference readStream.
[
[ stream atEnd ] whileFalse: [
self processLine: stream nextLine
]
] ensure: [
stream close
]
스트림은 내부적으로 현재 위치를 기준으로 과거 요소와 아직 읽지 않은 미래 요소를 나누는 순차 접근 모델로 볼 수 있다. Pharo의 스트림 설명도 스트림을 현재 위치가 두 요소 순서 사이에 놓인 구조로 설명한다.[47]
ANSI Smalltalk은 읽기 가능한 스트림, 쓰기 가능한 스트림, 양방향 스트림과 파일 스트림에 대한 공통 프로토콜을 정의한다. 실제 파일 경로 객체, 문자 인코딩, 버퍼링과 비동기 입출력 기능은 구현체마다 확장된다.[48]
예외 처리
Smalltalk의 예외는 실행 중 발생한 비정상 조건을 나타내는 객체이다. 예외 클래스는 계층을 구성하며, 블록에 보호할 코드를 담고 처리할 예외 종류와 처리 블록을 메시지로 전달한다.
[
self loadConfiguration
]
on: Error
do: [ :error |
Transcript
show: error messageText;
cr
]
on:do:는 보호 블록을 실행하고 지정된 예외 계층에 속한 예외가 발생하면 처리 블록을 호출한다. 처리 블록의 인자는 발생한 예외 객체이다.
구체적인 예외만 처리할 수도 있다.
[
account withdraw: amount
]
on: InsufficientFunds
do: [ :error |
self showWarning: error messageText
]
예외 객체에는 메시지, 발생 위치와 예외 계층에 따른 추가 상태가 포함될 수 있다.
InsufficientFunds
signal: '잔액이 부족하다'
사용자 정의 예외는 일반적으로 적절한 예외 클래스의 하위 클래스로 만든다.
Error subclass: #InsufficientFunds
instanceVariableNames: 'account requestedAmount'
classVariableNames: ''
package: 'Banking-Exceptions'
예외 처리기는 단순히 오류를 기록하고 종료하는 것 외에도 여러 동작을 선택할 수 있다. 구현체와 예외 종류에 따라 다음과 같은 메시지가 제공될 수 있다.
return은 보호 블록의 결과로 복귀한다.return:은 지정한 객체를 결과로 반환한다.retry는 보호 블록을 다시 실행한다.resume또는resume:은 예외를 발생시킨 지점에서 실행을 재개한다.pass는 바깥쪽 처리기가 예외를 처리하도록 넘긴다.outer는 바깥 처리기를 실행한 뒤 현재 처리로 돌아오는 계통에서 사용될 수 있다.
result := [
self readOptionalFile
]
on: FileDoesNotExist
do: [ :error |
error return: ''
]
[
connection send: request
]
on: ConnectionTimedOut
do: [ :error |
self reconnect.
error retry
]
Smalltalk 예외 체계의 특징 중 하나는 일부 예외가 재개 가능하다는 점이다. 예외를 처리한 뒤 예외가 발생한 계산으로 값을 전달하고 실행을 이어갈 수 있다. 모든 예외가 재개 가능하거나 모든 상황에서 재개가 적절한 것은 아니며, 구체적인 의미는 예외 클래스와 구현체의 규약에 따른다.[49]
정리 작업에는 ensure:를 사용한다.
stream := fileReference writeStream.
[
stream nextPutAll: content
] ensure: [
stream close
]
보호 블록이 정상적으로 끝나거나 예외, 비지역 반환으로 중단되어도 정리 블록은 실행된다.
특정 예외가 발생하지 않은 경우에만 후속 작업을 수행하는 ifCurtailed: 계열 프로토콜도 제공될 수 있다.
[
self performOperation
] ifCurtailed: [
self rollback
]
예외는 디버거와도 연결된다. 처리되지 않은 예외는 현재 프로세스를 중단하고 실행 문맥을 보존한 디버거를 열 수 있다. 개발자는 예외 객체, 호출 스택과 관련 객체를 조사하고 코드를 수정한 뒤 실행을 다시 시도할 수 있다.
ANSI Smalltalk은 예외 신호, 처리기 탐색, 복귀와 재개에 필요한 공통 예외 프로토콜을 정의한다. 구현체는 그 위에 파일, 네트워크, 파싱, 사용자 인터페이스와 시스템 오류에 대한 구체적인 예외 계층을 제공한다.[50]
프로세스와 동시성
전통적인 Smalltalk 시스템에서 프로세스는 가상 머신 안에서 실행되는 경량 실행 단위이다. 코드 블록에 fork 메시지를 보내 새로운 프로세스를 시작할 수 있다.
[
self runBackgroundTask
] fork
우선순위를 지정할 수도 있다.
[
self updateDisplay
] forkAt: Processor userBackgroundPriority
실제 우선순위 상수와 권장 사용 방식은 구현체마다 다르다.
Smalltalk 프로세스는 일반적으로 운영체제 스레드와 일대일로 대응하지 않는다. 가상 머신의 스케줄러가 여러 Smalltalk 프로세스를 관리하며, 전통적인 구현에서는 우선순위가 높은 실행 가능한 프로세스를 선택하는 방식이 사용된다.
현재 프로세스는 다른 프로세스가 실행될 기회를 주기 위해 양보할 수 있다.
Processor yield
일정 시간 동안 실행을 지연할 수 있다.
(Delay forMilliseconds: 500) wait
프로세스 사이의 동기화에는 Semaphore를 사용할 수 있다. 세마포어를 기다리는 프로세스는 신호가 올 때까지 실행 가능 상태에서 제외된다.
semaphore := Semaphore new.
[
result := self computeResult.
semaphore signal
] fork.
semaphore wait.
self consume: result
여러 프로세스가 공유 상태를 변경할 때는 상호 배제를 구성할 수 있다.
mutex := Semaphore forMutualExclusion.
mutex critical: [
sharedCounter := sharedCounter + 1
]
critical:은 블록을 실행하는 동안 같은 상호 배제 객체를 사용하는 다른 프로세스가 임계 구역에 들어오지 못하게 한다.
한 번만 값을 전달하는 동기화 객체나 생산자·소비자 큐를 사용하는 구현체도 있다.
queue := SharedQueue new.
[
queue nextPut: self produceValue
] fork.
value := queue next
SharedQueue는 구현체별 확장 클래스일 수 있으며 ANSI Smalltalk의 최소 공통 라이브러리에는 포함되지 않을 수 있다.
전통적인 단일 VM 스레드 기반 Smalltalk에서는 Smalltalk 프로세스들이 같은 객체 메모리를 공유하고 한 시점에 하나의 프로세스가 Smalltalk 바이트코드를 실행하는 방식이 일반적이다. 최신 구현체는 외부 함수 호출, 비동기 입출력이나 병렬 실행을 위해 운영체제 스레드와 별도 워커를 지원할 수 있다.
Smalltalk의 블록과 프로세스 모델은 제어 흐름을 객체로 다룬다는 언어 설계와 연결된다. 블록은 저장하고 전달할 수 있는 실행 객체이며, fork, newProcess, value 같은 메시지에 따라 현재 프로세스나 새로운 프로세스에서 실행된다.
process := [
self runTask
] newProcess.
process resume
프로세스를 직접 중단하거나 재개하는 기능도 제공될 수 있다.
process suspend.
process resume.
process terminate
이러한 직접 제어는 디버거와 시스템 도구에서 유용하지만, 응용 프로그램의 일반 동기화에는 세마포어, 큐와 메시지 전달 같은 상위 수준 구조를 사용하는 편이 상태를 관리하기 쉽다.
파일과 외부 자원
파일 시스템 API는 구현체마다 크게 다르지만, 일반적으로 파일 또는 경로 객체에서 스트림을 열고 스트림 프로토콜로 내용을 처리한다.
content := fileReference contents
fileReference writeStreamDo: [ :stream |
stream nextPutAll: 'Smalltalk'
]
블록 기반 API는 스트림의 종료를 라이브러리가 보장할 수 있다는 장점이 있다. 직접 스트림을 여는 경우에는 ensure:로 닫기를 보장한다.
stream := fileReference binaryReadStream.
[
self processBytesFrom: stream
] ensure: [
stream close
]
텍스트 스트림은 문자 인코딩과 줄바꿈 처리를 제공할 수 있으며, 이진 스트림은 바이트를 그대로 읽고 쓴다.
bytes := stream next: 16
네트워크 소켓, 데이터베이스 연결과 외부 함수 인터페이스도 일반적으로 객체와 스트림 또는 전용 메시지 프로토콜로 노출된다. 이러한 외부 자원은 가비지 컬렉션만으로 수명 관리를 끝내지 않고 명시적으로 닫거나 트랜잭션을 종료해야 한다.
이미지를 저장하고 재개해도 열린 파일 디스크립터와 네트워크 연결이 자동으로 동일한 외부 자원에 복원되는 것은 아니다. 구현체는 세션 시작·종료 프로토콜을 통해 외부 자원을 다시 연결하거나 무효화할 수 있다.
그래픽 사용자 인터페이스
Smalltalk은 초기부터 언어, 그래픽 시스템과 개발 도구를 하나의 환경으로 결합했다. 전통적인 Smalltalk-80 환경은 비트맵 그래픽, 창, 메뉴, 마우스 입력과 텍스트 편집 도구를 객체와 메시지로 구현했다.
그래픽 사용자 인터페이스 라이브러리는 구현체마다 다르다.
- Smalltalk-80 계열의 초기 환경은 Model–View–Controller 구조를 사용했다.
- Squeak은 Self에서 영향을 받은 Morphic 객체 시스템을 채택했다.
- Pharo는 Morphic 계통을 사용해 왔으며, 현대 버전에서는 Bloc과 Toplo 같은 별도 그래픽·UI 계층도 개발되고 있다.
- VisualWorks는 자체 GUI 프레임워크와 플랫폼 추상화를 제공한다.
- Dolphin Smalltalk는 Windows의 네이티브 UI와 긴밀하게 통합된다.
Smalltalk GUI에서는 화면 요소도 일반 객체이다. 창, 버튼, 텍스트 입력란, 메뉴와 그래픽 도형에 메시지를 보내 상태를 조회하고 변경할 수 있다.
Morphic 계통에서 화면 객체는 일반적으로 Morph 계층의 인스턴스이며, 다른 Morph를 하위 객체로 포함할 수 있다.
morph := Morph new.
morph
extent: 200 @ 100;
color: Color lightBlue;
openInWorld
위 코드의 구체적인 클래스와 메시지 지원은 Squeak·Pharo 버전에 따라 달라질 수 있지만, 그래픽 요소를 실행 중인 객체로 생성하고 즉시 화면에 여는 방식은 Morphic의 기본적인 사용 모델이다.
버튼의 동작도 대상 객체와 메시지 선택자를 연결하는 방식으로 구성할 수 있다.
button
label: '저장';
target: document;
actionSelector: #save
현대 프레임워크에서는 블록 기반 콜백이나 별도의 이벤트 객체를 사용할 수도 있다.
사용자 입력은 이벤트 객체나 메시지로 전달된다.
CanvasMorph >> mouseDown: anEvent
self addPoint: anEvent position
화면 갱신은 객체의 상태를 변경하고 다시 그리기를 요청하는 방식으로 이루어진다.
self changed
또는 Morphic 계통에서는 변경된 영역을 표시하거나 다시 그릴 필요를 알리는 별도의 메시지를 사용한다.
Smalltalk의 GUI 프로그래밍은 실행 중인 객체를 검사하고 변경할 수 있다는 점에서 라이브 환경과 밀접하게 연결된다. 화면에 열린 위젯을 검사기로 열어 상태를 조사하고, 그 클래스의 메서드를 수정한 뒤 기존 객체에 변경된 동작을 적용할 수 있다.
초기 Smalltalk-80의 Model–View–Controller는 응용 프로그램 상태를 가진 모델, 화면 표현을 담당하는 뷰와 입력 처리를 담당하는 컨트롤러를 분리했다. 이후 Smalltalk 공동체에서는 Presentation Model, Application Model, Morphic과 여러 프레임워크가 발전했다.
Smalltalk이 Model–View–Controller를 사용한 초기 환경 가운데 하나였다고 해서 모든 현대 Smalltalk GUI가 동일한 MVC 구조를 강제하는 것은 아니다. 구현체별 프레임워크는 위젯 계층, 이벤트 전달, 레이아웃과 모델 연결을 서로 다른 방식으로 구성한다.
모델과 메시지 중심 설계
Smalltalk의 프로그래밍 모델에서는 라이브러리 객체를 데이터 저장소로만 다루지 않고, 각 객체에 책임을 부여한다. 예를 들어 계좌에서 금액을 출금할 때 외부 코드가 잔액 필드를 직접 읽고 수정하기보다 계좌 객체에 메시지를 보낸다.
account withdraw: 100
계좌 객체는 잔액 확인, 오류 처리와 거래 기록을 내부 프로토콜로 수행한다.
BankAccount >> withdraw: anAmount
anAmount > balance
ifTrue: [
InsufficientFunds signal
].
balance := balance - anAmount.
transactions add: (Withdrawal amount: anAmount).
^ anAmount
이 설계에서는 호출자가 객체의 구체적인 저장 구조를 알 필요가 없다.
라이브러리의 고차 메시지도 같은 방향을 따른다. 호출자가 컬렉션의 내부 인덱스를 관리하는 대신 컬렉션에 순회를 요청하고, 요소별 작업을 블록으로 전달한다.
accounts do: [ :account |
account applyMonthlyInterest
]
이 방식을 내부 반복이라고 할 수 있다. 반복의 진행 방식은 컬렉션이 관리하고 호출자는 각 요소에 수행할 작업만 정의한다.
스트림도 저장 대상의 내부 구조를 직접 조작하지 않고 순차 읽기와 쓰기 책임을 맡는다. 예외 객체는 오류 조건과 처리 흐름을 관리하고, 프로세스와 세마포어는 실행 및 동기화 책임을 맡는다.
따라서 Smalltalk의 표준 라이브러리는 다음 원칙을 일관되게 적용한다.
- 자료와 동작을 객체 안에 함께 둔다.
- 작업은 수신자에게 메시지로 요청한다.
- 구체적인 클래스보다 필요한 프로토콜에 의존한다.
- 가변적인 동작은 코드 블록으로 전달한다.
- 반복과 제어 흐름도 객체의 메시지로 표현한다.
- 공통 메시지를 통해 서로 다른 구현을 교체한다.
- 오류와 실행 상태도 조사 가능한 객체로 다룬다.
블록 중심 프로그래밍
코드 블록은 Smalltalk 표준 라이브러리 전반에서 동작을 전달하는 공통 객체이다. 컬렉션 변환, 지연 평가, 조건문, 반복, 예외 처리, 정리와 프로세스 생성에 사용된다.
condition ifTrue: [ self performAction ].
items collect: [ :each | each transformed ].
[ self riskyOperation ]
on: Error
do: [ :error | self handle: error ].
[ self backgroundTask ] fork
블록은 외부 변수를 포획하는 클로저이므로, 주변 실행 상태를 사용해 동작을 구성할 수 있다.
minimum := 100.
largeOrders := orders select: [ :order |
order total >= minimum
]
라이브러리는 블록이 이해하는 value, value:, value:value: 계열 메시지를 사용하여 필요한 시점에 실행한다.
action := [ :left :right | left + right ].
action value: 10 value: 20
ANSI Smalltalk은 인자 수에 따른 Valuable 프로토콜을 정의하고, 블록을 그 프로토콜의 대표적인 구현으로 다룬다. 이로써 라이브러리 메서드는 구체적인 블록 클래스보다 평가 가능한 객체의 역할을 기준으로 인자를 받을 수 있다.[51]
객체 생성과 초기화
Smalltalk은 언어 수준의 생성자 구문 대신 클래스 객체에 메시지를 보내 인스턴스를 생성한다.
account := BankAccount new
new는 클래스 객체에 보내는 일반 메시지이다. 일반적인 구현에서는 클래스 계통의 new 메서드가 인스턴스를 할당하고 새 객체에 initialize 메시지를 보낸다.
BankAccount >> initialize
super initialize.
balance := 0.
transactions := OrderedCollection new
인자를 받는 생성을 위해 클래스 쪽 팩터리 메서드를 정의할 수 있다.
BankAccount class >> owner: aPerson
^ self new
owner: aPerson;
yourself
account := BankAccount owner: customer
이 방식은 객체 할당 방식과 유효한 초기 상태를 클래스 쪽 프로토콜 안에 캡슐화한다.
컬렉션도 같은 방식으로 생성된다.
Array new: 10.
OrderedCollection new.
Dictionary new.
Set with: 1 with: 2
클래스 자체가 객체이므로 생성 정책을 하위 클래스에서 재정의하거나 다른 클래스 객체에 위임할 수 있다. 다만 initialize를 직접 반복 호출하면 이미 설정된 객체의 상태를 초기화할 수 있으므로 일반적으로 새 객체 생성 과정에서 사용한다.
값 객체와 동일성
Smalltalk에서는 객체의 동일성을 비교하는 것과 값의 동등성을 비교하는 것을 구분한다.
left == right
==는 두 변수가 같은 객체를 참조하는지 비교한다.
left = right
=는 두 객체가 값의 의미에서 동등한지 묻는 메시지이며 클래스가 재정의할 수 있다.
Point >> = anotherPoint
self class = anotherPoint class
ifFalse: [ ^ false ].
^ x = anotherPoint x
and: [ y = anotherPoint y ]
해시 기반 컬렉션에서 값 동등성을 재정의할 때는 hash도 같은 동등성 규칙에 맞게 구현해야 한다.
Point >> hash
^ x hash bitXor: y hash
a = b인 두 객체는 같은 해시 값을 반환해야 Set과 Dictionary가 올바르게 동작한다.
IdentityDictionary와 같은 컬렉션은 값 동등성 대신 객체 동일성을 키 비교에 사용할 수 있다. 이는 내용이 같더라도 서로 다른 객체를 구분해야 하는 메타데이터와 객체 추적에 활용된다.
구현체별 차이
Smalltalk 계열의 라이브러리는 공통된 객체·메시지 모델을 공유하지만 완전히 동일하지 않다. 차이는 다음 영역에서 특히 크다.
- 컬렉션 클래스 계층과 반환 클래스
- 문자열과 유니코드 표현
- 파일과 경로 API
- 네트워크와 비동기 입출력
- 예외 클래스 이름과 재개 동작
- 프로세스 스케줄링과 운영체제 스레드 연동
- 그래픽 사용자 인터페이스
- 패키지와 모듈 구조
- 날짜·시간 API
- 정규 표현식과 데이터 직렬화
- 외부 함수 인터페이스
ANSI Smalltalk은 공통 언어와 핵심 라이브러리 프로토콜을 제공하지만, 구현체의 전체 플랫폼을 동일하게 만들지는 않는다. Smalltalk-80 소스가 Squeak, Pharo, VisualWorks, GNU Smalltalk와 GemStone/S에서 그대로 실행된다고 항상 가정할 수는 없다.
이식 가능한 코드를 작성하려면 공통 프로토콜을 우선 사용하고, 파일·그래픽·외부 함수와 같은 구현체별 기능은 별도 계층에 캡슐화할 수 있다.
Application >> saveData: data
storageBackend save: data
storageBackend를 구현체별 객체로 교체하면 응용 프로그램의 핵심 모델을 유지하면서 외부 환경과의 연결을 분리할 수 있다.
Smalltalk의 표준 라이브러리와 프로그래밍 모델에서 중요한 것은 특정 클래스 이름을 암기하는 데 그치지 않는다. 객체가 어떤 프로토콜을 제공하며, 블록과 메시지를 사용해 다른 객체와 어떻게 협력하는지를 이해하는 것이 핵심이다.
주요 계통과 구현체
Smalltalk은 하나의 표준 구현체가 모든 환경을 지배하는 언어가 아니라, Smalltalk-80에서 갈라진 여러 방언과 실행 환경으로 발전했다. 각 구현체는 객체와 메시지, 클래스, 동적 바인딩과 라이브 프로그래밍이라는 공통 기반을 유지하면서도 가상 머신, 클래스 라이브러리, 그래픽 환경, 소스 관리 방식과 주된 활용 분야에서 차이를 보인다.
Smalltalk 구현체의 계통은 대체로 다음과 같이 구분할 수 있다.
- Xerox PARC의 Smalltalk-80에서 이어진 원형 계통
- ParcPlace의 ObjectWorks와 VisualWorks로 이어진 상용 계통
- Digitalk의 Smalltalk/V와 Visual Smalltalk로 이어진 개인용 컴퓨터 계통
- 기업용 객체 데이터베이스와 서버 환경으로 발전한 GemStone/S
- GNU 환경과 명령줄·스크립트 사용에 집중한 GNU Smalltalk
- Apple Smalltalk에서 이어져 공개 소프트웨어로 발전한 Squeak
- Squeak에서 분리되어 현대적인 개발 환경을 지향한 Pharo
- Microsoft Windows와 네이티브 사용자 인터페이스에 집중한 Dolphin Smalltalk
- IBM VisualAge Smalltalk에서 이어진 VA Smalltalk
- Squeak에서 파생되어 작고 단순한 시스템을 지향하는 Cuis
이러한 구현체는 단순히 같은 언어의 서로 다른 컴파일러에 해당하지 않는다. 전통적인 Smalltalk 환경은 언어, 가상 머신, 객체 이미지, 클래스 라이브러리와 개발 도구가 하나의 시스템을 이루기 때문이다. 따라서 구현체를 바꾸면 문법보다 라이브러리, 사용자 인터페이스, 패키지 관리와 개발 흐름에서 더 큰 차이를 경험할 수 있다.
Smalltalk-80
Smalltalk-80은 현대 Smalltalk 계열의 공통 기반이 된 언어이자 프로그래밍 환경이다. Smalltalk-72와 Smalltalk-76에서 발전한 객체 모델을 정리하고, 외부 기관이 다른 하드웨어에 이식할 수 있도록 언어와 가상 머신의 구조를 명세했다.
Smalltalk-80은 플랫폼 독립적인 객체 이미지와 플랫폼별 가상 머신을 분리했다. 클래스, 메서드와 개발 도구는 이미지 안에 저장되고, 가상 머신은 바이트코드 실행과 객체 메모리, 입력·출력을 담당했다. 이러한 분리는 이후 VisualWorks, Squeak과 여러 Smalltalk 구현체가 서로 다른 운영체제에서 같은 객체 환경을 제공하는 기반이 되었다.
Smalltalk-80은 단일한 하나의 배포본만을 의미하지 않는다. Xerox는 외부 검증과 이식을 거치면서 서로 다른 시기의 이미지와 가상 머신 명세를 만들었으며, 후속 구현체는 이 가운데 서로 다른 계통에서 출발했다.
Smalltalk-80에서 확립된 대표적인 요소는 다음과 같다.
- 단항·이항·키워드 메시지 문법
- 클래스와 메타클래스 구조
- 단일 상속과 동적 메서드 탐색
- 바이트코드 기반 가상 머신
- 객체 이미지 저장과 재개
- 시스템 브라우저, 작업 공간, 검사기와 디버거
- 컬렉션과 스트림 중심의 기본 라이브러리
- Model–View–Controller 기반 그래픽 환경
Smalltalk-80의 언어와 구현은 Adele Goldberg와 David Robson의 《Smalltalk-80: The Language and Its Implementation》을 비롯한 문헌으로 공개되었다. 이른바 Blue Book은 언어와 핵심 클래스뿐 아니라 가상 머신의 구조를 설명하여 독립적인 구현체를 만드는 기준이 되었다.[52]
오늘날 Smalltalk-80 자체는 일반적인 신규 응용 프로그램 개발용 제품이라기보다, Smalltalk 언어 계열의 역사적·기술적 기준으로 다뤄진다. 현대 구현체는 Smalltalk-80의 문법과 객체 모델을 계승하면서 메모리 관리자, JIT 컴파일, 유니코드, 패키지 관리와 운영체제 연동을 각자의 방식으로 확장했다.
VisualWorks
VisualWorks는 Xerox PARC의 Smalltalk-80 상용화 계통에서 직접 이어진 크로스 플랫폼 Smalltalk 환경이다. ParcPlace Systems의 Smalltalk-80 제품과 ObjectWorks를 거쳐 VisualWorks라는 이름으로 발전했으며, 1999년 이후 Cincom Systems가 제품을 개발하고 공급하고 있다.
VisualWorks는 객체 이미지와 가상 머신을 기반으로 한 라이브 개발 환경을 제공한다. 하나의 개발 환경에서 만든 코드와 사용자 인터페이스를 여러 운영체제에 배포하는 크로스 플랫폼 개발을 주요 방향으로 삼는다. Cincom은 VisualWorks를 다국어·크로스 플랫폼 응용 프로그램 개발 및 배포 플랫폼으로 소개하며, 클라이언트, 서버, 분산 시스템과 웹 응용 프로그램 개발을 지원한다.[53]
VisualWorks의 주요 구성 요소에는 다음과 같은 것들이 있다.
- 라이브 객체 이미지와 통합 개발 도구
- 크로스 플랫폼 가상 머신
- 클래스 브라우저와 디버거
- 패키지와 번들
- Parcel 기반 구성 요소 배포
- Store 소스 코드 관리 환경
- 데이터베이스 연결
- 네트워크와 분산 객체 기능
- 그래픽 사용자 인터페이스 프레임워크
- 서버 및 웹 응용 프로그램 지원
VisualWorks의 사용자 인터페이스는 플랫폼별 네이티브 위젯을 직접 사용하는 방식보다, 공통된 Smalltalk UI 계층을 통해 여러 플랫폼에서 일관된 응용 프로그램을 제공하는 방향으로 발전했다. 하나의 이미지와 소스 기반으로 Windows, Linux와 macOS 등의 환경에 배포할 수 있다.[54]
VisualWorks는 새로운 언어 실험보다 기존 기업용 Smalltalk 시스템의 장기 운영, 크로스 플랫폼 응용 프로그램과 대규모 객체 지향 시스템 개발에 중점을 둔다. 오래된 Smalltalk 응용 프로그램을 유지하면서 현대 운영체제와 외부 시스템에 연결해야 하는 환경에서도 사용된다.
VisualWorks는 독점 상용 제품이지만 비상업적 개인 프로젝트를 위한 Personal Use License도 제공된다. 상업적 이용과 조직 업무에는 별도의 라이선스가 필요하다.[55]
Visual Smalltalk
Visual Smalltalk은 Digitalk의 Smalltalk/V에서 발전한 개인용 컴퓨터 중심의 Smalltalk 계통이다. ParcPlace 계통이 주로 UNIX 워크스테이션과 고가의 기업 개발 환경에 집중한 것과 달리, Digitalk는 DOS, Windows와 OS/2 같은 Intel 기반 개인용 컴퓨터에서 사용할 수 있는 Smalltalk 제품을 공급했다.
Smalltalk/V는 Smalltalk-80의 객체와 메시지 중심 모델을 개인용 컴퓨터의 자원과 운영체제 환경에 맞게 구현했다. 제품군에는 DOS와 Windows용 Smalltalk/V, OS/2용 환경과 이후의 Visual Smalltalk Enterprise가 포함되었다.
Visual Smalltalk 계통은 다음과 같은 성격을 가졌다.
- 개인용 컴퓨터와 데스크톱 응용 프로그램에 집중
- Windows와 OS/2 환경 지원
- Smalltalk 이미지와 통합 개발 환경 제공
- 네이티브 운영체제 서비스와 사용자 인터페이스 연결
- 비교적 낮은 도입 비용
- 기업용 데이터베이스와 클라이언트·서버 개발 지원
1995년 ParcPlace Systems와 Digitalk가 합병하여 ParcPlace-Digitalk가 만들어졌고, 회사는 이후 ObjectShare로 이름을 바꾸었다. 두 회사의 제품 계통은 완전히 하나로 통합되지 않았으며, VisualWorks와 Visual Smalltalk는 서로 다른 기술 기반을 유지했다. ObjectShare의 Smalltalk 사업이 재편되면서 VisualWorks는 Cincom으로 이전되었고, Visual Smalltalk 계통은 독립적인 주력 제품으로서의 개발이 중단되었다.[56]
Visual Smalltalk은 현재 활발히 배포되는 Smalltalk 구현체라기보다, 1980년대 후반과 1990년대 Windows 기반 객체 지향 개발 환경의 주요 계통으로 역사적 의미를 가진다. Digitalk에서 축적된 일부 인력과 기술적 경험은 ParcPlace 합병과 후속 Smalltalk 제품 생태계에 이어졌다.
Visual Smalltalk과 VisualWorks는 이름이 비슷하지만 서로 다른 계통이다.
- VisualWorks는 ParcPlace의 Smalltalk-80·ObjectWorks 계통에서 발전했다.
- Visual Smalltalk는 Digitalk의 Smalltalk/V 계통에서 발전했다.
- 두 제품은 합병 이후에도 서로 다른 가상 머신과 클래스 라이브러리를 사용했다.
- 현재 VisualWorks는 Cincom이 계속 공급하지만 Visual Smalltalk는 역사적 제품으로 남아 있다.
GemStone/S
GemStone/S는 Smalltalk 언어와 분산 객체 데이터베이스를 결합한 서버 중심의 실행 환경이다. 일반적인 Smalltalk 이미지가 한 프로세스의 객체 메모리를 저장하는 데 초점을 둔다면, GemStone/S는 여러 사용자가 공유하는 대규모 지속 객체 저장소를 제공한다.
GemStone/S에서 Smalltalk 객체는 서버의 객체 저장소에 지속적으로 보관될 수 있다. 응용 프로그램은 객체를 관계형 테이블로 변환한 뒤 다시 객체로 복원하는 계층 없이, Smalltalk 객체와 참조 관계를 직접 저장하고 조회할 수 있다.
GemStone/S의 주요 기능은 다음과 같다.
- 다중 사용자 객체 저장소
- Smalltalk로 작성하는 서버 측 로직
- 객체 단위 지속성
- 트랜잭션과 동시성 제어
- 사용자 계정과 접근 권한
- 분산된 클라이언트와 서버 실행
- 대규모 객체 그래프 관리
- 고가용성과 장시간 서버 운영
- 외부 데이터 소스와의 연결
GemTalk Systems는 GemStone/S를 금융, 보험, 운송, 통신과 공공 설비 분야의 핵심 업무 시스템에 사용되는 고가용성 Smalltalk 플랫폼으로 제공한다.[57]
GemStone/S의 객체 저장소에서는 여러 세션이 같은 지속 객체를 사용할 수 있다. 각 세션은 트랜잭션 안에서 객체를 읽고 변경하며, 커밋하면 변경 사항이 다른 세션에 공개된다.
System commitTransaction
실제 트랜잭션 API와 권장 방식은 GemStone/S 버전에 따라 달라질 수 있다.
GemStone/S는 이름 해석과 객체 공유를 위해 사용자별 Symbol 목록과 공유 사전을 사용한다. 서버 저장소 안의 클래스와 전역 객체도 지속 객체이며, 여러 사용자가 접근 권한에 따라 같은 응용 프로그램 모델을 공유할 수 있다.
GemStone/S는 Smalltalk 언어, 객체 데이터베이스, 트랜잭션, 보안과 저장소 관리 기능을 하나의 서버 환경에 통합한다. 공식 프로그래머 안내서는 이를 다중 사용자·프로그래밍 가능·확장 가능한 객체 데이터베이스로 설명한다.[58]
클라이언트 Smalltalk 환경은 GemBuilder를 통해 GemStone/S에 연결할 수 있다. VisualWorks와 VA Smalltalk 같은 클라이언트 이미지의 객체와 서버 저장소의 객체를 연동하여, 사용자 인터페이스는 클라이언트에서 실행하고 대규모 데이터와 업무 로직은 서버에서 처리하는 구조를 구성할 수 있다.
GemStone/S는 일반적인 데스크톱 Smalltalk 구현체를 대체하기보다, Smalltalk 객체 모델을 데이터베이스와 다중 사용자 서버까지 확장한 계통에 해당한다.
GNU Smalltalk
GNU Smalltalk은 GNU 프로젝트가 제공하는 자유 소프트웨어 Smalltalk-80 구현체이다. 전통적인 그래픽 이미지 중심 Smalltalk보다 명령줄, 소스 파일, 스크립트와 헤드리스 실행에 더 중점을 둔다.
GNU Smalltalk은 Adele Goldberg와 David Robson의 Smalltalk-80 Blue Book에 기술된 언어를 가깝게 따르는 것을 목표로 한다.[59]
대표적인 실행 방식은 .st 파일을 gst 인터프리터에 전달하는 것이다.
Object subclass: Greeter [
greet [
'Hello from GNU Smalltalk' displayNl
]
]
Greeter new greet
gst greeter.st
GNU Smalltalk의 특징은 다음과 같다.
- Smalltalk-80에 가까운 언어 구현
- 명령줄 인터프리터
- 텍스트 소스 파일 중심의 실행
- UNIX와 POSIX 환경에 적합한 구성
- 셸 스크립트와 시스템 작업 연동
- 헤드리스 서버 및 배치 처리
- C 라이브러리와의 연동
- 자유 소프트웨어 라이선스
GNU 프로젝트는 GNU Smalltalk을 대부분의 UNIX 계열 및 POSIX 호환 환경에서 실행되는 자유 Smalltalk-80 구현체로 소개하며, 스크립팅과 화면 없는 처리에 적합하다는 점을 특징으로 든다.[60]
GNU Smalltalk도 객체 이미지와 가상 명령어 체계를 사용하지만, 사용자는 전통적인 Smalltalk 브라우저를 중심으로 작업하기보다 소스 파일과 명령줄을 중심으로 프로그램을 구성할 수 있다. 이 때문에 UNIX 도구 체계와 자동화 스크립트에서 다른 Smalltalk 구현체보다 자연스럽게 사용할 수 있다.
그래픽 라이브 환경보다 텍스트 편집기, 빌드 자동화와 명령줄 실행을 선호하는 환경에서는 GNU Smalltalk의 방식이 적합할 수 있다. 반면 Squeak이나 Pharo에서 제공하는 통합 Morphic 개발 환경과 라이브 객체 탐색 경험은 기본 사용 흐름의 중심이 아니다.
Squeak
Squeak은 Smalltalk-80에서 직접 이어진 공개 소프트웨어 Smalltalk 시스템이다. 1995년 Apple Computer에서 Alan Kay, Dan Ingalls, Ted Kaehler, John Maloney와 Scott Wallace 등이 개발을 시작했으며, 첫 버전은 1996년에 공개되었다.
Squeak은 Apple Smalltalk을 거쳐 Smalltalk-80 계통의 객체 이미지와 가상 머신 구조를 이어받았다. 가상 머신의 핵심 부분을 Smalltalk로 기술하고 이를 C 코드로 변환하는 방식으로 높은 이식성을 확보했다.
Squeak의 주요 특징은 다음과 같다.
- Smalltalk-80에서 직접 이어진 객체 모델
- 공개 소프트웨어로 제공되는 이미지와 가상 머신
- 크로스 플랫폼 지원
- Morphic 그래픽 프레임워크
- 멀티미디어와 교육 기능
- 시스템 전체를 탐색하고 수정할 수 있는 라이브 환경
- Smalltalk로 기술된 가상 머신 구성 요소
- 이미지 기반의 점진적 업데이트
Squeak 공식 사이트는 Squeak을 주요 플랫폼에서 실행되는 현대적인 공개 Smalltalk 프로그래밍 시스템으로 설명하며, Morphic을 이용한 그래픽·상호작용 응용 프로그램 개발을 주요 특징으로 제시한다.[61]
Morphic은 Self에서 개발된 같은 이름의 사용자 인터페이스 프레임워크에서 영향을 받았다. 화면 요소를 Morph 객체로 표현하며, 개발자는 실행 중인 Morph를 생성하고 검사하며 동작을 수정할 수 있다.
Morph new
color: Color blue;
extent: 200 @ 100;
openInWorld
Squeak은 교육, 멀티미디어, 게임, 연구와 상업 응용 프로그램에 사용되었다. Etoys, Croquet, Open Cobalt와 초기 Scratch 같은 프로젝트도 Squeak의 객체 환경과 그래픽 기능을 기반으로 발전했다.
Squeak의 개발은 공동체가 관리하며, Monticello 패키지와 점진적 업데이트 체계를 사용한다. 핵심 시스템과 라이브러리는 이미지 안에서 계속 수정·배포된다.[62]
Squeak은 여러 후속 Smalltalk 환경의 기반이 되었다. Pharo와 Cuis는 Squeak 이미지에서 분리되었으며, 각자 다른 우선순위에 따라 클래스 라이브러리와 개발 환경을 재구성했다.
Pharo
Pharo는 Squeak에서 분리되어 소프트웨어 개발, 언어 연구와 현대적인 라이브 프로그래밍 환경에 집중한 공개 Smalltalk 계열 언어이다. 프로젝트는 2008년에 시작되었으며, Squeak의 객체와 메시지 모델을 바탕으로 오래된 구성 요소를 정리하고 독립적인 언어·라이브러리·개발 도구 생태계를 구축했다.
Pharo는 자신을 Smalltalk 전통에 속하는 순수 객체 지향 언어이자, 단순성과 즉각적인 피드백에 집중한 강력한 개발 환경으로 설명한다.[63]
Pharo의 주요 특징은 다음과 같다.
- 순수 객체 지향 언어
- 동적 타입과 메시지 전송
- 이미지 기반 라이브 환경
- 실행 중 메서드와 객체 수정
- Glamorous Toolkit 계통에서 발전한 객체 중심 도구의 영향
- Calypso 시스템 브라우저
- 객체별 확장 보기를 제공하는 Inspector
- Iceberg를 통한 Git 연동
- SUnit 테스트
- 코드 분석과 리팩터링
- 서버와 웹 응용 프로그램 개발
- FFI와 네이티브 라이브러리 연동
- OpenSmalltalk VM의 Cog·Spur 계통 사용
Pharo는 Squeak의 역사적 호환성을 그대로 보존하는 것보다 시스템의 단순화와 적극적인 변경을 우선한다. 이에 따라 오래된 API를 제거하거나 클래스 라이브러리를 재구성할 수 있으며, Pharo 버전 사이에도 마이그레이션 작업이 필요할 수 있다.
Pharo의 개발 도구는 실행 중인 객체를 중심으로 구성된다. 객체 검사기는 객체 종류에 맞는 전용 보기를 제공할 수 있고, 디버거에서 메서드를 작성·수정하며 실행을 재개할 수 있다. 패키지는 Iceberg를 통해 Git 저장소와 연결할 수 있다.
Pharo는 국제 공개 소프트웨어 공동체가 개발하며 Pharo Consortium과 Inria를 비롯한 여러 연구·교육 기관의 지원을 받는다.[64]
Pharo는 다음 분야에서 사용된다.
- 웹 서버와 백엔드 응용 프로그램
- 객체 지향 설계 교육
- 프로그래밍 언어와 가상 머신 연구
- 소프트웨어 분석과 시각화
- 코드 변환과 리팩터링 연구
- 도메인 특화 언어
- 기업용 응용 프로그램
- 라이브 객체 개발 도구 연구
Pharo는 Smalltalk-80 계통의 문법과 기본 개념을 유지하지만, 독자적인 클래스 라이브러리와 도구를 갖춘 별도의 Smalltalk 방언으로 보는 편이 정확하다.
Dolphin Smalltalk
Dolphin Smalltalk은 Microsoft Windows에서 네이티브 데스크톱 응용 프로그램을 만드는 데 집중한 Smalltalk 구현체이다. Object Arts가 개발했으며, Windows의 사용자 인터페이스와 시스템 API를 Smalltalk 객체로 자연스럽게 사용할 수 있도록 설계되었다.
Dolphin은 크로스 플랫폼 이식성보다 Windows와의 깊은 통합을 우선한다. 공식 소개는 Dolphin을 Microsoft Windows를 대상으로 하는 완전한 Smalltalk 통합 개발 환경으로 설명하며, 독립 실행형 그래픽 및 콘솔 프로그램을 만들 수 있다고 밝힌다.[65]
Dolphin의 주요 특징은 다음과 같다.
- Windows 네이티브 Smalltalk 가상 머신
- Windows 위젯과 운영체제 API 연동
- 통합 개발 환경
- 패키지 브라우저
- 리팩터링 브라우저
- WYSIWYG 방식의 View Composer
- COM과 ActiveX 연동
- 독립 실행형 프로그램 배포
- 네이티브 DLL과 외부 함수 연결
Dolphin의 사용자 인터페이스 프레임워크는 Windows의 창과 컨트롤을 직접 활용할 수 있다. 이를 통해 실행 결과가 다른 플랫폼에 맞춰 추상화된 공통 위젯보다 Windows 응용 프로그램의 동작과 외형에 가깝게 구성된다.
Dolphin 7부터 프로젝트는 공개 소프트웨어로 전환되었다. 현재 소스 저장소에는 Dolphin 가상 머신을 빌드하는 Visual Studio 솔루션과 핵심 이미지를 구성하는 Smalltalk 패키지가 포함되어 있다.[66]
Dolphin은 Windows 전용이라는 점에서 VisualWorks, Squeak과 Pharo의 크로스 플랫폼 방향과 구분된다. Windows 네이티브 API, COM 구성 요소와 기존 데스크톱 환경을 깊게 활용하는 Smalltalk 응용 프로그램에 적합한 계통이다.
VA Smalltalk
VA Smalltalk은 IBM의 VisualAge for Smalltalk에서 이어진 기업용 Smalltalk 구현체이다. IBM이 VisualAge Smalltalk 제품에서 철수한 뒤 Instantiations가 2005년에 전 세계 제품 권리를 인수했고, 이후 VA Smalltalk라는 이름으로 개발을 이어 왔다.[67]
현재 제품군은 VAST Platform이라는 이름으로 제공되며, Smalltalk 런타임과 이미지 기반 통합 개발 환경을 결합한다. Instantiations는 이를 이식 가능하고 확장 가능한 다계층 기업 응용 프로그램을 개발하기 위한 환경으로 설명한다.[68]
VA Smalltalk의 주요 특징은 다음과 같다.
- IBM VisualAge Smalltalk 계통의 호환성
- 이미지 기반 통합 개발 환경
- 기업용 클라이언트·서버 개발
- 다계층 응용 프로그램
- 웹 서비스와 데이터베이스 연결
- 패키지와 구성 맵 기반 소스 관리
- 기존 VisualAge Smalltalk 시스템의 이전과 유지
- Windows와 Linux 등 복수 플랫폼 지원
- GemStone/S 연결
VA Smalltalk은 특히 VisualAge Smalltalk로 구축된 기존 기업 시스템을 현대 운영체제와 데이터베이스 환경에서 계속 운영하는 역할을 한다. 동시에 새로운 Smalltalk 응용 프로그램과 웹 서비스를 만들 수 있는 라이브 개발 환경도 제공한다.
GemBuilder for Smalltalk/VA를 사용하면 VA Smalltalk 클라이언트와 GemStone/S 서버를 연결하여 응용 프로그램을 클라이언트와 객체 데이터베이스 사이에 분할할 수 있다.[69]
Cuis
Cuis는 Squeak 3.7에서 분리되어 작고 단순하며 이해하기 쉬운 Smalltalk 시스템을 지향하는 공개 구현체이다. Squeak과 같은 Smalltalk-80 계통의 객체 모델과 이미지 방식을 계승하지만, 핵심 시스템을 가능한 한 간결하게 유지하고 불필요한 복잡성을 줄이는 방향으로 발전했다.
Cuis의 주요 방향은 다음과 같다.
- 작고 읽기 쉬운 핵심 이미지
- Smalltalk-80 계통의 문법과 객체 모델
- Squeak 계통의 가상 머신 활용
- 단순한 Morphic 사용자 인터페이스
- 고해상도 벡터 그래픽과 글꼴 처리
- 시스템 전체를 이해하고 수정하기 쉬운 구조
- 교육과 개인 연구에 적합한 환경
Cuis는 Squeak의 단순한 배포판이 아니라 별도의 클래스 라이브러리와 사용자 인터페이스 방향을 가진 독립적인 Smalltalk 계통으로 발전했다. Squeak 3.7에서 분리된 뒤 두 프로젝트는 각자 변화했으며, 일부 기술은 상호 영향을 주고받았다.[70]
구현체 선택 기준
Smalltalk 구현체는 문법의 미세한 차이보다 개발 대상과 실행 환경에 따라 선택할 수 있다.
| 구현체 | 주요 성격 | 대표적인 활용 |
|---|---|---|
| Smalltalk-80 | 역사적 기준과 원형 환경 | 언어 역사, 객체 모델과 VM 연구 |
| VisualWorks | 상용 크로스 플랫폼 환경 | 기업용 응용 프로그램, 기존 시스템 운영 |
| Visual Smalltalk | 중단된 PC 중심 상용 계통 | 역사적 Windows·OS/2 Smalltalk 시스템 |
| GemStone/S | 분산 객체 데이터베이스 | 대규모 지속 객체, 다중 사용자 서버 |
| GNU Smalltalk | 명령줄·소스 파일 중심 | UNIX 스크립트, 배치와 헤드리스 실행 |
| Squeak | 범용 공개 Smalltalk 환경 | 교육, 멀티미디어, 연구와 그래픽 응용 |
| Pharo | 현대적 라이브 개발 환경 | 서버, 연구, 분석 도구와 신규 개발 |
| Dolphin Smalltalk | Windows 네이티브 환경 | Windows 데스크톱과 COM 응용 프로그램 |
| VA Smalltalk | 기업용 VisualAge 계통 | 기존 업무 시스템과 다계층 응용 프로그램 |
| Cuis | 작고 단순한 공개 시스템 | 교육, 실험과 시스템 이해 |
하나의 구현체에서 작성한 순수한 계산 코드와 기본 컬렉션 코드는 다른 Smalltalk로 비교적 쉽게 옮길 수 있다. 그러나 그래픽 사용자 인터페이스, 파일 시스템, 네트워크, 패키지 관리, 데이터베이스와 외부 함수 인터페이스는 구현체별 차이가 크다.
이식성을 고려한 응용 프로그램은 핵심 도메인 모델을 공통 메시지 프로토콜로 작성하고, 플랫폼별 기능을 별도의 어댑터와 서비스 객체에 배치할 수 있다.
Application >> saveDocument: aDocument
storageService save: aDocument
storageService를 VisualWorks, Pharo, Dolphin 또는 GemStone/S 환경에 맞는 객체로 교체하면 핵심 객체 모델을 유지하면서 외부 기능을 분리할 수 있다.
Smalltalk 생태계의 여러 구현체는 하나의 기준 구현체를 복제하기보다 서로 다른 목적에 맞게 언어와 환경을 함께 발전시켰다. 그 결과 Smalltalk은 단일 제품보다 객체와 메시지, 라이브 이미지와 반영 가능한 개발 환경을 공유하는 프로그래밍 언어 계열로 남아 있다.
활용 분야
Smalltalk은 객체 지향 프로그래밍과 그래픽 사용자 인터페이스가 발전하던 초기에는 개인용 컴퓨팅과 소프트웨어 개발 환경을 연구하는 데 사용되었고, 이후에는 기업용 정보 시스템, 금융·보험 업무, 객체 데이터베이스, 데스크톱 응용 프로그램과 교육 환경으로 활용 범위가 넓어졌다.
Smalltalk의 라이브 프로그래밍 환경은 요구 사항을 실행 중인 객체와 빠르게 연결하고, 오류가 발생한 실제 상태에서 코드를 수정할 수 있게 한다. 이러한 특성은 업무 규칙이 복잡하거나 사용자의 피드백에 따라 기능을 반복적으로 변경해야 하는 시스템에 적합했다.
1990년대에는 VisualWorks, VisualAge for Smalltalk, Visual Smalltalk와 GemStone/S 같은 상용 환경이 금융, 보험, 제조, 운송과 통신 분야의 대규모 시스템에 사용되었다. 이후 Java, C#과 웹 기반 플랫폼이 기업 개발의 주류가 되면서 신규 Smalltalk 프로젝트의 비중은 줄었지만, 기존 업무 시스템의 유지·확장과 특정 전문 영역에서는 계속 사용되고 있다.
현대 Smalltalk은 하나의 분야에 집중된 언어라기보다 구현체에 따라 서로 다른 활용 방향을 가진다.
- VisualWorks와 VA Smalltalk는 기업용 응용 프로그램과 기존 시스템 운영에 사용된다.
- GemStone/S는 다중 사용자 객체 데이터베이스와 서버 시스템에 사용된다.
- Pharo는 웹·서버 개발, 연구, 소프트웨어 분석과 교육에 사용된다.
- Squeak은 교육, 멀티미디어, 그래픽과 프로그래밍 환경 연구에 사용된다.
- Dolphin Smalltalk는 Windows 데스크톱 응용 프로그램에 사용된다.
- GNU Smalltalk는 명령줄 프로그램과 자동화 작업에 사용할 수 있다.
Cincom은 VisualWorks가 반도체 제조, 해운, 금융 위험 관리, 보험, 은행, 정부, 교육과 의료 등의 분야에서 사용된다고 설명한다.[71] Squeak은 교육, 멀티미디어, 게임, 연구와 상업 응용 프로그램을 대표적인 활용 범위로 제시한다.[72]
신속한 프로토타이핑
Smalltalk은 새로운 기능과 사용자 인터페이스를 빠르게 시험하는 신속한 프로토타이핑에 적합하다. 클래스와 메서드를 실행 중인 이미지에 즉시 추가할 수 있고, 생성된 객체의 상태를 유지한 채 동작을 반복해서 수정할 수 있기 때문이다.
Order >> total
^ items
inject: 0
into: [ :sum :item |
sum + item price
]
개발자는 작업 공간에서 실제 주문 객체를 만들고 결과를 확인할 수 있다.
order := Order new.
order addItem: (Product name: 'Keyboard' price: 50000).
order total
계산 방식이 바뀌면 total 메서드만 수정한 뒤 같은 order 객체에 다시 메시지를 보낼 수 있다. 주문 객체와 내부 상품 목록을 다시 구성할 필요가 없다.
Smalltalk의 프로토타이핑은 완성 후 폐기되는 화면 모형만을 만드는 데 그치지 않는다. 프로토타입에서 만든 클래스와 객체 모델을 점진적으로 다듬어 실제 시스템으로 발전시킬 수 있다. 이 방식은 사용자의 업무 과정을 관찰하면서 도메인 모델과 인터페이스를 함께 조정하는 개발에 적합하다.
그래픽 개발 환경에서는 화면 요소도 실행 중인 객체이므로, 창과 위젯을 열어 둔 상태에서 색상, 위치, 동작과 이벤트 처리 메서드를 바꿀 수 있다.
button
label: '주문 확정';
target: orderController;
actionSelector: #confirmOrder
오류가 발생하면 실제 사용 상태를 보존한 디버거에서 원인을 확인하고 메서드를 수정한 뒤 계산을 이어갈 수 있다. 이와 같은 짧은 피드백 주기는 요구 사항이 아직 고정되지 않았거나 사용자와 개발자가 함께 시스템을 구체화하는 프로젝트에서 유용하다.
기업용 정보 시스템
Smalltalk은 1980년대 후반부터 1990년대에 걸쳐 대규모 기업용 정보 시스템을 구축하는 주요 객체 지향 언어 가운데 하나였다. VisualWorks, VisualAge for Smalltalk, Smalltalk/V와 GemStone/S는 클라이언트·서버 응용 프로그램, 업무 처리 시스템과 데이터베이스 기반 프로그램에 사용되었다.
기업 시스템은 고객, 계약, 주문, 계좌, 운송, 생산 설비와 같은 복잡한 업무 개념을 다룬다. Smalltalk은 이러한 개념을 상태와 동작을 가진 객체로 직접 모델링할 수 있다.
InsurancePolicy >> premiumFor: aCustomer
^ pricingRules
inject: basePremium
into: [ :premium :rule |
rule
applyTo: premium
for: aCustomer
]
업무 규칙을 각각의 객체로 분리하면 규칙 추가와 교체를 하위 클래스, 합성 또는 공통 프로토콜로 처리할 수 있다.
pricingRules add: YoungDriverSurcharge new.
pricingRules add: SafeDrivingDiscount new
Smalltalk은 다음과 같은 기업 응용 프로그램에 활용되어 왔다.
- 고객 관계와 계약 관리
- 주문 및 재고 처리
- 청구와 정산
- 보험 계약과 보험금 처리
- 은행 계좌와 거래 처리
- 생산 계획과 설비 관리
- 물류와 운송 일정
- 위험 분석과 의사 결정 지원
- 정부·공공기관의 행정 시스템
- 의료 정보와 업무 관리
VisualWorks는 클라이언트, 서버, 분산 시스템, 그리드와 웹 응용 프로그램을 하나의 환경에서 개발할 수 있도록 구성된다.[73]
기업용 Smalltalk 시스템은 오랜 기간 실행되면서 많은 업무 규칙과 데이터를 축적한 경우가 많다. 이러한 시스템에서는 전면 교체보다 기존 객체 모델을 유지하면서 새 데이터베이스, 웹 서비스와 사용자 인터페이스를 연결하는 방식이 선택될 수 있다.
금융과 보험
금융과 보험은 Smalltalk이 역사적으로 널리 사용된 대표적인 산업 분야이다. 금융 상품과 보험 계약은 조건, 상태 변화, 시간에 따른 계산과 예외 규칙이 복잡하며, 규정과 시장 상황에 따라 계속 변경된다.
Smalltalk에서는 금융 상품과 거래 규칙을 객체로 모델링하고 메시지를 통해 계산을 구성할 수 있다.
Portfolio >> marketValueAt: aDate
^ positions
inject: 0
into: [ :total :position |
total + (position marketValueAt: aDate)
]
주식, 채권, 파생상품과 현금 포지션은 서로 다른 클래스여도 marketValueAt: 메시지에 응답하면 같은 포트폴리오 계산에 참여할 수 있다.
금융 분야의 활용 사례에는 다음과 같은 시스템이 포함된다.
- 금융 위험 분석
- 증권 거래와 주문 처리
- 포트폴리오 관리
- 파생상품 가격 계산
- 은행 계좌와 결제 처리
- 신용 평가
- 보험료 산정
- 보험 계약 관리
- 보험금 청구와 지급
- 규제 보고와 감사 자료 생성
보험 시스템에서는 상품별 규칙과 계약 조건을 객체 계층 또는 규칙 객체로 구성할 수 있다.
Claim >> payableAmount
self isCovered
ifFalse: [ ^ 0 ].
^ coverage
amountFor: self
afterDeductible: policy deductible
금융·보험 업무에서는 이미 검증된 기존 시스템을 안정적으로 유지하는 것이 중요하다. 따라서 조직이 다른 언어와 플랫폼을 신규 개발에 사용하더라도, 핵심 계산과 업무 처리를 담당하는 기존 Smalltalk 시스템을 계속 운영하고 외부 서비스와 연결할 수 있다.
Cincom은 VisualWorks의 활용 분야에 금융 위험 관리, 보험과 은행 업무를 포함하고 있다.[74]
제조와 물류
Smalltalk은 생산 설비, 공정, 자재, 선박, 화물과 운송 경로처럼 서로 관계를 맺으며 상태가 계속 변하는 대상을 모델링하는 데 사용되어 왔다.
제조 시스템에서는 다음과 같은 기능을 다룰 수 있다.
- 생산 계획
- 공정 제어와 상태 추적
- 반도체 제조 관리
- 설비 유지보수
- 품질 관리
- 자재와 부품 흐름
- 창고 및 재고 관리
- 생산 결과 분석
ProductionLine >> schedule: aJob
self canProcess: aJob
ifFalse: [
InvalidProductionJob signal
].
pendingJobs add: aJob.
self recalculateSchedule
각 설비와 작업은 자신의 처리 능력, 소요 시간과 상태를 객체로 관리할 수 있다.
운송·물류 분야에서는 선박, 차량, 화물, 항구, 경로와 일정을 객체로 표현할 수 있다.
Shipment >> estimatedArrival
^ route
inject: departureTime
into: [ :time :segment |
time + segment expectedDuration
]
지연, 경로 변경과 화물 상태 같은 사건은 관련 객체에 메시지를 보내 반영할 수 있다.
Cincom은 VisualWorks가 반도체 제조와 해운 분야에서도 사용된다고 설명한다.[75]
객체 데이터베이스
Smalltalk의 객체 모델은 객체 데이터베이스와 자연스럽게 결합된다. 일반적인 관계형 데이터베이스에서는 응용 프로그램의 객체를 테이블의 행과 열로 변환하고, 조회 결과를 다시 객체로 구성해야 한다. 객체 데이터베이스는 객체의 클래스, 상태와 참조 관계를 직접 지속시키는 방식을 제공한다.
GemStone/S는 Smalltalk 언어와 다중 사용자 객체 데이터베이스를 통합한 대표적인 환경이다. 여러 세션이 서버의 지속 객체 공간을 공유하고, 트랜잭션을 통해 객체를 변경할 수 있다.
customer := Customer
name: 'Luna'
accountNumber: 'A-1001'.
CustomerRegistry current add: customer.
System commitTransaction
객체가 저장소의 지속 루트에서 참조되면, 객체 자체와 그 객체가 참조하는 다른 객체도 지속 객체 그래프의 일부가 될 수 있다.
객체 데이터베이스는 다음과 같은 경우에 활용할 수 있다.
- 객체 사이의 관계가 복잡한 업무 시스템
- 장기간 유지되는 공유 객체 그래프
- 다수의 사용자가 같은 객체 공간을 사용하는 서버
- 객체 구조를 관계형 스키마로 변환하기 어려운 시스템
- 실행 코드와 데이터 모델을 동일한 언어로 관리하는 환경
GemStone/S는 다중 사용자, 확장 가능한 객체 데이터베이스로 제공되며, 금융·보험·운송·통신과 공공 설비 같은 분야의 장시간 운영 시스템에 사용된다.[76]
객체 데이터베이스를 사용하지 않는 Smalltalk 구현체도 관계형 데이터베이스와 연결할 수 있다. VisualWorks와 Pharo에서는 SQL 드라이버와 객체 관계 매핑 계층을 통해 기존 데이터베이스를 사용할 수 있다.
웹과 서버 응용 프로그램
Smalltalk은 이미지 기반 데스크톱 환경으로 알려졌지만, HTTP 서버, REST API, 웹 애플리케이션과 백엔드 서비스에도 사용할 수 있다.
현대 Smalltalk 서버는 일반적으로 다음 계층으로 구성할 수 있다.
WebApplication >> handleRequest: aRequest
| user |
user := userRepository
findById: (aRequest parameterAt: #userId).
^ self responseFor: user
- HTTP 요청과 응답 처리
- URL 라우팅
- 세션과 인증
- HTML 생성
- JSON 직렬화
- 데이터베이스 연결
- WebSocket 통신
- 백그라운드 작업
- 외부 API 연동
VisualWorks는 HTTP·HTTPS, WebSocket, JSON, 웹 서비스와 관계형 데이터베이스 연동을 제공하며, 기업용 웹 및 분산 응용 프로그램을 지원한다.[77]
Pharo에서는 Zinc가 HTTP 클라이언트와 서버 기능을 제공하고, Seaside 같은 프레임워크를 사용해 웹 응용 프로그램을 만들 수 있다. Zinc는 HTTP와 HTTPS, 문자 인코딩, 요청·응답 객체와 서버 구성을 Smalltalk 객체 모델로 제공한다.[78]
ZnServer startDefaultOn: 8080
실제 서버 구성 API는 Pharo와 Zinc 버전에 따라 달라질 수 있다.
Smalltalk의 라이브 환경은 서버 코드에도 적용할 수 있다. 개발 중인 서버를 실행한 상태에서 요청 처리 메서드를 수정하고, 실제 요청 객체와 응답 상태를 디버거에서 조사할 수 있다.
운영 환경에서 코드를 실시간으로 수정할 수도 있지만, 여러 서버 인스턴스와 지속 데이터가 연결된 시스템에서는 테스트, 버전 관리와 배포 절차를 함께 사용해야 한다.
데스크톱과 그래픽 응용 프로그램
Smalltalk은 그래픽 사용자 인터페이스와 함께 발전했으며, 데스크톱 응용 프로그램은 역사적으로 중요한 활용 분야였다. Smalltalk-80은 창, 메뉴, 텍스트 편집기, 그래픽 도구와 개발 환경을 같은 객체 시스템 안에서 구현했다.
후속 구현체는 서로 다른 GUI 계통을 발전시켰다.
- VisualWorks는 크로스 플랫폼 사용자 인터페이스를 제공한다.
- Dolphin Smalltalk는 Windows 네이티브 응용 프로그램에 집중한다.
- Squeak과 Pharo는 Morphic 계통의 그래픽 객체 환경을 사용한다.
- VA Smalltalk는 기업용 데스크톱과 클라이언트·서버 인터페이스를 제공한다.
데스크톱 응용 프로그램의 예로는 다음을 들 수 있다.
- 업무용 데이터 입력 도구
- 데이터 시각화
- 설계와 모델링 도구
- 코드 및 소프트웨어 분석 도구
- 문서와 콘텐츠 편집기
- 교육용 시뮬레이션
- 과학·공학용 대화형 도구
- 시스템 관리 도구
Smalltalk의 GUI 요소는 실행 중인 객체이므로 화면에 열린 구성 요소를 검사하고 수정할 수 있다.
window := ApplicationWindow new.
window
label: 'Inventory';
model: inventoryModel;
open
사용자 인터페이스 API는 구현체별로 다르지만, 모델과 화면 객체를 메시지로 연결하고 라이브 환경에서 조작한다는 기본 방향은 공통적이다.
교육
교육은 Smalltalk이 처음부터 중요하게 다뤄 온 활용 분야이다. Alan Kay의 Dynabook 구상과 Learning Research Group의 연구는 어린이가 컴퓨터를 수동적으로 사용하는 데 그치지 않고, 자신만의 모델과 시뮬레이션을 만들 수 있어야 한다는 생각을 바탕으로 했다.
Smalltalk은 다음과 같은 특성으로 교육에 활용된다.
- 모든 값을 객체라는 일관된 모델로 설명할 수 있다.
- 코드를 실행하면 결과를 즉시 확인할 수 있다.
- 객체의 상태를 검사기로 직접 볼 수 있다.
- 그래픽과 사용자 입력을 프로그램과 연결하기 쉽다.
- 오류가 발생해도 실행 상태를 보존한 채 원인을 탐색할 수 있다.
- 언어와 개발 환경의 구현 자체를 읽고 수정할 수 있다.
Squeak은 교육과 멀티미디어를 주요 활용 분야로 삼았으며, Etoys는 화면의 그래픽 객체에 동작을 부여하여 이야기, 시뮬레이션과 대화형 모델을 만들 수 있게 했다.
초기 Scratch도 Squeak 환경에서 개발되었다. Scratch는 Smalltalk 문법을 직접 노출하지는 않았지만, Squeak의 객체 환경과 Morphic 그래픽 체계를 기반으로 시각적 블록 프로그래밍 환경을 구현했다.
Pharo는 대학의 객체 지향 설계와 프로그래밍 언어 교육에도 사용된다. Pharo 기반 Advanced Object-Oriented Design MOOC는 순수 객체 지향 언어와 라이브 객체 환경을 사용해 객체 지향 설계 개념을 설명한다.[79]
교육에서 Smalltalk은 단순히 문법을 가르치는 언어보다 객체, 메시지, 상태, 추상화와 프로그램 실행을 직접 관찰하는 환경으로 활용될 수 있다.
연구와 프로그래밍 언어 개발
Smalltalk은 언어 구현과 개발 도구 자체가 Smalltalk 객체로 노출되므로 프로그래밍 언어와 소프트웨어 공학 연구에 활용된다.
대표적인 연구 주제는 다음과 같다.
- 객체 모델과 메시지 전송
- 동적 타입과 타입 추론
- 반영과 메타객체 프로토콜
- 가상 머신과 JIT 컴파일
- 가비지 컬렉션
- 라이브 프로그래밍
- 디버거와 실행 문맥
- 코드 분석과 리팩터링
- 소프트웨어 시각화
- 도메인 특화 언어
- 사용자 정의 개발 도구
- 프로그래밍 교육 환경
Squeak은 연구자와 학생이 시스템 전체의 소스와 가상 머신 구성 요소를 조사할 수 있도록 설계되었다. Squeak의 초기 논문은 연구자, 교수와 학생이 시스템의 모든 부분을 살펴볼 수 있다는 점을 중요한 특징으로 설명한다.[80]
Pharo는 소프트웨어 분석과 도구 개발 연구에 널리 활용된다. 클래스, 메서드, 구문 트리와 실행 문맥이 반영 가능한 객체이므로 분석 결과를 같은 환경에서 시각화하고, 코드 변환과 리팩터링을 적용할 수 있다.
Object allSubclasses
select: [ :class |
class methodDictionary
includesKey: #deprecatedMethod
]
연구자는 외부 분석 프로그램을 별도로 구축하지 않고 실행 중인 이미지의 코드 모델을 직접 질의할 수 있다.
Smalltalk은 새로운 언어의 인터프리터와 컴파일러를 구현하는 호스트 언어로도 사용된다. 파서, 구문 트리, 실행 환경과 디버거를 객체로 표현하고, 작업 공간에서 즉시 시험할 수 있기 때문이다.
소프트웨어 분석과 개발 도구
Smalltalk의 반영 기능은 대규모 소프트웨어를 분석하고 전용 개발 도구를 만드는 데 활용된다.
분석 대상에는 다음이 포함될 수 있다.
- 클래스와 상속 구조
- 메서드 호출 관계
- 패키지 의존성
- 코드 중복
- 변경 이력
- 테스트 범위
- 객체 수와 메모리 사용
- 실행 시간과 호출 빈도
- 아키텍처 규칙 위반
package classes collect: [ :class |
class -> class methods size
]
분석 결과는 표, 트리, 그래프와 전용 검사기 보기로 표현할 수 있다. Smalltalk에서는 분석 코드, 대상 코드와 시각화 도구가 같은 객체 환경 안에서 실행되므로, 결과에서 클래스나 메서드 객체로 직접 이동할 수 있다.
Pharo 계열에서는 Moose와 Glamorous Toolkit 같은 환경이 이러한 객체 중심 분석과 도구 제작에 사용되어 왔다. 개발자는 일반화된 IDE 화면에 데이터를 맞추기보다, 특정 시스템과 문제에 맞는 전용 도구를 구성할 수 있다.
이 방식은 다음과 같은 작업에 활용된다.
- 오래된 시스템 구조 파악
- 마이그레이션 준비
- 코드 품질 검사
- 도메인 모델 시각화
- 데이터 탐색 환경 제작
- 사용자 정의 디버거
- 전용 객체 검사기
- 언어별 편집·분석 도구
멀티미디어와 대화형 콘텐츠
Squeak은 컬러 그래픽, 소리, 애니메이션과 Morphic 환경을 결합하여 멀티미디어 및 대화형 콘텐츠 제작에 사용되었다. 화면에 표시된 객체를 직접 이동하고 속성을 변경하며 동작을 연결할 수 있다.
대표적인 활용은 다음과 같다.
- 대화형 이야기
- 교육용 애니메이션
- 물리·수학 시뮬레이션
- 그림과 소리 편집
- 프레젠테이션
- 협업형 가상 환경
- 간단한 게임
- 어린이용 콘텐츠 제작
[
100 timesRepeat: [
morph position: morph position + (2 @ 0).
World displayWorld.
(Delay forMilliseconds: 20) wait
]
] fork
위 코드는 개념적인 Morphic 애니메이션 예이며 실제 화면 갱신 방식은 구현체와 버전에 따라 달라질 수 있다.
Squeak 기반 프로젝트인 Etoys는 사용자가 그래픽 객체를 만들고 객체별 스크립트를 구성하도록 했다. Croquet은 Squeak과 Smalltalk의 객체 환경을 사용해 협업 가능한 3차원 공간을 연구했다.
Squeak 공식 사이트는 교육과 연구 외에도 멀티미디어와 게임을 실제 프로젝트가 만들어진 분야로 제시한다.[81]
현대 상용 게임 개발에서 Smalltalk은 주류 언어가 아니지만, 대화형 시스템과 게임 규칙을 빠르게 실험하거나 라이브 객체 환경을 연구하는 데 활용될 수 있다.
명령줄과 자동화
GNU Smalltalk은 전통적인 그래픽 이미지 중심 환경보다 소스 파일과 명령줄 실행에 적합한 사용 방식을 제공한다.
files := Directory working filesMatching: '*.txt'.
files do: [ :file |
Transcript
show: file basename;
cr
]
Smalltalk 스크립트는 다음과 같은 작업에 사용할 수 있다.
- 파일 변환
- 데이터 처리
- 보고서 생성
- 배치 작업
- 소스 코드 분석
- 테스트 자동화
- 시스템 관리 보조 도구
- 다른 프로그램과의 파이프라인 구성
GNU Smalltalk은 POSIX 환경에서 실행되는 명령줄 인터프리터와 파일 기반 개발 방식을 제공하므로 셸, 빌드 시스템과 서버 자동화에 연결할 수 있다.[82]
Pharo도 헤드리스 이미지 실행과 명령줄 인자를 지원하여 자동화, 지속적 통합과 서버 작업에 사용할 수 있다. 이 경우에도 프로그램은 객체 이미지와 가상 머신 위에서 실행되지만, 그래픽 개발 화면을 열지 않고 지정된 작업을 수행한다.
기존 시스템의 유지와 현대화
현재 Smalltalk의 중요한 활용 분야 중 하나는 수십 년 동안 운영된 기존 시스템의 유지와 현대화이다. 금융, 보험, 제조와 공공 업무 시스템에는 오랜 기간 검증된 Smalltalk 코드와 업무 규칙이 축적되어 있을 수 있다.
이러한 시스템을 현대화하는 방향은 여러 가지로 나뉜다.
- 기존 Smalltalk 구현체의 최신 버전으로 이전한다.
- 32비트 이미지를 64비트 환경으로 이전한다.
- 오래된 GUI를 웹 인터페이스나 새 데스크톱 UI로 교체한다.
- 기존 객체 모델에 REST API를 추가한다.
- 새로운 데이터베이스와 메시지 시스템을 연결한다.
- Smalltalk 서버를 다른 언어의 서비스와 통합한다.
- 패키지를 Git과 자동화된 빌드 체계로 옮긴다.
- 일부 기능을 다른 플랫폼으로 단계적으로 분리한다.
CustomerService >> customerResponseFor: anIdentifier
| customer |
customer := repository findByIdentifier: anIdentifier.
^ JsonObject new
at: #name put: customer name;
at: #status put: customer status;
yourself
기존 업무 모델은 유지하면서 외부에 JSON API를 제공하는 식으로 다른 시스템과 연결할 수 있다.
Smalltalk의 반영과 코드 탐색 도구는 오래된 시스템의 구조를 파악하는 데 유용하다. 선택자의 송신자와 구현자를 검색하고, 실제 운영 데이터와 비슷한 객체를 검사하면서 업무 흐름을 추적할 수 있다.
기존 시스템을 유지하는 것은 단순히 오래된 기술을 보존하는 작업만을 의미하지 않는다. 수십 년간 검증된 업무 규칙과 데이터 모델을 유지하면서 운영체제, 네트워크, 보안과 사용자 인터페이스를 현재 환경에 맞게 갱신하는 작업이다.
현재의 위치
Smalltalk은 현대 범용 소프트웨어 개발에서 Java, JavaScript, Python, C# 또는 C++와 같은 주류 언어만큼 넓게 사용되지는 않는다. 새로운 개발자와 라이브러리, 클라우드 플랫폼 지원의 규모도 주류 생태계보다 작다.
그러나 Smalltalk의 활용이 역사적 연구에만 한정된 것은 아니다. VisualWorks, VA Smalltalk와 GemStone/S는 기업 시스템에서 계속 공급되고 있으며, Pharo, Squeak, Cuis와 Dolphin Smalltalk는 공개 개발 환경으로 유지되고 있다.
현재 Smalltalk의 활용은 대체로 다음 영역에 집중된다.
- 기존 기업용 시스템의 운영과 확장
- 금융·보험 분야의 복잡한 업무 모델
- 지속 객체와 객체 데이터베이스
- 웹 서버와 전문 백엔드
- 객체 지향 설계 교육
- 라이브 프로그래밍 연구
- 소프트웨어 분석과 전용 개발 도구
- 그래픽·멀티미디어 실험
- 프로그래밍 언어와 가상 머신 연구
Smalltalk을 선택하는 주된 이유는 단순한 문법의 간결함보다, 실행 중인 객체와 직접 상호작용하는 개발 환경, 반영 가능한 시스템, 메시지와 프로토콜 중심의 객체 모델에 있다. 반대로 기존 생태계와 인력 확보, 외부 라이브러리 통합이 더 중요한 프로젝트에서는 다른 언어와 Smalltalk 서비스를 함께 사용하는 구조가 선택될 수 있다.
다른 프로그래밍 언어와의 관계
Smalltalk은 객체 지향 프로그래밍 언어의 역사에서 선행 언어의 개념을 하나의 일관된 실행 환경으로 결합하고, 그 결과를 여러 후속 언어와 개발 환경에 전달한 위치에 있다. Simula에서 클래스와 객체에 의한 시스템 모델링을 받아들였고, Lisp에서 동적인 실행 환경과 프로그램을 자료처럼 다루는 성격을 이어받았으며, Logo와 개인용 컴퓨팅 연구에서는 학습자가 직접 시스템을 탐색하고 구성하는 방향을 받아들였다.
Smalltalk 이후에는 메시지 전송, 동적 바인딩, 모든 값을 객체로 다루는 모델, 코드 블록, 가비지 컬렉션, 바이트코드 가상 머신, 반영과 라이브 개발 환경이 여러 언어에 서로 다른 형태로 이어졌다. 그러나 후속 언어가 Smalltalk의 일부 특징을 사용한다고 해서 전체 객체 모델과 개발 방식을 그대로 계승한 것은 아니다.
Smalltalk과 다른 언어의 관계는 대체로 다음 세 방향으로 나뉜다.
- Smalltalk의 설계에 직접적인 영향을 준 선행 언어
- Smalltalk의 객체·메시지 모델을 직접 계승하거나 변형한 언어
- Smalltalk과 일부 실행 기술이나 객체 지향 개념을 공유하지만 다른 설계 전통을 따르는 언어
Simula
Simula는 Smalltalk의 객체 지향 모델에 가장 중요한 선행 언어 가운데 하나이다. Simula는 시뮬레이션 대상의 상태와 행동을 클래스와 객체로 표현하고, 상속과 가상 절차를 통해 객체별 동작을 바꾸는 구조를 제공했다.
앨런 케이는 Simula의 클래스와 객체 개념에서 큰 영향을 받았다. 특히 프로그램의 자료구조를 단순한 수동적 기록이 아니라 상태와 동작을 가진 독립된 계산 단위로 볼 수 있다는 점이 Smalltalk의 객체 모델로 이어졌다.[83]
Simula와 Smalltalk은 모두 객체를 사용해 문제 영역을 모델링하지만 객체를 바라보는 중심 관점에는 차이가 있다.
Simula에서는 클래스가 자료 구조와 절차를 정의하고, 프로그램이 그 인스턴스를 생성하여 시뮬레이션과 일반 계산에 사용한다. 객체 지향 기능은 ALGOL 계열의 명령형 언어 구조 위에 추가되었다.
Smalltalk에서는 객체와 메시지 전송이 언어 전체의 기본 계산 모델이 된다. 정수, 문자열, 클래스, 코드 블록과 실행 문맥까지 객체로 표현하며, 대부분의 연산은 객체에 보내는 메시지로 구성된다.
account deposit: 100
Smalltalk은 Simula의 클래스 기반 구조를 계승하면서 다음과 같은 방향으로 확장했다.
- 기본값과 클래스까지 포함해 거의 모든 구성 요소를 객체로 표현한다.
- 연산을 절차 호출보다 메시지 전송으로 해석한다.
- 클래스와 메서드를 실행 중인 객체로 다룬다.
- 프로그램과 개발 도구를 같은 객체 환경에 배치한다.
- 실행 중 메서드와 클래스 구조를 변경할 수 있게 한다.
- 그래픽 사용자 인터페이스와 객체 모델을 하나의 시스템으로 결합한다.
Simula의 객체는 주로 현실의 시뮬레이션 대상과 프로그램 구조를 표현하는 수단이었다. Smalltalk은 이 개념을 확장해 실행 환경, 사용자 인터페이스와 개발 도구 자체도 협력하는 객체들로 구성했다.
상속 방식에서도 두 언어는 공통점이 있다. 두 언어 모두 클래스 기반 단일 상속 계층과 동적 메서드 선택에 가까운 구조를 사용한다. 그러나 Smalltalk에서는 메서드 선택이 수신자에게 보내는 메시지의 의미와 직접 결합하며, 메서드 이름은 선택자 객체로 시스템 안에서 조사할 수 있다.
Smalltalk은 Simula를 단순히 문법적으로 단순화한 언어가 아니다. Simula의 클래스와 객체라는 구조에 Lisp 계열의 동적 실행 특성, 메시지 기반 계산과 라이브 그래픽 환경을 결합하여 별도의 객체 지향 전통을 형성했다.
Lisp와 Logo
Lisp는 Smalltalk의 동적이고 자기 기술적인 실행 환경에 큰 영향을 주었다. Lisp에서는 코드와 자료가 공통된 목록 구조로 표현될 수 있고, 실행 중 함수를 정의하거나 프로그램 구조를 조작할 수 있다. 대화형 평가 환경을 통해 표현식을 즉시 실행하고 결과를 확인하는 방식도 Smalltalk의 라이브 환경과 연결된다.
Smalltalk은 Lisp의 목록 중심 문법이나 함수형 계산 모델을 그대로 채택하지 않았다. 대신 프로그램과 실행 환경을 고정된 외부 도구가 아니라 실행 중 조사하고 변경할 수 있는 시스템으로 본다는 관점을 객체 모델에 결합했다.
두 언어는 다음과 같은 성질을 공유한다.
- 동적 타입
- 자동 메모리 관리
- 대화형 실행
- 실행 중 함수 또는 메서드 정의
- 프로그램 구조에 대한 반영
- 코드 블록이나 함수를 객체처럼 전달하는 기능
- 언어 자체를 사용한 개발 도구 구현
- 실행 중인 시스템의 점진적 확장
Lisp에서는 함수 호출과 목록 처리가 중심이지만 Smalltalk에서는 메시지 전송과 객체 협력이 중심이다.
(mapcar #'square numbers)
numbers collect: [ :each | each squared ]
두 코드는 컬렉션의 각 요소를 변환한다. Lisp에서는 고차 함수 mapcar에 함수를 전달하고, Smalltalk에서는 컬렉션에 collect: 메시지를 보내 코드 블록을 전달한다.
Smalltalk의 코드 블록은 람다 표현식과 비슷한 역할을 한다.
square := [ :number | number * number ].
square value: 5
그러나 Smalltalk에서는 조건문과 반복도 Boolean, 정수, 컬렉션과 블록 사이의 메시지로 표현된다.
condition
ifTrue: [ self performAction ]
ifFalse: [ self performAlternative ]
이 구조는 Lisp의 함수와 동적 실행 성질을 Smalltalk의 객체·메시지 체계에 맞게 재구성한 것으로 볼 수 있다.
Logo는 Smalltalk의 교육적 방향과 직접 조작 가능한 프로그래밍 환경에 영향을 주었다. Logo는 학습자가 Turtle 그래픽을 통해 명령의 결과를 즉시 보고, 작은 절차를 결합하여 복잡한 그림과 동작을 만드는 환경을 제공했다.
앨런 케이의 초기 Smalltalk 설계는 Logo, FLEX, Planner와 META II 등의 영향을 받았다. 그는 Smalltalk-71이 이러한 언어와 시스템에서 많은 영향을 받았다고 기록했다.[84]
Logo와 Smalltalk은 어린이와 비전문가가 컴퓨터를 단순한 소비 도구가 아니라 표현 매체로 사용할 수 있게 한다는 목표를 공유했다. 차이는 Logo가 절차와 Turtle을 중심으로 한 반면, Smalltalk은 화면의 그래픽 요소와 계산 대상을 객체로 구성했다는 점이다.
Smalltalk의 교육 환경에서는 화면 객체에 메시지를 보내 위치와 동작을 변경할 수 있었다.
pen
down;
go: 100;
turn: 90
이러한 메시지는 Logo의 Turtle 명령과 유사한 학습 경험을 제공하지만, Smalltalk에서는 펜과 화면 요소도 클래스, 상태와 메서드를 가진 일반 객체로 존재한다.
Objective-C
Objective-C는 Smalltalk의 메시지 전송 모델을 C (프로그래밍 언어)에 결합한 언어이다. Brad Cox와 Tom Love가 1980년대 초 개발했으며, C의 자료형·함수·포인터와 Smalltalk 계열의 객체·메시지 문법을 함께 사용할 수 있도록 설계되었다.
Apple의 Objective-C 문서는 Objective-C가 ANSI C에 객체 지향 기능을 추가한 언어이며, 추가된 기능의 대부분이 Smalltalk을 기반으로 한다고 설명한다.[85]
Smalltalk의 메시지 전송은 다음과 같이 표현된다.
account deposit: 100
Objective-C에서는 대괄호 안에 수신자와 메시지를 작성한다.
[account deposit:100];
여러 인자를 가진 Smalltalk 키워드 선택자도 Objective-C에서 비슷한 형태로 이어진다.
canvas
drawString: text
at: position
[canvas drawString:text at:position];
두 언어 모두 선택자의 각 부분이 인자의 의미를 설명한다. drawString:at:은 하나의 전체 메시지 이름이며, 실행할 메서드는 수신자의 실제 클래스와 런타임 메서드 탐색에 따라 선택된다.
Objective-C가 Smalltalk에서 받아들인 주요 요소는 다음과 같다.
- 수신자에게 메시지를 보내는 문법
- 콜론으로 인자 위치를 나타내는 다중 키워드 선택자
- 런타임 기반 동적 메서드 탐색
- 클래스 객체
- 반영 가능한 클래스와 선택자
- 메시지를 처리하지 못할 때의 전달·포워딩 구조
- 카테고리를 통한 기존 클래스 확장
- 동적 객체 런타임
그러나 Objective-C는 Smalltalk과 달리 순수 객체 지향 언어가 아니다. C의 원시 자료형, 구조체, 포인터, 함수와 직접적인 메모리 접근을 그대로 사용할 수 있다.
int count = 10;
void *buffer = malloc(1024);
Smalltalk에서는 정수와 컬렉션도 객체이며 일반적인 응용 코드가 원시 메모리 주소를 직접 조작하지 않는다.
메서드 호출 표현에서도 Objective-C는 객체 메시지와 C 함수 호출을 구분한다.
[result calculateValue];
printf("%d\n", value);
첫 번째는 Objective-C 메시지 전송이고 두 번째는 C 함수 호출이다.
Objective-C에서 nil에 메시지를 보내면 일반적으로 아무 동작도 수행하지 않고 0에 해당하는 결과를 반환한다. Smalltalk에서 nil은 UndefinedObject 계통의 실제 객체이며, 그 객체가 이해하지 못하는 메시지를 받으면 일반적인 메시지 이해 실패가 발생한다.
Objective-C는 NeXTSTEP과 이후 Apple의 Cocoa 환경에서 사용되었다. Cocoa의 클래스와 프레임워크에는 Smalltalk에서 발전한 객체 모델과 Model–View–Controller 설계의 영향도 이어졌다. Apple은 Cocoa의 Objective-C 기능이 Smalltalk에서 유래했으며, 초기 Application Kit이 객체 지향 프레임워크로 구성되었다고 설명한다.[86]
Swift는 Objective-C 런타임과 Cocoa 생태계를 이어받았지만 문법과 타입 체계는 Smalltalk보다 정적 타입 언어의 방향에 가깝다. 다만 Objective-C와의 상호 운용을 통해 선택자, 동적 메시지 전달과 Cocoa 객체 모델의 일부를 계속 사용한다.
Self
Self는 Smalltalk의 객체·메시지 모델을 더 단순하고 일관되게 만들기 위해 개발된 프로토타입 기반 객체 지향 언어이다. David Ungar와 Randall Smith가 1980년대 Sun Microsystems에서 개발했으며, 클래스 없이 객체 복제와 위임만으로 객체 구조를 구성한다.
Smalltalk에서는 객체가 클래스의 인스턴스이다.
account := BankAccount new
Self에서는 기존 객체를 복제하여 새 객체를 만든다. 객체는 슬롯을 가지며, 슬롯에는 상태나 메서드가 들어갈 수 있다. 다른 객체의 동작을 공유하려면 부모 슬롯을 통한 위임을 사용한다.
Self는 스스로를 프로토타입 기반의 동적 객체 지향 언어·환경·가상 머신으로 설명하며, 단순성, 일관성, 구체성과 라이브성을 중심 원칙으로 둔다.[87]
Smalltalk과 Self는 다음 성질을 공유한다.
- 모든 계산을 객체와 메시지로 표현하려는 방향
- 동적 타입
- 자동 메모리 관리
- 라이브 프로그래밍 환경
- 실행 중 객체 탐색
- 코드와 객체 상태의 즉각적인 변경
- 그래픽 객체 환경
- 가상 머신과 JIT 컴파일
- 블록 객체
두 언어의 가장 큰 차이는 클래스의 존재 여부이다.
Smalltalk에서는 클래스가 인스턴스의 구조와 메서드를 정의한다.
Object subclass: #Point
instanceVariableNames: 'x y'
Self에서는 구체적인 객체가 직접 슬롯을 가지고, 다른 객체를 복제하거나 부모 객체에 위임한다.
이 방식은 객체의 공통 동작을 반드시 클래스라는 추상적인 별도 객체에 배치하지 않고, 실제 사용할 수 있는 프로토타입 객체에 둔다. 새 객체는 프로토타입을 복제한 뒤 필요한 슬롯만 변경할 수 있다.
Self의 설계는 객체 지향 계산을 다음 요소로 단순화했다.
- 객체
- 이름이 붙은 슬롯
- 메시지 전송
- 프로토타입 복제
- 부모 슬롯을 통한 위임
Self의 논문은 객체를 복제하여 만들고, 객체로부터 상속하며, 메시지를 보내 메서드를 실행하는 모델을 설명한다.[88]
Self는 언어 설계뿐 아니라 동적 언어의 실행 성능에도 중요한 영향을 주었다. 수신자의 타입이 실행 중에 결정되는 메시지 전송을 빠르게 만들기 위해 적응형 컴파일, 인라인 캐시와 실행 정보 기반 최적화가 발전했다.
Self에서 발전한 다형적 인라인 캐시는 하나의 메시지 전송 위치에서 관찰되는 여러 수신자 클래스 또는 객체 형태를 기록해 동적 디스패치를 최적화한다.[89]
이러한 가상 머신 기술은 이후 Smalltalk 구현체와 Java 가상 머신, JavaScript 엔진을 포함한 여러 동적·객체 지향 실행 환경의 최적화에 영향을 주었다.
Self의 Morphic 사용자 인터페이스는 화면 요소를 직접 조작 가능한 그래픽 객체로 구성했다. Morphic은 이후 Squeak으로 이식되었고, Squeak 계통과 초기 Scratch의 그래픽 환경에도 이어졌다.
Ruby
Ruby는 Smalltalk, Lisp, Perl과 여러 객체 지향·스크립트 언어의 영향을 결합한 동적 객체 지향 언어이다. Yukihiro Matsumoto가 1990년대에 설계했으며, 프로그래머가 자연스럽고 생산적으로 사용할 수 있는 객체 지향 스크립트 언어를 목표로 했다.
Ruby 공식 설명은 숫자와 기본값을 포함한 모든 타입에 메서드와 인스턴스 변수를 부여하는 방식이 Smalltalk의 영향을 따른다고 명시한다.[90]
Smalltalk과 Ruby는 다음과 같은 특징을 공유한다.
- 동적 타입
- 자동 메모리 관리
- 거의 모든 값을 객체로 취급
- 클래스와 단일 상속
- 동적 메서드 탐색
- 코드 블록과 클로저
- 실행 중 클래스 확장
- 반영과 메타프로그래밍
- 덕 타이핑
- 메시지 이해 실패를 가로채는 기능
두 언어에서 정수도 메시지를 받는 객체이다.
5 timesRepeat: [
Transcript show: 'Smalltalk'; cr
]
5.times do
puts "Ruby"
end
컬렉션 변환도 블록과 메시지를 사용한다.
numbers collect: [ :each | each squared ]
numbers.map { |each| each * each }
Ruby의 블록 문법과 열거 메서드는 Smalltalk의 블록·컬렉션 프로토콜과 비슷한 프로그래밍 감각을 제공한다. 그러나 Ruby는 Smalltalk 문법을 그대로 사용하지 않고 Perl과 셸 스크립트에 가까운 텍스트 처리 문법, 키워드 기반 제어문과 파일 중심 실행 방식을 결합했다.
Smalltalk에서는 조건문이 Boolean 객체에 보내는 메시지이다.
condition
ifTrue: [ self run ]
ifFalse: [ self stop ]
Ruby에서는 언어 키워드를 사용한다.
if condition
run
else
stop
end
Ruby는 클래스 정의를 다시 열어 메서드를 추가하거나 교체할 수 있다.
class String
def shout
upcase + "!"
end
end
이는 실행 중 기존 클래스에 메서드를 추가할 수 있는 Smalltalk과 유사하다. 다만 Ruby의 일반적인 개발 흐름은 소스 파일을 편집하고 프로세스를 다시 실행하는 방식이며, 객체 이미지 전체를 저장하고 재개하는 Smalltalk의 이미지 기반 환경과는 다르다.
메서드를 찾지 못했을 때 Smalltalk은 doesNotUnderstand:로 이어지고, Ruby는 method_missing을 호출할 수 있다.
Proxy >> doesNotUnderstand: aMessage
^ aMessage sendTo: target
def method_missing(name, *arguments)
target.public_send(name, *arguments)
end
두 기능은 프록시, 동적 위임과 DSL 구현에 활용할 수 있다.
Ruby는 Smalltalk의 순수 객체 모델과 Lisp 계열의 메타프로그래밍, Perl의 실용적인 텍스트 처리와 스크립트 성격을 결합한 언어로 볼 수 있다. Smalltalk과 같은 객체 지향 전통을 공유하지만, 개발 환경과 프로그램 배포 방식은 일반적인 파일 기반 언어에 더 가깝다.
Python
Python과 Smalltalk은 동적 타입, 자동 메모리 관리, 객체 중심의 실행과 대화형 평가를 공유한다. Python 공식 비교 문서는 두 언어가 동적 타입과 동적 바인딩을 사용하고 모든 값을 객체로 다룬다는 공통점을 설명한다.[91]
Python은 Smalltalk보다 일반적인 명령형 언어에 가까운 문법을 사용한다.
class BankAccount:
def deposit(self, amount):
self.balance += amount
Smalltalk에서는 메서드가 클래스 브라우저의 객체 모델 안에 배치되고 키워드 선택자를 사용한다.
BankAccount >> deposit: anAmount
balance := balance + anAmount
두 언어는 다음과 같은 공통점을 가진다.
- 변수에 정적 타입 선언이 필요하지 않다.
- 객체의 실제 타입에 따라 메서드가 동적으로 선택된다.
- 클래스와 함수 또는 메서드를 실행 중 조사할 수 있다.
- 가비지 컬렉션을 사용한다.
- REPL 또는 작업 공간에서 표현식을 즉시 평가할 수 있다.
- 덕 타이핑을 널리 활용한다.
- 코드 객체와 실행 상태에 대한 반영 기능을 제공한다.
- 바이트코드 가상 머신 구현을 사용할 수 있다.
Python의 초기 구현은 소스 코드를 바이트코드로 컴파일한 뒤 가상 머신에서 실행하는 구조를 사용했다. Guido van Rossum은 Smalltalk의 바이트코드가 Python 바이트코드 설계에 영향을 주었다고 회고했다.[92]
그러나 Python의 언어 설계가 전체적으로 Smalltalk에서 직접 파생된 것은 아니다. Python은 ABC (프로그래밍 언어)의 가독성과 명령형 문법, Modula 계열의 모듈 구조, C 기반 확장 생태계와 파일 중심 실행 모델에서 큰 영향을 받았다.
Python에서는 함수가 클래스 밖에도 존재할 수 있다.
def square(number):
return number * number
Smalltalk의 전통적인 모델에서는 실행 가능한 동작이 클래스에 설치된 메서드나 코드 블록으로 표현된다.
Python은 다음과 같은 구문을 언어 자체에 포함한다.
if condition:
run()
for item in items:
process(item)
Smalltalk에서는 같은 작업이 객체에 보내는 메시지로 표현된다.
condition ifTrue: [ self run ].
items do: [ :item |
self process: item
]
Python은 모듈과 패키지, 텍스트 소스 파일을 코드 조직의 주요 단위로 사용한다. Smalltalk은 전통적으로 이미지 안의 클래스, 프로토콜과 메서드를 중심으로 코드를 탐색하며, 패키지는 이 객체들을 묶는 단위로 사용한다.
Python의 클래스와 객체는 동적으로 수정할 수 있지만, 일반적인 Python 프로세스는 종료하면 객체 상태를 잃는다. Smalltalk 이미지는 클래스, 객체와 개발 세션을 함께 저장하고 재개할 수 있다.
두 언어는 비슷한 동적 객체 기능을 제공하면서도 Python은 전통적인 운영체제·파일·명령줄 환경에 쉽게 통합되는 범용 언어를 지향하고, Smalltalk은 언어·객체 세계·개발 도구가 결합된 라이브 프로그래밍 시스템을 지향한다.
Java와 C#
Java와 C#은 Smalltalk 이후 널리 사용된 클래스 기반 객체 지향 언어이지만, Smalltalk의 객체 모델을 그대로 계승하지는 않는다. 두 언어는 C++ 계열에 가까운 문법과 정적 타입 체계를 사용하면서, 자동 메모리 관리와 가상 머신 또는 관리형 런타임을 결합했다.
Smalltalk, Java와 C#은 다음과 같은 공통 요소를 가진다.
- 클래스와 객체
- 단일 클래스 상속
- 메서드 재정의
- 동적 디스패치
- 자동 메모리 관리
- 예외 처리
- 바이트코드 또는 중간 언어
- 가상 머신 또는 관리형 런타임
- 반영 API
- 대규모 클래스 라이브러리
그러나 언어의 중심 모델에는 차이가 있다.
Smalltalk은 변수와 메서드 인자에 정적 타입을 선언하지 않는다.
Processor >> process: anObject
^ anObject transform
Java와 C#은 일반적으로 타입을 명시한다.
Result process(Transformable object) {
return object.transform();
}
Result Process(ITransformable value)
{
return value.Transform();
}
Java와 C#에서는 인터페이스나 클래스 타입을 사용해 허용되는 연산을 컴파일 시점에 검사한다. Smalltalk에서는 객체가 필요한 선택자를 이해하는지가 실행 중 메시지 전송에서 확인된다.
Smalltalk에서는 정수와 Boolean도 객체 모델 안에 포함되고, 조건 분기와 반복도 메시지로 표현된다.
condition ifTrue: [
self process
]
Java와 C#의 조건문은 언어가 직접 해석하는 문법이다.
if (condition) {
process();
}
Java와 C#은 기본 수치형과 Boolean을 객체 참조와 구분한다. 자동 박싱을 제공하지만 언어와 가상 머신 수준에서 원시값과 객체의 차이가 남아 있다. Smalltalk은 구현 내부에서 작은 정수를 즉시값으로 최적화할 수 있어도 언어 수준에서는 정수 객체에 메시지를 보낸다.
5 factorial
Smalltalk의 클래스는 실행 중인 객체이며, 클래스 정의와 메서드를 라이브 이미지 안에서 직접 교체할 수 있다. Java와 C#도 반영, 동적 코드 생성과 핫 리로드 기능을 제공하지만, 일반적인 실행 환경에서 클래스 구조와 이미 존재하는 객체 전체를 Smalltalk처럼 자유롭게 변경하는 것은 기본 모델이 아니다.
Java는 소스 코드를 바이트코드로 컴파일하고 JVM에서 실행하며, C#은 공통 중간 언어를 CLR에서 실행한다. 이 구조는 Smalltalk의 플랫폼 독립적 바이트코드와 가상 머신 모델과 형태상 유사하다. 다만 Java와 C#은 정적 검증, 파일·어셈블리 기반 배포와 명시적인 타입 메타데이터를 중심으로 설계되었다.
Smalltalk에서 가상 머신은 객체 이미지 전체를 적재한다.
객체 이미지
→ 클래스와 메서드
→ 실행 객체와 개발 도구
→ Smalltalk 가상 머신
Java와 C#에서는 일반적으로 클래스 또는 어셈블리 파일을 런타임이 로드하고 응용 프로그램 객체는 프로세스 실행 중 생성한다.
클래스·어셈블리 파일
→ 런타임 로딩
→ 응용 프로그램 객체 생성
→ 프로세스 종료 시 일반 객체 상태 소멸
Java와 C#의 통합 개발 환경은 Smalltalk에서 발전한 브라우저, 디버거, 리팩터링과 객체 검사 개념의 영향을 넓은 의미에서 공유한다. 그러나 IDE는 일반적으로 응용 프로그램과 별도의 프로세스이거나 외부 도구로 존재한다. Smalltalk에서는 브라우저와 디버거가 응용 프로그램과 같은 이미지 안의 객체로 실행된다.
Java와 C#은 Smalltalk의 순수 메시지 중심 모델보다 정적 타입 안정성, 기존 C 계열 개발자에게 익숙한 문법, 모듈식 배포와 대규모 도구 생태계를 우선한다. Smalltalk은 실행 중인 객체와 직접 상호작용하는 유연성과 라이브성을 우선한다.
JavaScript
JavaScript는 이름과 초기 마케팅과 달리 Java의 단순한 변형이 아니며, 동적 객체 모델에서는 Smalltalk와 Self 계열의 개념에 더 가까운 부분을 가진다.
JavaScript는 클래스가 아닌 객체의 프로토타입 연결을 통해 속성과 메서드를 공유하는 방식으로 시작했다. 이 구조는 Smalltalk의 클래스 기반 모델보다 Self의 프로토타입 기반 객체 모델과 직접 연결된다.
const account = {
balance: 0,
deposit(amount) {
this.balance += amount;
}
};
Smalltalk에서는 클래스가 인스턴스의 구조와 동작을 정의한다.
Object subclass: #BankAccount
instanceVariableNames: 'balance'
JavaScript와 Smalltalk은 다음 특징을 공유한다.
- 동적 타입
- 자동 메모리 관리
- 실행 중 객체 확장
- 함수 또는 블록을 값으로 전달
- 동적 메서드 탐색
- 반영 기능
- 대화형 실행 환경
- 실행 시점의 객체 구조 변경
그러나 JavaScript의 속성 접근은 메시지 전송과 구분되지 않는 일반적인 필드 조회 형태를 사용한다.
account.balance
Smalltalk은 다른 객체의 상태를 일반적으로 메시지를 통해 요청한다.
account balance
JavaScript의 함수는 객체에 속하지 않는 독립 함수로도 존재할 수 있고, this의 값은 호출 방식에 따라 달라질 수 있다. Smalltalk의 메서드에서 self는 메시지를 받은 수신자로 일관되게 결합된다.
Self에서 발전한 적응형 컴파일, 숨은 클래스와 인라인 캐시 계열의 기술은 현대 JavaScript 엔진의 동적 객체 최적화와 밀접한 관계를 가진다. 따라서 JavaScript는 언어 표면 문법보다 실행 기술과 프로토타입 객체 모델을 통해 Smalltalk 이후의 동적 객체 지향 전통과 연결된다.
현대 객체 지향 언어에 미친 영향
Smalltalk의 영향은 특정 문법을 복사한 언어에만 한정되지 않는다. 객체 지향 프로그래밍을 언어의 부가 기능이 아니라 프로그램 전체를 조직하는 중심 모델로 제시했다는 점에서 이후 언어와 개발 도구 전반에 영향을 주었다.
Smalltalk에서 확립되거나 널리 알려진 개념에는 다음이 포함된다.
- 객체가 메시지를 주고받는 계산 모델
- 클래스와 인스턴스
- 상속과 메서드 재정의
- 동적 디스패치
- 모든 값을 객체로 다루는 일관된 모델
- 코드 블록과 클로저
- 자동 메모리 관리
- 바이트코드 가상 머신
- 객체 이미지
- 실행 중 코드 수정
- 반영과 메타프로그래밍
- 통합 클래스 브라우저
- 객체 검사기
- 실행 문맥 기반 디버거
- 리팩터링 도구
- Model–View–Controller
- 테스트 주도 개발과 단위 테스트 프레임워크의 초기 확산
이 가운데 클래스와 상속은 Simula에서 먼저 등장했지만, Smalltalk은 이 개념을 메시지 전송, 그래픽 인터페이스와 라이브 개발 환경까지 확장하여 객체 지향 프로그래밍의 대표적인 형태로 보급했다.
언어별로 계승한 요소는 서로 다르다.
| 언어 | Smalltalk과 연결되는 주요 요소 |
|---|---|
| Objective-C | Smalltalk식 메시지 문법, 선택자, 동적 런타임 |
| Self | 메시지 중심 계산, 라이브 환경, 순수 객체 모델의 단순화 |
| Ruby | 모든 값의 객체화, 블록, 동적 클래스와 메타프로그래밍 |
| Python | 동적 객체 모델, 대화형 실행, 일부 바이트코드 설계 |
| JavaScript | Self를 경유한 프로토타입 객체와 동적 VM 기술 |
| Java | 클래스 기반 객체 지향, 가비지 컬렉션, 바이트코드 VM |
| C# | 관리형 객체 환경, 반영, 가비지 컬렉션과 중간 언어 |
| Swift | Objective-C·Cocoa를 통한 선택자와 객체 프레임워크의 간접 계승 |
이 표의 관계가 모두 직접적인 언어 계보를 의미하는 것은 아니다. Java와 C#의 클래스 문법은 C++ 계열과 정적 타입 언어의 영향을 강하게 받았으며, JavaScript의 객체 모델은 Smalltalk보다 Self를 직접적인 선행 언어로 삼는다. Python 역시 Smalltalk 전체보다 일부 객체·바이트코드 개념에서 영향을 받았다.
Smalltalk의 더 넓은 영향은 언어와 개발 환경을 하나의 상호작용 시스템으로 설계한 데 있다. 현대 IDE의 클래스 탐색, 메서드 검색, 객체 디버깅, 즉시 평가, 자동 리팩터링과 핫 리로드는 Smalltalk 환경에서 통합된 형태로 일찍 구현되었다.
객체 지향 프로그래밍이 널리 보급되는 과정에서 Smalltalk의 원래 메시지 중심 관점은 종종 클래스, 상속과 캡슐화라는 설명으로 축소되었다. 그러나 Smalltalk의 핵심은 객체가 내부 표현을 감추고 메시지를 통해 협력하며, 실행 중인 시스템 전체를 객체로 탐색하고 변경할 수 있다는 데 있다.
따라서 현대 언어가 클래스와 상속을 지원한다는 사실만으로 Smalltalk과 동일한 객체 모델을 가진다고 보기는 어렵다. Smalltalk의 직접적인 계승 정도는 다음 요소를 얼마나 함께 제공하는가에 따라 달라진다.
- 객체 외부의 직접적인 상태 접근을 제한하는가
- 실행 동작을 메시지 전송으로 일관되게 표현하는가
- 클래스와 메서드를 실행 중인 객체로 다루는가
- 코드와 실행 상태를 라이브 환경에서 함께 수정할 수 있는가
- 기본값과 제어 구조까지 객체 모델에 포함하는가
- 개발 도구 자체가 같은 언어와 객체 환경으로 구성되는가
Objective-C와 Ruby는 Smalltalk의 메시지·동적 객체 특성을 일반적인 파일 기반 언어에 결합했고, Self는 클래스까지 제거하여 메시지 기반 객체 모델을 단순화했다. Java와 C#은 객체 지향 구조를 정적 타입과 관리형 런타임에 결합했으며, Python은 동적 객체 모델을 전통적인 명령형 문법과 모듈 환경에 결합했다.
이처럼 Smalltalk은 하나의 후속 언어로 그대로 이어지기보다 객체 모델, 실행 기술, 개발 도구와 프로그래밍 문화가 분리되어 여러 언어와 환경에 전파되었다.
그래픽 사용자 인터페이스와 개인용 컴퓨팅
Smalltalk은 프로그래밍 언어의 실행 환경과 그래픽 사용자 인터페이스를 하나의 객체 시스템으로 결합한 초기 사례이다. 개발자는 텍스트 명령만 입력하는 대신 화면에 열린 창, 메뉴, 편집기와 그래픽 객체를 마우스로 조작하고, 같은 환경에서 클래스와 메서드를 수정할 수 있었다.
Smalltalk의 그래픽 환경은 Xerox PARC에서 개발된 Xerox Alto의 하드웨어와 결합하여 발전했다. Alto는 개인이 한 대를 전용으로 사용하는 연구용 컴퓨터로 설계되었으며, 세로 방향의 비트맵 화면, 키보드, 마우스, 교환식 디스크와 이더넷 연결을 갖추었다. PARC에서는 이러한 장치를 바탕으로 이동 가능한 창, 메뉴, 문서 편집, 그래픽 편집과 네트워크 기반 작업을 실험했다.[93]
Smalltalk은 Alto에서 실행된 유일한 프로그래밍 환경이나 그래픽 시스템은 아니었다. Alto에는 BCPL, Mesa, Lisp와 Smalltalk 환경이 함께 존재했고, Bravo 문서 편집기, Draw와 Sil 같은 그래픽 프로그램, Laurel 전자우편 시스템도 개발되었다. 따라서 현대 GUI의 모든 요소가 Smalltalk 하나에서 만들어졌다고 보는 것은 정확하지 않다. Smalltalk의 중요성은 그래픽 상호작용, 객체 모델과 프로그래밍 도구를 통합된 라이브 환경으로 구성했다는 데 있다.[94]
Xerox Alto
Xerox Alto는 1972년부터 PARC에서 개발된 연구용 개인 컴퓨터이다. 당시 대부분의 컴퓨터가 여러 사용자가 공동으로 사용하는 대형 시스템이거나 텍스트 터미널을 통해 접근하는 형태였던 것과 달리, Alto는 한 사람이 화면과 입력 장치를 직접 사용하는 작업 환경을 지향했다.
Alto의 화면은 종이 문서와 비슷한 세로 방향으로 배치되었고, 각 픽셀을 소프트웨어가 제어할 수 있는 비트맵 디스플레이를 사용했다. 문자 단말기처럼 정해진 글자만 표시하는 것이 아니라 글꼴, 선, 도형, 그림과 여러 창을 자유롭게 그릴 수 있었다.
Alto는 다음과 같은 하드웨어와 시스템 요소를 결합했다.
- 비트맵 그래픽 디스플레이
- 마우스와 키보드
- 개인별 작업을 저장하는 교환식 디스크
- 이더넷 네트워크
- 레이저 프린터 연결
- 그래픽 문서와 편집 프로그램
- 여러 프로그래밍 언어와 실행 환경
Alto의 마우스는 화면의 위치를 가리키고 객체를 선택하는 데 사용되었다. PARC의 프로그램들은 마우스 버튼마다 서로 다른 기능을 배치하거나 화면상의 텍스트와 명령을 선택하는 여러 방식을 실험했다. 당시에는 오늘날과 같은 하나의 공통 GUI 규칙이 완성되어 있지 않았으므로 프로그램마다 상호작용 방식에 차이가 있었다.[95]
Smalltalk 환경은 Alto의 비트맵 화면과 마우스를 사용하여 소스 코드 편집기, 객체 검사기, 클래스 브라우저와 실행 결과를 각각 창으로 표시했다. 사용자는 창 사이를 이동하면서 코드를 작성하고, 평가 결과를 검사하며, 실행 중 발생한 오류를 디버거에서 다룰 수 있었다.
Alto는 상용 개인용 컴퓨터로 판매되지 않았고 PARC와 Xerox 내부, 일부 대학과 연구기관에 배치되었다. 그럼에도 그래픽 화면, 마우스, 직접 조작, 네트워크와 개인용 작업 환경을 결합한 방식은 이후 개인용 컴퓨터의 설계에 큰 영향을 주었다.[96]
비트맵 그래픽과 BitBLT
Smalltalk의 그래픽 환경은 화면을 문자 셀의 집합이 아니라 픽셀의 배열로 다루었다. 이 구조에서는 문자, 창 테두리, 메뉴, 아이콘과 그림이 모두 같은 비트맵 연산을 통해 화면에 표시될 수 있었다.
댄 잉걸스는 Smalltalk 그래픽 시스템을 위해 BitBLT를 개발했다. BitBLT는 한 비트맵의 사각형 영역을 다른 위치로 복사하면서 논리 연산을 적용하는 기본 그래픽 연산이다. 이름은 Bit Block Transfer에서 유래했다.
BitBLT는 다음과 같은 작업에 사용될 수 있었다.
- 창의 일부 또는 전체 이동
- 가려졌던 화면 영역 복원
- 문자와 글꼴 그리기
- 스크롤
- 선택 영역 표시
- 그래픽 객체 복사
- 마우스 커서와 간단한 애니메이션
- 여러 비트맵의 논리적 합성
창을 이동할 때 각 픽셀을 고수준 객체 코드에서 하나씩 다시 그리는 대신, 이미 그려진 사각형 영역을 빠르게 다른 위치로 복사할 수 있었다. 문자도 별도의 문자 표시 장치가 아니라 글꼴 비트맵을 화면에 복사하는 방식으로 표현되었다.
이러한 공통 그래픽 기본 연산은 화면 요소를 동일한 비트맵 모델로 다루게 했다. 그래픽과 문자가 서로 완전히 다른 출력 체계를 사용하지 않았으므로 문서 편집기와 프로그래밍 도구 안에 다양한 글꼴과 도형을 함께 표시할 수 있었다.
Smalltalk-74 계통에서는 BitBLT와 객체 지향 가상 메모리 체계가 그래픽 환경과 객체 시스템의 성능을 개선했다. 이후 비트 블리팅은 여러 그래픽 시스템과 운영체제에서 화면 복사와 합성의 기본 기법으로 사용되었다.[97]
창과 메뉴 기반 인터페이스
Smalltalk 환경은 여러 작업을 각각의 창에 배치했다. 사용자는 클래스 브라우저, 작업 공간, 객체 검사기와 디버거를 동시에 열고 서로 겹쳐 놓을 수 있었다.
Smalltalk-80의 창은 제목 영역을 가지고 서로 겹칠 수 있었으며, 선택한 창을 앞쪽으로 가져와 작업할 수 있었다. 이러한 겹치는 창과 제목 표시 방식은 이후 그래픽 인터페이스에서 널리 사용되는 형태가 되었다.[98]
창은 단순한 화면 장식이 아니라 특정 객체와 작업 문맥을 나타냈다.
- 시스템 브라우저는 클래스와 메서드를 탐색했다.
- 작업 공간은 임의의 표현식을 작성하고 평가했다.
- 검사기는 선택한 객체의 상태를 표시했다.
- 디버거는 중단된 프로세스와 실행 문맥을 보여 주었다.
- 전사창은 프로그램의 텍스트 출력을 표시했다.
- 그래픽 응용 프로그램은 자체 모델과 화면을 창에 열었다.
Smalltalk은 화면 공간이 제한된 Alto에서 많은 명령을 제공하기 위해 팝업 메뉴를 사용했다. 사용자가 마우스 버튼을 누르면 현재 위치나 선택 대상에 맞는 메뉴가 나타났으며, 항목을 선택하면 관련 동작이 실행되었다.
메뉴의 내용은 고정된 시스템 명령만으로 이루어지지 않았다. 객체와 도구가 자신에게 필요한 명령을 정의할 수 있었으며, 메뉴 선택은 대상 객체에 메시지를 보내는 방식으로 구현될 수 있었다.
menu
add: '검사'
target: selectedObject
selector: #inspect
실제 메뉴 API는 Smalltalk 버전과 구현체마다 다르지만, 사용자 인터페이스의 동작을 객체와 메시지로 연결한다는 구조는 Smalltalk GUI의 기본 방향이었다.
직접 조작
Smalltalk의 그래픽 환경은 사용자가 대상의 이름을 명령어로만 입력하는 대신, 화면에 표시된 객체와 텍스트를 직접 선택하고 조작하는 방식을 발전시켰다.
직접 조작에서는 사용자가 작업 대상을 화면에서 보고, 마우스로 선택하거나 이동하며, 결과를 즉시 확인한다. 이 방식은 추상적인 명령 문자열과 내부 시스템 상태 사이의 간격을 줄인다.
Smalltalk 환경에서는 다음과 같은 직접 조작이 가능했다.
- 창을 선택하고 이동한다.
- 메뉴에서 현재 대상에 적용할 명령을 고른다.
- 소스 코드 일부를 선택하여 즉시 평가한다.
- 평가 결과를 같은 위치에 출력한다.
- 객체를 검사기로 열어 참조 관계를 탐색한다.
- 오류가 발생한 스택 프레임을 선택한다.
- 그래픽 객체를 화면에서 움직이거나 크기를 바꾼다.
Smalltalk에서 직접 조작은 최종 사용자용 GUI와 개발 도구에 모두 적용되었다. 프로그래머도 소스 파일과 명령줄만 다루는 사용자가 아니라 실행 중인 객체를 보고 만지는 사용자였다.
selectedObject inspect
위 메시지는 객체를 단순한 텍스트 문자열로 출력하는 대신, 객체 자체를 탐색하는 검사기 창을 연다. 개발자는 검사기에서 객체의 인스턴스 변수와 참조 객체를 선택해 객체 그래프 안으로 이동할 수 있다.
코드 편집 역시 선택과 즉시 평가를 중심으로 이루어졌다. 작업 공간에서 표현식을 선택하고 실행하거나 결과를 출력하고, 결과 객체를 검사기로 연결할 수 있었다.
이러한 구조는 프로그램의 추상적인 내부 상태를 화면상의 조작 가능한 표현으로 연결한다. 객체 지향 모델은 프로그램 내부를 구성하고, 그래픽 인터페이스는 그 객체를 보고 다루는 방법을 제공했다.
텍스트 편집과 개발 도구
Smalltalk의 그래픽 환경에서 소스 코드는 외부 텍스트 편집기로만 관리되지 않았다. 클래스 브라우저와 작업 공간의 텍스트 영역에서 코드를 작성하고 선택한 표현식을 즉시 실행할 수 있었다.
시스템 브라우저는 일반적으로 다음 항목을 서로 연결된 목록으로 표시했다.
- 시스템 분류 또는 패키지
- 클래스
- 메시지 프로토콜
- 선택자
- 메서드 소스
사용자가 클래스를 선택하면 해당 클래스의 프로토콜이 표시되고, 프로토콜을 선택하면 그 안의 메서드가 표시되었다. 파일의 줄 번호를 따라 이동하는 대신 실행 중인 언어 구조를 중심으로 코드를 탐색했다.
브라우저의 각 목록과 편집기는 별개의 화면 객체였으며, 사용자의 선택이 바뀌면 다른 객체에 메시지를 보내 표시 내용을 갱신했다.
browser selectClass: BankAccount
개발 도구는 응용 프로그램과 같은 그래픽 체계와 언어로 구현되었다. 따라서 브라우저, 검사기와 디버거도 실행 중 조사하고 변경할 수 있는 Smalltalk 객체였다.
오류가 발생하면 텍스트 기반 오류 메시지만 출력하는 대신 디버거 창에서 호출 스택과 메서드 소스, 인자와 임시 변수를 함께 볼 수 있었다. 개발자는 오류가 발생한 상태에서 코드를 수정한 뒤 실행을 다시 시도할 수 있었다.
이처럼 그래픽 사용자 인터페이스는 Smalltalk에서 최종 사용자용 응용 프로그램을 만드는 기능에 그치지 않았다. 언어 자체를 사용하고 이해하는 주요 수단이었다.
Model–View–Controller
Smalltalk-80의 사용자 인터페이스는 Model–View–Controller 구조를 사용한 초기 환경 가운데 하나이다. MVC는 응용 프로그램의 상태와 동작, 화면 표현, 사용자 입력 처리를 서로 다른 객체의 책임으로 나눈다.
- Model은 응용 프로그램의 데이터와 업무 동작을 관리한다.
- View는 모델의 상태를 화면에 표시한다.
- Controller는 키보드와 마우스 입력을 해석하고 모델 또는 뷰에 동작을 요청한다.
예를 들어 텍스트 편집기에서 문서 내용은 모델에 저장되고, 뷰는 현재 보이는 문자와 선택 영역을 그리며, 컨트롤러는 마우스 클릭과 키 입력을 처리할 수 있다.
CounterModel >> increment
count := count + 1.
self changed: #count
CounterView >> update: anAspect
anAspect = #count
ifTrue: [ self displayCount ]
위 코드는 개념적인 예이며 Smalltalk-80과 현대 구현체의 실제 API는 다를 수 있다.
MVC는 사용자 인터페이스 전체를 하나의 거대한 객체에 넣는 대신 역할별 객체가 메시지를 주고받도록 한다. 동일한 모델에 여러 뷰를 연결하거나, 모델을 유지하면서 화면 표현을 바꿀 수 있다.
Smalltalk-80의 MVC에서 컨트롤러는 마우스와 키보드 입력을 적극적으로 관리하는 별도의 객체였다. 이후 여러 프레임워크가 MVC라는 이름을 사용했지만, 웹 프레임워크와 현대 GUI에서 말하는 MVC는 Smalltalk-80의 원래 구조와 역할 분담이 다를 수 있다.
Smalltalk에서 발전한 GUI 설계는 이후 Presentation Model, Application Model과 여러 객체 지향 사용자 인터페이스 패턴으로 이어졌다.
그래픽 객체와 Morphic
초기 Smalltalk-80은 MVC 기반 창 시스템을 사용했지만, 이후 Self에서 개발된 Morphic은 화면 요소 자체를 직접 조작 가능한 객체로 구성하는 다른 접근을 제시했다.
Morphic에서 화면의 도형, 버튼, 창과 텍스트 요소는 Morph라는 객체 계층으로 표현된다. 각 Morph는 위치, 크기, 색상, 하위 Morph와 이벤트 처리 동작을 가질 수 있다.
morph := Morph new.
morph
extent: 200 @ 100;
color: Color lightBlue;
openInWorld
Morphic에서는 화면에 열린 객체를 직접 선택하고 이동하거나 속성을 변경할 수 있다. 화면 구성 요소와 그 객체 표현 사이의 관계가 가까우므로 라이브 객체 환경과 잘 결합된다.
Self의 Morphic은 이후 Squeak으로 이식되었고, Squeak 기반 Etoys와 초기 Scratch의 그래픽 환경에 영향을 주었다. Squeak은 현재도 Morphic을 그래픽·상호작용 응용 프로그램 개발을 위한 핵심 프레임워크로 제공한다.[99]
Smalltalk-80의 MVC와 Squeak의 Morphic은 같은 Smalltalk 계열 안에서도 GUI 모델이 고정되지 않았음을 보여 준다.
- MVC는 모델, 화면 표현과 입력 제어의 역할 분리에 중점을 둔다.
- Morphic은 화면 요소를 직접 조작 가능한 그래픽 객체로 표현하는 데 중점을 둔다.
- 현대 Smalltalk 구현체는 자체 위젯 체계와 이벤트 모델을 추가로 제공한다.
개인용 동적 매체
Smalltalk의 목표는 기존 대형 컴퓨터의 기능을 작은 기계로 옮기는 것에 그치지 않았다. Alan Kay와 Learning Research Group은 개인용 컴퓨터를 읽기, 쓰기, 그림, 음악, 시뮬레이션과 프로그래밍을 결합하는 동적 매체로 보았다.
Kay가 구상한 Dynabook은 개인이 휴대하며 사용하는 책 크기의 컴퓨터였다. 당시 기술로 완전한 Dynabook을 만들 수는 없었지만, Alto와 Smalltalk은 그 개념을 시험하는 연구 환경으로 사용되었다.
이 구상에서 사용자는 완성된 응용 프로그램만 소비하지 않는다. 자신의 아이디어를 프로그램으로 표현하고, 화면의 객체를 조작하며, 새로운 도구를 직접 만들 수 있어야 한다.
Smalltalk의 언어와 GUI는 이러한 목표에 맞춰 다음과 같이 결합되었다.
- 언어 문법을 비교적 작고 일관되게 유지한다.
- 실행 결과를 화면에서 즉시 확인할 수 있다.
- 그래픽 객체를 프로그램의 일반 객체로 다룬다.
- 도구와 응용 프로그램을 같은 언어로 작성한다.
- 사용자가 시스템의 내부 구조까지 탐색할 수 있다.
- 객체와 코드를 실행 중에 변경할 수 있다.
- 시스템 상태 전체를 저장하고 다시 시작할 수 있다.
Smalltalk 환경에서 프로그램은 화면 뒤에 숨은 별개의 산출물이 아니었다. 화면에 보이는 창과 도구, 내부의 클래스와 객체, 사용자가 만든 응용 프로그램이 하나의 지속적인 객체 세계를 구성했다.
이 관점은 이후의 개인용 컴퓨터가 문서 작성과 응용 프로그램 실행을 위한 장치로 발전한 것보다 더 넓은 목표를 가졌다. 사용자가 시스템 자체를 이해하고 바꿀 수 있는 매체를 지향했다.
Apple과 그래픽 개인용 컴퓨터
1979년 Apple의 개발자와 경영진은 Xerox PARC를 방문하여 Alto와 Smalltalk 계열의 그래픽 환경을 보았다. Apple은 이미 자체적으로 그래픽 인터페이스 연구를 진행하고 있었지만, PARC의 창, 마우스와 직접 조작 환경은 Lisa와 Macintosh 개발에 중요한 자극이 되었다.
Apple Lisa는 1983년에 출시된 그래픽 개인용 컴퓨터로, 창, 아이콘, 메뉴와 포인터를 마우스로 조작하는 데스크톱 환경을 제공했다. Computer History Museum은 마우스로 제어하는 창·아이콘·메뉴·포인터 형태의 GUI가 1970년대 Xerox PARC의 Alto에서 발전했다고 설명한다.[100]
Apple은 PARC에서 본 시스템을 그대로 복제한 것이 아니라 소비자와 사무 환경에 맞게 상호작용 규칙과 시각적 표현을 다시 설계했다. 메뉴 막대, 파일과 폴더의 아이콘 표현, 드래그 방식, 사용자 피드백과 일관된 응용 프로그램 지침을 상품화된 컴퓨터 환경에 맞게 정리했다.
Smalltalk과 초기 Macintosh 사이에는 인적 연결도 있었다. PARC에서 Smalltalk과 Alto 환경을 연구했던 Larry Tesler는 이후 Apple에 합류하여 Lisa와 Macintosh의 사용자 인터페이스 개발에 참여했다.
Smalltalk의 직접적인 영향은 화면 모양에만 있지 않았다. 객체 지향 응용 프로그램 프레임워크와 MVC 계열 설계는 이후 NeXTSTEP과 Cocoa 같은 Apple 플랫폼의 소프트웨어 구조에도 간접적으로 이어졌다. Objective-C는 Smalltalk식 메시지 전송을 C에 결합했고, NeXT의 Application Kit은 객체로 구성된 GUI 프레임워크를 제공했다.
다만 현대 GUI의 기원을 Smalltalk과 Apple의 단일한 전달 관계로만 설명할 수는 없다. Sketchpad, SRI의 NLS, PARC의 Alto·Bravo·Gypsy·Star, 대학 연구와 여러 상용 시스템이 서로 영향을 주며 그래픽 인터페이스를 발전시켰다. Smalltalk은 이 흐름에서 객체 지향 프로그래밍, 라이브 개발 도구와 그래픽 상호작용을 긴밀하게 결합한 위치에 있다.
Xerox Star와 사무용 인터페이스
Xerox Star는 PARC와 Xerox의 연구 결과를 상용 사무 시스템으로 발전시킨 제품이다. 1981년 출시된 Star는 문서, 폴더, 프린터와 같은 사무실의 대상을 화면의 아이콘으로 나타내고, 사용자가 마우스로 선택해 작업하도록 구성했다.
Smalltalk 환경이 프로그래밍과 실험을 위한 유연한 객체 세계에 가까웠다면, Star는 일반 사무 사용자를 위한 일관된 작업 환경에 중점을 두었다.
두 환경은 다음과 같은 공통 기반을 공유했다.
- 비트맵 디스플레이
- 마우스 입력
- 여러 창과 문서
- 네트워크와 프린터
- 화면에 표시된 대상을 선택하는 상호작용
- 그래픽 문서 편집
그러나 Star는 사용자가 프로그램의 내부 구조를 변경하는 환경이 아니라, 정해진 사무 작업을 일관된 방식으로 수행하는 제품이었다. Smalltalk은 사용자와 개발자가 시스템의 객체와 코드를 직접 변경할 수 있는 환경이었다.
이 차이는 이후 개인용 컴퓨팅이 발전한 두 방향을 보여 준다.
- 완성된 응용 프로그램과 문서를 쉽게 사용하는 그래픽 데스크톱
- 사용자가 도구와 프로그램 자체를 만들고 바꾸는 프로그래밍 환경
상용 개인용 컴퓨터 시장에서는 첫 번째 방향이 널리 보급되었다. Smalltalk이 지향한 두 번째 방향은 프로그래밍 환경, 교육용 시스템과 라이브 개발 도구에서 계속 이어졌다.
그래픽 개발 환경의 영향
Smalltalk은 GUI 자체뿐 아니라 그래픽으로 구성된 통합 개발 환경의 발전에도 영향을 주었다.
Smalltalk 환경에서는 다음 도구가 서로 연결되어 있었다.
- 클래스 브라우저
- 소스 편집기
- 작업 공간
- 객체 검사기
- 디버거
- 변경 기록
- 그래픽 응용 프로그램
- 실행 중인 객체 이미지
클래스 브라우저에서 메서드를 수정하고, 작업 공간에서 객체를 생성한 뒤, 검사기에서 상태를 보고, 오류가 발생하면 디버거로 이동하는 흐름이 하나의 환경 안에서 이루어졌다.
현대 IDE가 제공하는 다음 기능도 Smalltalk 환경에서 통합된 형태로 일찍 사용되었다.
- 클래스와 메서드별 코드 탐색
- 선택자의 구현자와 송신자 검색
- 구문을 선택하여 즉시 평가
- 실행 중 객체의 구조적 검사
- 호출 스택과 지역 변수 표시
- 디버거에서 코드 수정
- 자동 리팩터링
- 코드와 실행 상태의 연결
현대 IDE는 일반적으로 소스 파일과 외부 프로세스를 중심으로 동작하지만, Smalltalk의 도구는 실행 중인 객체 이미지 안에 존재했다. 도구가 표시하는 클래스와 메서드는 별도의 색인 데이터가 아니라 실제 실행 시스템을 구성하는 객체였다.
이러한 차이에도 불구하고 소스 코드와 실행 상태를 그래픽 도구로 탐색하고 서로 연결한다는 개발 경험은 이후 여러 언어의 IDE에서 일반적인 방식이 되었다.
사용자 인터페이스와 객체 모델의 결합
Smalltalk에서 GUI가 중요한 이유는 화면이 화려했기 때문만이 아니다. 사용자 인터페이스를 구성하는 요소와 응용 프로그램의 내부 요소가 같은 객체 모델을 사용했다.
버튼은 대상 객체에 메시지를 보낼 수 있고, 창은 모델 객체의 상태를 표시하며, 메뉴 항목은 선택자를 실행할 수 있었다.
button
target: document;
actionSelector: #save
사용자가 버튼을 누르면 document 객체에 save 메시지가 전송된다. 사용자 입력과 객체 모델 사이에 별도의 명령 문자열 해석 계층을 두기보다 메시지 전송을 직접 연결할 수 있다.
화면 요소도 일반 객체이므로 개발자는 검사기에서 그 상태를 보고 메서드를 수정할 수 있다.
window inspect
화면에 이미 열린 window 객체의 클래스에서 그리기 또는 입력 처리 메서드를 수정하면 기존 창에 변경된 동작을 적용할 수 있다.
이러한 구조는 사용자 인터페이스를 프로그램 위에 얹는 외부 표현 계층으로만 보지 않는다. GUI도 실행 중인 객체 세계의 일부이며, 다른 객체와 메시지를 교환하는 참여자로 다룬다.
현대 환경에서의 계승
Smalltalk-80의 원래 창 시스템은 현대 운영체제의 GUI와 동일하지 않지만, Smalltalk의 그래픽 환경은 여러 후속 계통에서 계속 발전했다.
- VisualWorks는 크로스 플랫폼 GUI 프레임워크를 제공한다.
- Dolphin Smalltalk는 Windows 네이티브 위젯과 시스템 API에 집중한다.
- Squeak은 Morphic을 사용해 그래픽·멀티미디어 환경을 제공한다.
- Pharo는 Morphic 계통과 새로운 Bloc·Toplo 계열 UI 기술을 발전시켜 왔다.
- Cuis는 단순화된 Morphic과 벡터 그래픽을 중심으로 한다.
- GemStone/S는 주로 서버 객체 환경으로 사용되며 클라이언트 Smalltalk GUI와 연결될 수 있다.
현대 Smalltalk GUI는 웹과 모바일 UI 생태계의 주류는 아니지만, 화면 요소를 실행 중인 객체로 탐색하고 바꾸는 라이브 개발 방식은 계속 유지된다.
Smalltalk의 GUI와 개인용 컴퓨팅에 대한 가장 큰 기여는 하나의 창 모양이나 위젯 종류를 만든 데 있지 않다. 비트맵 그래픽과 마우스, 객체 지향 언어, 라이브 개발 도구와 개인용 컴퓨터를 하나의 상호작용 가능한 시스템으로 결합한 데 있다.
이 결합은 개인용 컴퓨터가 단순히 작은 계산 장치가 아니라 문서, 그림, 프로그램과 시뮬레이션을 직접 만들고 조작하는 매체가 될 수 있다는 가능성을 보여 주었다.
표준화와 호환성
Smalltalk은 Smalltalk-80에서 공통 언어 모델이 확립되었지만, 하나의 중앙 구현체가 모든 Smalltalk 환경을 통제하는 방식으로 발전하지 않았다. Smalltalk-80을 바탕으로 여러 기업과 공개 소프트웨어 공동체가 독자적인 가상 머신, 클래스 라이브러리, 그래픽 환경과 개발 도구를 만들면서 서로 다른 방언이 형성되었다.
구현체 대부분은 객체와 메시지, 단항·이항·키워드 선택자, 코드 블록, 동적 바인딩과 단일 상속 같은 기본 원리를 공유한다. 그러나 파일 시스템, 문자열, 예외 처리, 프로세스, 그래픽 사용자 인터페이스, 패키지 관리와 외부 함수 연동은 구현체별 차이가 크다.
Smalltalk의 표준화는 이러한 방언 사이에서 공통 언어와 핵심 라이브러리 프로토콜을 정의하려는 작업이었다. 표준은 Smalltalk 시스템 전체를 하나의 제품처럼 규정하기보다, 서로 다른 구현체가 공통으로 제공할 수 있는 언어 의미와 객체 프로토콜을 정리하는 데 중점을 두었다.
따라서 Smalltalk의 호환성은 다음과 같은 여러 층으로 구분해야 한다.
- 소스 문법의 호환성
- 선택자와 핵심 프로토콜의 호환성
- 클래스 라이브러리의 호환성
- 패키지와 소스 저장 형식의 호환성
- 객체 이미지와 가상 머신의 호환성
- 그래픽 사용자 인터페이스의 호환성
- 외부 운영체제 및 라이브러리 연동의 호환성
한 계층에서 호환된다고 해서 다른 계층까지 자동으로 호환되는 것은 아니다. 예를 들어 Smalltalk-80 문법으로 작성된 계산 메서드는 여러 구현체에서 쉽게 옮길 수 있지만, Squeak의 Morphic 사용자 인터페이스를 사용하는 응용 프로그램을 VisualWorks나 Dolphin Smalltalk에서 그대로 실행하기는 어렵다.
Smalltalk-80 명세
Smalltalk-80은 Smalltalk 계열의 사실상 최초 공통 기준으로 기능했다. Xerox PARC는 Smalltalk을 외부 기관과 개발자에게 공개하면서 언어 문법, 클래스 구조, 바이트코드, 객체 메모리와 가상 머신의 기본 동작을 문서화했다.
Smalltalk-80 관련 문헌은 표지 색상에 따라 흔히 여러 이름으로 불렸다.
- Blue Book은 언어와 가상 머신 구현을 설명한다.
- Orange Book은 프로그래밍 환경과 개발 도구를 설명한다.
- Green Book은 여러 시스템으로 Smalltalk-80을 이식한 경험과 구현상의 논의를 담는다.
- Red Book은 대화형 프로그래밍 환경과 사용자 인터페이스를 다룬다.
이 가운데 Adele Goldberg와 David Robson의 《Smalltalk-80: The Language and Its Implementation》은 언어 문법, 객체 모델, 핵심 클래스와 가상 머신의 추상 명세를 제공했다. 이 문서는 독립적인 Smalltalk 구현체를 만드는 중요한 기준이 되었다.[101]
Smalltalk-80의 명세는 다음과 같은 내용을 정의하거나 설명했다.
- 객체와 클래스의 관계
- 메타클래스 구조
- 메시지 선택자와 메서드 탐색
- 단항·이항·키워드 메시지 문법
- 인스턴스 변수와 임시 변수
- 코드 블록과 실행 문맥
- 컴파일된 메서드
- 바이트코드 명령
- 원시 연산
- 객체 메모리
- 가비지 컬렉션
- 이미지 저장과 재개
- 기본 클래스 라이브러리
Smalltalk-80은 언어와 실행 환경을 함께 설명했기 때문에 단순한 문법 사양보다 범위가 넓었다. 그러나 Smalltalk-80 시스템이 발전하고 여러 회사가 독립적인 제품을 만들면서 원래 명세만으로 모든 구현체의 공통 동작을 정의하기 어려워졌다.
Smalltalk-80의 이미지와 가상 머신 형식도 시간이 지나면서 변경되었다. 새로운 객체 표현, 부동소수점 형식, 문자 집합, 메모리 크기와 운영체제 지원이 필요해지면서 각 구현체는 원래 Smalltalk-80 구조를 확장하거나 교체했다.
오늘날 Smalltalk-80은 최신 구현체가 그대로 복제해야 하는 완전한 실행 규격이라기보다, Smalltalk 계열의 언어와 객체 모델을 이해하기 위한 역사적·기술적 기준으로 사용된다.
ANSI Smalltalk
Smalltalk 구현체가 상용 및 공개 환경으로 확산되면서 공통 표준의 필요성이 커졌다. 이에 여러 Smalltalk 개발사와 전문가가 참여하여 ANSI Smalltalk 표준을 마련했다.
표준은 1998년 ANSI INCITS 319-1998, Information Systems—Programming Languages—Smalltalk로 승인되었다. 이후 미국 국가 표준을 기반으로 국제 표준화와 재확인 절차가 이어졌다.
ANSI Smalltalk은 특정 구현체의 전체 클래스 계층을 복제하도록 요구하지 않는다. 대신 Smalltalk 프로그램이 공통적으로 기대할 수 있는 언어 의미와 프로토콜을 정의한다.
표준의 범위에는 다음과 같은 요소가 포함된다.
- 어휘 구조와 문법
- 리터럴 표현
- 변수와 유효 범위
- 메시지 전송
- 메서드와 코드 블록
- 반환과 실행 의미
- 객체 생성
- 숫자 프로토콜
- 컬렉션 프로토콜
- 스트림 프로토콜
- 파일 프로토콜
- 예외 처리
- 평가 가능한 객체
- 시간과 문자 관련 기본 동작
ANSI Smalltalk은 클래스의 구체적인 내부 구현보다 객체가 제공해야 하는 프로토콜을 중심으로 기술한다. 예를 들어 컬렉션이 반드시 특정 자료구조를 사용해야 한다고 규정하기보다, 읽기 가능한 컬렉션과 확장 가능한 컬렉션이 어떤 메시지에 어떤 방식으로 응답해야 하는지를 정의한다.[102]
표준에서 다루는 주요 프로토콜에는 다음과 같은 것들이 있다.
ObjectMagnitude- 수치 객체
CharacterStringCollection- 순차 컬렉션
- 집합과 사전
- 읽기·쓰기 스트림
- 파일 스트림
- 평가 가능한 객체
- 예외 객체
표준은 다음과 같은 Smalltalk 환경 전체를 통일하지 않는다.
- 그래픽 사용자 인터페이스
- 클래스 브라우저와 디버거의 화면 구성
- 객체 이미지의 파일 형식
- 가상 머신의 내부 구현
- 패키지 관리
- 소스 코드 저장소
- 네트워크 라이브러리
- 데이터베이스 연결
- 외부 함수 인터페이스
- 운영체제별 서비스
따라서 ANSI Smalltalk을 지원하는 두 구현체도 GUI와 패키지 시스템, 클래스 이름과 부가 라이브러리에서는 크게 다를 수 있다.
ANSI 표준은 Smalltalk 구현체가 공통 코드를 교환할 수 있는 기반을 제공했지만, 모든 Smalltalk 방언을 완전히 동일하게 만들지는 않았다. 실제 호환성은 표준 프로토콜을 얼마나 충실하게 구현하고, 코드가 구현체별 확장 기능을 얼마나 사용하는지에 따라 달라진다.
문법 호환성
Smalltalk-80 계열의 구현체는 기본 문법에서 높은 공통성을 가진다.
일반적으로 다음 문법은 대부분의 Smalltalk 환경에서 같은 의미로 사용된다.
| total |
total := 0.
numbers do: [ :each |
total := total + each
].
^ total
공통적인 문법 요소에는 다음이 포함된다.
- 작은따옴표 문자열
$문자 리터럴#Symbol#()리터럴 배열:=대입^반환.문장 구분;캐스케이드[]코드 블록||임시 변수- 단항 메시지
- 이항 메시지
- 키워드 메시지
다음 메서드는 여러 Smalltalk-80 계열 구현체에서 거의 같은 형태로 작성할 수 있다.
Account >> deposit: anAmount
balance := balance + anAmount.
^ balance
그러나 메서드를 클래스에 정의하는 외부 표기에는 차이가 있다. ClassName >> selector 형식은 현대 Smalltalk 문서와 파일 기반 코드에서 널리 쓰이지만, 전통적인 브라우저에서는 클래스와 프로토콜을 화면에서 선택한 뒤 메서드 본문만 입력했다.
GNU Smalltalk은 파일에서 클래스와 메서드를 정의하기 위한 독자적인 선언 문법을 제공한다.
Object subclass: Account [
| balance |
deposit: anAmount [
balance := balance + anAmount.
^ balance
]
]
이 문법은 GNU Smalltalk의 파일 입력에 적합하지만 다른 Smalltalk 구현체에서 그대로 해석되지 않을 수 있다.
Pharo와 Squeak에서도 패키지 또는 소스 파일 표현에 따라 메서드 정의 형식이 달라질 수 있다. 따라서 메서드 본문 자체의 문법과 클래스·패키지를 파일에 저장하는 형식을 구분해야 한다.
구현체별로 추가된 문법도 있다.
- 동적 배열
{ ... } - 바이트 배열 리터럴
- 특정 숫자 리터럴
- pragma 표기
- 네임스페이스 참조
- 파일 기반 클래스 선언
- 메서드 어노테이션
- FFI 선언
예를 들어 pragma는 현대 Smalltalk 구현체에서 메서드에 메타데이터를 붙이는 데 사용된다.
WebController >> handleRequest: aRequest
<route: '/users' method: #GET>
^ self usersResponse
pragma의 기본 구문은 여러 구현체에서 유사하지만, 실제 pragma의 이름과 처리 도구는 호환되지 않을 수 있다.
핵심 프로토콜 호환성
Smalltalk 코드의 실질적인 이식성은 문법보다 사용하는 메시지 프로토콜에 더 크게 좌우된다.
다음과 같은 기본 메시지는 많은 Smalltalk 구현체에서 공통적으로 사용할 수 있다.
aCollection do: [ :each | self process: each ].
aCollection collect: [ :each | each transformed ].
aCollection select: [ :each | each isValid ].
aString size.
aNumber + anotherNumber.
anObject printString
대표적인 공통 프로토콜은 다음과 같다.
- 객체 동일성과 동등성
- 클래스 조회
- 문자열 표현
- 컬렉션 순회
- 요소 선택과 변환
- 수치 연산
- 스트림 읽기와 쓰기
- 블록 평가
- 예외 신호와 처리
- 객체 생성
이러한 프로토콜만 사용하는 핵심 도메인 코드는 구현체 사이에서 비교적 쉽게 이식할 수 있다.
Invoice >> total
^ items
inject: 0
into: [ :sum :item |
sum + item price
]
위 코드는 items, inject:into:, price와 숫자 덧셈이라는 일반적인 프로토콜에만 의존한다.
반면 다음 코드는 구현체별 API에 의존한다.
ZnServer startDefaultOn: 8080
ZnServer는 Pharo의 Zinc HTTP 구성 요소에 속하며 VisualWorks, GNU Smalltalk와 Dolphin Smalltalk에서 기본적으로 제공되지 않는다.
호환 가능한 코드를 작성하려면 구현체별 기능을 별도의 어댑터로 분리할 수 있다.
Application >> startServer
serverPlatform startOn: 8080
각 구현체는 serverPlatform 프로토콜에 맞는 서로 다른 객체를 제공할 수 있다.
클래스 라이브러리 차이
Smalltalk 구현체는 공통적인 핵심 클래스 이름을 일부 공유하지만, 전체 클래스 계층과 세부 메서드는 크게 다르다.
대부분의 Smalltalk에서 다음과 같은 클래스 또는 유사한 역할의 객체를 찾을 수 있다.
ObjectBehaviorClassMetaclassArrayOrderedCollectionSetDictionaryStringCharacterIntegerFloatStreamExceptionProcessSemaphore
그러나 다음 항목은 구현체마다 다를 수 있다.
- 클래스의 상위 클래스
- 내부 저장 구조
- 제공되는 선택자
- 메서드의 반환값
- 오류 발생 조건
- 컬렉션 변환 결과의 클래스
- 문자열의 변경 가능성
- 문자 인코딩
- 숫자 클래스의 종류
- 파일과 경로 객체
- 시간과 날짜 API
예를 들어 파일 시스템은 Smalltalk 구현체 사이에서 차이가 큰 영역이다.
어떤 구현체는 문자열 경로와 파일 스트림을 직접 사용한다.
FileStream fileNamed: 'data.txt'
현대 Pharo는 파일 참조 객체를 중심으로 API를 구성한다.
'data.txt' asFileReference readStream
VisualWorks와 Dolphin Smalltalk는 각자의 플랫폼 및 파일 클래스 계층을 제공한다.
문자열과 Symbol의 내부 표현도 다를 수 있다. 일부 구현체는 바이트 문자열과 넓은 문자열을 별도 클래스로 유지하고, 다른 구현체는 유니코드 중심 구조로 통합한다.
컬렉션 프로토콜에서도 같은 선택자가 존재하더라도 반환 클래스가 달라질 수 있다.
aCollection select: [ :each | each isValid ]
결과가 수신자와 같은 종류의 컬렉션인지, 일반적인 가변 컬렉션인지, 또는 구현체가 선택한 다른 자료구조인지는 메서드 계약과 구현체에 따라 달라질 수 있다.
예외 처리의 차이
ANSI Smalltalk은 예외 신호와 처리에 대한 공통 프로토콜을 정의하지만, 실제 예외 클래스 계층과 세부 동작은 구현체마다 다르다.
다음 형태는 여러 현대 Smalltalk에서 공통적으로 사용된다.
[
self performOperation
]
on: Error
do: [ :error |
self handle: error
]
그러나 다음 요소는 달라질 수 있다.
- 최상위 예외 클래스의 이름
- 오류와 알림의 계층
- 재개 가능한 예외의 범위
retry,resume,return의 동작- 파일·네트워크 예외 클래스
- 처리되지 않은 예외가 디버거로 연결되는 방식
구현체별 예외 클래스 이름을 도메인 코드에서 직접 사용하면 이식성이 낮아질 수 있다.
[
self openFile
]
on: FileDoesNotExist
do: [ :error |
error return: nil
]
공통 코드를 유지하려면 외부 자원 계층에서 구현체별 예외를 도메인 예외로 변환할 수 있다.
FileStorage >> load: aPath
[
^ platformFiles read: aPath
]
on: platformFiles missingFileException
do: [ :error |
DocumentNotFound signal: aPath
]
패키지와 소스 코드 형식
Smalltalk은 전통적으로 이미지 안에서 클래스와 메서드를 편집했기 때문에, 소스 코드 저장과 교환 방식이 일반적인 파일 기반 언어와 다르게 발전했다.
초기 환경에서는 클래스나 메서드를 텍스트 형태로 내보내는 file-out 형식이 사용되었다. 파일에는 클래스 정의와 메서드를 어느 클래스·프로토콜에 설치할지를 나타내는 지시 정보가 포함되었다.
전통적인 청크 형식은 느낌표(!)를 구분자로 사용한다.
!Account methodsFor: 'transactions'!
deposit: anAmount
balance := balance + anAmount.
^ balance
!
이 형식은 여러 초기 Smalltalk에서 사용되었지만, 청크 헤더와 메타데이터의 구체적인 구조는 구현체마다 달랐다.
이후 구현체별 패키지 시스템이 등장했다.
- VisualWorks는 Parcel과 Store를 사용한다.
- Squeak은 Monticello 패키지를 사용한다.
- Pharo는 Monticello 계통과 Tonel, Iceberg·Git을 사용한다.
- VA Smalltalk는 Application과 Configuration Map을 사용한다.
- Dolphin Smalltalk는 자체 패키지 시스템을 제공한다.
- GNU Smalltalk는 텍스트
.st파일과 패키지를 사용한다. - GemStone/S는 저장소 안의 클래스와 Symbol Dictionary를 중심으로 코드를 구성한다.
같은 Smalltalk 메서드라도 패키지 파일의 구조는 서로 호환되지 않을 수 있다.
Pharo에서 널리 사용하는 Tonel 형식은 클래스와 메서드를 Git에 적합한 텍스트 파일로 저장한다. Squeak의 Monticello .mcz 패키지는 스냅샷과 조상 정보를 포함하는 별도 저장 형식을 사용한다.
패키지 관리 도구는 다음 메타데이터도 함께 다룬다.
- 클래스가 속한 패키지
- 메서드 확장
- 패키지 의존성
- 버전과 작성자
- 저장소 위치
- 테스트와 그룹
- 로딩 순서
따라서 한 구현체에서 다른 구현체로 코드를 옮길 때는 메서드 소스뿐 아니라 패키지와 확장 메서드의 배치도 변환해야 할 수 있다.
네임스페이스와 전역 이름
전통적인 Smalltalk-80은 하나의 전역 이름 공간을 중심으로 구성되었다. 클래스 이름은 전역 사전에 Symbol을 키로 등록되었다.
Smalltalk at: #BankAccount
시스템이 커지면서 클래스 이름 충돌과 모듈 분리가 문제가 되었고, 여러 구현체는 서로 다른 네임스페이스 체계를 도입했다.
- VisualWorks는 네임스페이스를 제공한다.
- GemStone/S는 Symbol Dictionary와 사용자별 이름 공간을 사용한다.
- GNU Smalltalk는 네임스페이스 기능을 제공한다.
- Pharo는 오랫동안 전역 클래스 이름과 패키지를 중심으로 구성되었으며 패키지가 언어 수준 네임스페이스와 동일하지는 않다.
- Squeak 역시 기본적으로 전역 클래스 이름과 패키지 분류를 사용한다.
네임스페이스를 사용하는 코드의 표현은 구현체별로 다를 수 있다.
Finance.Account
위와 같은 표현은 특정 방언에서는 네임스페이스 참조가 될 수 있지만 모든 Smalltalk에서 동일하게 지원되는 문법은 아니다.
공통 코드에서는 지나치게 일반적인 클래스 이름을 피하고 프로젝트별 접두사를 사용하는 관례가 사용되기도 한다.
Object subclass: #BankingAccount
instanceVariableNames: 'balance'
패키지가 클래스 이름 충돌을 자동으로 해결한다고 가정해서는 안 된다. 패키지는 소스 관리 단위일 수 있지만 언어의 이름 해석 공간과 반드시 같지는 않다.
이미지 호환성
Smalltalk의 객체 이미지는 실행 중인 객체 메모리를 저장한 바이너리 파일이다. 이미지에는 클래스와 메서드뿐 아니라 일반 객체, 전역 상태와 개발 도구가 함께 저장될 수 있다.
Squeak 계통에서 전체 시스템은 객체 메모리 이미지와 이를 실행하는 가상 머신의 결합으로 구성된다.[103]
이미지 호환성에는 다음 요소가 영향을 준다.
- 객체 헤더 형식
- 포인터 크기
- 작은 정수 표현
- 바이트 순서
- 문자 객체 표현
- 컴파일된 메서드 형식
- 바이트코드 집합
- 특수 객체 배열
- 가비지 컬렉터 구조
- 필수 가상 머신 원시 연산
- 플러그인과 외부 라이브러리
같은 Smalltalk 방언 안에서도 오래된 이미지가 최신 가상 머신에서 바로 실행되지 않을 수 있다. 객체 메모리와 바이트코드 형식이 바뀌면 중간 버전의 이미지를 통해 단계적으로 마이그레이션하거나 전용 변환 도구를 사용해야 할 수 있다.
Squeak과 Pharo는 OpenSmalltalk VM 계통을 공유할 수 있지만, 두 프로젝트의 이미지가 서로 호환된다는 의미는 아니다. 클래스 라이브러리, 특수 객체, 바이트코드와 객체 메모리 요구 사항이 달라 각 이미지에 맞는 가상 머신 구성이 필요하다.
Squeak 실행 환경은 일반적으로 다음 파일을 함께 사용해 왔다.
.image는 객체 메모리 스냅샷이다..changes는 이미지에서 수행한 변경을 기록한다..sources는 기본 시스템의 소스 코드를 제공한다.
Squeak VM 설명도 이미지, 변경 파일과 시스템 소스 파일의 결합을 설명한다.[104]
이미지는 소스 코드 저장소와 달리 바이너리 객체 그래프이므로, 두 이미지 사이의 변경을 일반적인 텍스트 diff로 비교하기 어렵다. 현대 Smalltalk 프로젝트는 이미지 호환성에만 의존하지 않고 패키지 소스와 자동화 스크립트로 새 이미지를 재구성하는 방식을 함께 사용한다.
가상 머신 호환성
Smalltalk 가상 머신은 이미지가 요구하는 객체 메모리와 바이트코드 형식을 구현해야 한다. 이미지와 VM이 같은 계통이라고 해도 모든 버전 조합이 호환되는 것은 아니다.
가상 머신 호환성은 다음 항목에 따라 결정된다.
- 지원하는 이미지 버전
- 바이트코드 집합
- 객체 메모리 형식
- 32비트 또는 64비트 객체
- JIT 컴파일러의 지원
- 원시 연산과 플러그인
- 운영체제와 CPU 아키텍처
- FFI와 외부 라이브러리 ABI
OpenSmalltalk VM은 Squeak, Pharo, Cuis와 Newspeak 계통이 공유하는 가상 머신 프로젝트이지만, 실제 빌드 옵션과 플러그인, 객체 메모리 구성은 대상 이미지에 맞춰진다.
하나의 VM 실행 파일이 여러 Smalltalk 이미지를 실행할 수 있는 경우도 있지만, 이미지가 요구하는 기능을 제공해야 한다. 반대로 같은 이미지를 Windows, Linux와 macOS용 VM에서 실행하면 객체 상태와 Smalltalk 코드는 유지하면서 플랫폼별 입출력 계층만 바꿀 수 있다.
이것이 Smalltalk 이미지의 플랫폼 이식성이다. 그러나 플랫폼 이식성과 구현체 이식성은 다르다.
- 같은 Pharo 이미지를 여러 운영체제의 Pharo VM에서 실행하는 것은 플랫폼 이식이다.
- Pharo 이미지를 VisualWorks VM에서 실행하는 것은 구현체 간 이미지 이식이며 일반적으로 지원되지 않는다.
소스 코드 이식성
소스 코드의 이식성은 코드가 사용하는 계층에 따라 달라진다.
가장 이식하기 쉬운 코드는 다음 요소에 의존한다.
- 기본 객체와 메시지
- 수치 연산
- ANSI 컬렉션 프로토콜
- 코드 블록
- 일반 예외 처리
- 구현체 독립적인 도메인 객체
Statistics >> averageOf: numbers
numbers isEmpty
ifTrue: [ ^ 0 ].
^ (numbers
inject: 0
into: [ :sum :each | sum + each ])
/ numbers size
다음 기능에 의존하면 이식 작업이 증가한다.
- 파일 시스템
- 네트워크
- 데이터베이스
- 사용자 인터페이스
- 프로세스와 스레드
- FFI
- 패키지 시스템
- 반영 API
- 컴파일러 내부 구조
- 객체 이미지 관리
- VM 원시 연산
이식 가능한 응용 프로그램은 코드를 여러 계층으로 나눌 수 있다.
도메인 모델
→ 공통 Smalltalk 프로토콜만 사용
응용 서비스
→ 저장소·네트워크 인터페이스 정의
플랫폼 어댑터
→ 구현체별 파일·GUI·DB API 사용
도메인 모델은 구현체 독립적인 객체와 메시지로 작성하고 외부 기능은 주입된 서비스 객체를 통해 접근한다.
DocumentService >> save: aDocument
storage save: aDocument
storage는 구현체별 파일이나 데이터베이스 API를 감싸는 객체가 된다.
PharoFileStorage >> save: aDocument
path writeStreamDo: [ :stream |
aDocument writeOn: stream
]
VisualWorksFileStorage >> save: aDocument
"VisualWorks 파일 API를 사용한다."
이 구조는 Smalltalk 방언 사이의 차이를 제거하지 않지만, 차이가 응용 프로그램 전체로 퍼지는 것을 줄인다.
그래픽 사용자 인터페이스 호환성
그래픽 사용자 인터페이스는 Smalltalk 방언 사이에서 호환성이 가장 낮은 영역 가운데 하나이다.
주요 GUI 계통은 다음과 같다.
- Smalltalk-80의 MVC
- VisualWorks의 UI 프레임워크
- Squeak과 Pharo의 Morphic
- Pharo의 Bloc·Toplo 계통
- Dolphin의 Windows 네이티브 UI
- VA Smalltalk의 VisualAge 계통
- Cuis의 Morphic 계통
Squeak의 Morphic 코드는 같은 계통인 Cuis나 Pharo와 일부 개념을 공유하지만, 클래스와 메서드가 서로 달라 그대로 실행된다고 보장할 수 없다.
Morph new
color: Color blue;
extent: 200 @ 100;
openInWorld
이 코드는 Morphic을 제공하지 않는 VisualWorks나 GNU Smalltalk에서 기본적으로 실행되지 않는다.
GUI 이식성을 높이려면 화면 프레임워크와 도메인 모델을 분리할 수 있다.
OrderPresenter >> confirmOrder
order confirm.
view showConfirmedOrder: order
view가 제공해야 하는 프로토콜만 정의하고 구현체별 GUI 객체가 그 프로토콜을 구현하도록 구성한다.
하지만 화면 배치, 이벤트 모델, 위젯 수명 주기와 그래픽 렌더링 차이가 크기 때문에 실제 GUI 이식은 어댑터 몇 개만으로 끝나지 않을 수 있다.
바이트코드와 컴파일된 메서드 호환성
Smalltalk 소스 코드는 메서드 단위로 컴파일되어 CompiledMethod 계통의 객체가 된다. 컴파일된 메서드에는 바이트코드, 리터럴, 인자와 임시 변수 정보가 저장된다.
바이트코드 형식은 Smalltalk-80에서 명세되었지만 이후 구현체들은 새로운 기능과 최적화를 위해 확장했다.
차이가 발생하는 요소에는 다음이 있다.
- 바이트코드 번호와 의미
- 점프 범위
- 리터럴 인코딩
- 클로저 생성 방식
- 원시 연산 표기
- 추가 명령
- 메서드 헤더 형식
- JIT가 기대하는 메타데이터
따라서 한 구현체의 컴파일된 메서드 객체를 다른 구현체의 이미지로 복사해 실행하는 방식은 일반적인 이식 수단이 아니다. 소스 코드를 대상 구현체의 컴파일러로 다시 컴파일해야 한다.
targetClass compile: sourceCode
소스 코드가 호환되더라도 대상 구현체에 필요한 클래스와 선택자가 존재해야 한다.
표준 준수의 한계
Smalltalk 구현체가 ANSI Smalltalk을 지원한다고 해도 전체 코드가 자동으로 이식 가능한 것은 아니다. 표준은 핵심 언어와 프로토콜의 공통 기반이며, 실제 응용 프로그램은 표준 밖의 기능을 많이 사용한다.
대표적인 표준 외 영역은 다음과 같다.
- HTTP 서버
- JSON
- SQL 데이터베이스
- 정규 표현식
- GUI
- 이미지와 그래픽
- 패키지 저장소
- Git 연동
- 네이티브 코드 호출
- 암호화
- 비동기 입출력
- 운영체제 서비스
또한 표준 프로토콜을 제공하더라도 구현체가 추가 기능이나 다른 관례를 함께 제공하면서 코드가 특정 방언에 의존할 수 있다.
collection
parallelCollect: [ :each |
each transformed
]
parallelCollect:가 특정 구현체나 라이브러리의 확장이라면 다른 환경에서는 대체 구현이 필요하다.
표준 준수 여부를 확인할 때는 다음 사항을 구분해야 한다.
- 문법을 지원하는가
- 표준 선택자를 제공하는가
- 선택자의 동작이 표준과 같은가
- 오류 조건과 반환값이 같은가
- 표준 외 클래스에 의존하지 않는가
- 프로그램이 구현체별 초기화 과정에 의존하지 않는가
구현체 간 코드 이전
Smalltalk 프로젝트를 다른 구현체로 이전할 때는 일반적으로 다음 순서로 진행할 수 있다.
- 핵심 도메인 클래스와 테스트를 식별한다.
- 구현체별 GUI·파일·네트워크 코드를 분리한다.
- 소스를 대상 구현체가 읽을 수 있는 형식으로 변환한다.
- 클래스와 메서드 프로토콜 차이를 수정한다.
- 패키지와 네임스페이스 구조를 재구성한다.
- 예외와 외부 자원 API를 교체한다.
- 테스트를 대상 구현체에서 실행한다.
- 사용자 인터페이스와 배포 환경을 다시 연결한다.
소스 변환은 자동화할 수 있지만 전체 의미를 자동으로 보존하기는 어렵다. 특히 반영, 동적 메서드 생성, doesNotUnderstand:, pragma와 VM 원시 연산에 의존하는 코드는 대상 구현체의 실행 모델을 함께 분석해야 한다.
다음과 같은 검색을 통해 방언 의존 코드를 찾을 수 있다.
package methods select: [ :method |
dialectSpecificSelectors anySatisfy: [ :selector |
method sendsSelector: selector
]
]
실제 코드 분석 API는 구현체마다 다르다.
테스트가 충분하면 공통 동작을 유지하면서 플랫폼 계층을 하나씩 교체할 수 있다. Smalltalk의 반영과 코드 분석 도구는 클래스와 선택자 의존성을 조사하는 데 도움이 된다.
이미지 이전과 재구축
오래된 Smalltalk 시스템을 최신 버전으로 옮길 때는 이미지 파일 자체를 직접 변환하는 방식과, 소스 패키지를 새 이미지에 다시 적재하는 방식이 있다.
이미지 이전은 기존 객체와 개발 환경 상태를 가능한 한 유지하면서 객체 메모리 형식을 새 버전으로 바꾼다. 이 방식은 실행 데이터와 장기간 축적된 이미지 상태를 보존해야 할 때 필요할 수 있다.
이미지 재구축은 새 기본 이미지에 패키지와 설정을 다시 로드하여 같은 응용 프로그램을 구성한다. 현대 Smalltalk 프로젝트에서는 재현 가능한 빌드와 지속적 통합을 위해 이 방식을 함께 사용한다.
이미지 재구축에는 다음 요소가 필요하다.
- 패키지 목록
- 정확한 패키지 버전
- 외부 의존성
- 로딩 순서
- 구성 스크립트
- 데이터 마이그레이션
- 테스트 실행
- 배포 이미지 정리
ProjectLoader new
loadCorePackages;
loadApplicationPackages;
configureEnvironment;
runTests
이미지 기반 환경에서도 소스와 빌드 절차를 외부에 선언하면 개발자 개인의 이미지 상태에만 의존하지 않는 시스템을 만들 수 있다.
운영체제 간 이식성
같은 Smalltalk 구현체 안에서는 객체 이미지와 가상 머신의 분리 덕분에 운영체제 간 이식성이 높을 수 있다. Squeak은 주요 플랫폼용 실행 환경을 제공하고, Pharo 역시 크로스 플랫폼 이미지와 VM을 제공한다.[105][106]
운영체제 간에 비교적 쉽게 유지되는 요소는 다음과 같다.
- Smalltalk 클래스와 메서드
- 객체 상태
- 기본 컬렉션과 알고리즘
- 이미지 안의 개발 도구
- 플랫폼 독립적인 GUI 프레임워크
추가 작업이 필요한 요소는 다음과 같다.
- 파일 경로 표현
- 문자 인코딩
- 줄바꿈
- 파일 권한
- 동적 라이브러리 이름
- FFI 호출 규약
- 네이티브 위젯
- 클립보드와 입력기
- 화면 배율과 글꼴
- 외부 프로그램 실행
예를 들어 경로를 문자열로 직접 결합하면 운영체제별 구분자 차이에 의존할 수 있다.
path := directory , '/' , fileName
파일 참조 객체나 경로 프로토콜을 사용하면 이식성이 높아진다.
path := directory / fileName
구체적인 경로 결합 메시지는 구현체에 따라 다르지만, 문자열 조작 대신 파일 시스템 추상화를 사용하는 원칙은 공통적이다.
호환성의 실제 범위
Smalltalk 계열에서 호환성을 한 문장으로 단정하기는 어렵다. 대체로 다음과 같이 볼 수 있다.
| 대상 | 일반적인 호환성 |
|---|---|
| 기본 Smalltalk 문법 | 높음 |
| 수와 기본 객체 | 높음 |
| 핵심 컬렉션 프로토콜 | 비교적 높음 |
| 코드 블록과 메시지 전송 | 높음 |
| 예외 처리 | 중간 |
| 프로세스와 동시성 | 중간 |
| 파일과 경로 | 낮음 |
| 패키지 형식 | 낮음 |
| 네임스페이스 | 낮음 |
| GUI | 매우 낮음 |
| FFI와 운영체제 API | 매우 낮음 |
| 객체 이미지 | 같은 계통·버전에 한정 |
| 컴파일된 바이트코드 | 구현체별 |
| 개발 도구 확장 | 구현체별 |
Smalltalk의 공통성은 모든 구현체가 같은 제품이라는 데 있지 않다. 객체와 메시지, 동적 메서드 탐색, 블록과 라이브 객체 환경이라는 설계 전통을 공유한다는 데 있다.
ANSI Smalltalk은 이 공통 전통 가운데 언어와 핵심 프로토콜을 표준화했다. 그러나 각 구현체가 언어뿐 아니라 가상 머신, 객체 이미지, 라이브러리와 개발 도구를 함께 제공하기 때문에 실제 응용 프로그램의 이식성은 표준보다 넓은 환경에 의해 결정된다.
따라서 구현체 독립적인 Smalltalk 코드를 작성하려면 공통 프로토콜을 중심으로 도메인 모델을 구성하고, 파일·GUI·네트워크·데이터베이스와 같은 환경 기능을 별도의 객체 계층으로 분리하는 방식이 적합하다.
장점과 한계
Smalltalk의 장점과 한계는 언어 문법만으로 결정되지 않는다. Smalltalk은 객체와 메시지를 중심으로 한 언어, 객체 이미지, 가상 머신, 클래스 라이브러리와 라이브 개발 도구가 결합된 프로그래밍 시스템이다. 따라서 언어의 단순성, 동적 실행과 높은 생산성은 이미지 중심 개발, 구현체별 생태계 분화와 배포 방식이라는 특성과 함께 평가해야 한다.
Smalltalk은 모든 상황에 같은 방식으로 적합한 언어라기보다, 실행 중인 객체를 직접 탐색하고 도메인 모델을 빠르게 발전시키는 작업에서 강점을 가진다. 반면 네이티브 플랫폼과 방대한 외부 생태계를 우선하거나, 소스 파일과 정적 빌드를 중심으로 조직된 환경에서는 추가적인 통합과 운영 체계가 필요할 수 있다.
일관된 객체 모델
Smalltalk의 가장 큰 장점 가운데 하나는 언어의 핵심 모델이 작고 일관되어 있다는 점이다. 정수, 문자열, 컬렉션, 코드 블록, 클래스와 실행 문맥이 모두 객체로 표현되며, 대부분의 계산은 객체에 메시지를 보내는 방식으로 이루어진다.
5 factorial.
'Smalltalk' size.
numbers collect: [ :each | each squared ].
BankAccount new
위 표현식은 서로 다른 종류의 작업을 수행하지만 모두 수신자에게 메시지를 보내는 동일한 구조를 사용한다.
Smalltalk에서는 산술 연산자처럼 보이는 +도 이항 메시지이며, 조건과 반복도 Boolean, 정수, 컬렉션과 블록 객체 사이의 메시지로 구성된다.
balance > 0
ifTrue: [ self processBalance ].
items do: [ :each |
self process: each
]
이러한 일관성은 새로운 언어 기능마다 별도의 문법과 호출 규칙을 익혀야 하는 부담을 줄인다. 객체 생성, 계산, 제어 흐름과 고차 연산을 모두 객체와 프로토콜의 관점에서 이해할 수 있다.
GNU Smalltalk의 설명도 Smalltalk에서 모든 값이 객체이며, 객체의 데이터 표현과 그 객체에 가능한 연산이 분리되지 않는다고 설명한다.[107]
다만 실제 Smalltalk 가상 머신은 성능과 하드웨어 연동을 위해 작은 정수, 객체 헤더, 원시 연산과 네이티브 자원을 특별하게 처리한다. 언어 수준의 일관된 객체 모델과 구현 내부가 모든 값을 완전히 같은 형식으로 저장한다는 주장은 구분해야 한다.
메시지와 프로토콜 중심의 다형성
Smalltalk은 호출 측이 객체의 구체적인 클래스보다 객체가 이해해야 하는 메시지에 의존하도록 만든다.
documents do: [ :document |
document renderOn: canvas
]
각 문서 객체가 renderOn: 프로토콜을 제공하면 텍스트, 이미지와 벡터 문서가 서로 다른 클래스여도 같은 코드로 처리할 수 있다.
이 방식은 다음과 같은 장점을 제공한다.
- 새로운 클래스를 기존 알고리즘에 연결하기 쉽다.
- 테스트용 객체와 프록시를 만들기 쉽다.
- 공통 상위 클래스에 과도한 기능을 넣지 않아도 된다.
- 객체의 저장 구조와 호출 코드를 분리할 수 있다.
- 구현 교체를 메시지 프로토콜 유지로 처리할 수 있다.
service := FakePaymentService new.
orderProcessor paymentService: service
FakePaymentService가 실제 결제 서비스와 같은 메시지에 응답하면 명시적인 정적 인터페이스 선언 없이 시험 환경에 연결할 수 있다.
반면 프로토콜이 소스 코드에 명시적인 타입 선언으로 나타나지 않으므로, 객체가 필요한 메시지를 이해하지 못하는 문제는 실행 경로가 수행될 때 발견될 수 있다. 브라우저의 구현자·송신자 검색, 테스트와 정적 분석 도구를 함께 사용해야 대규모 코드에서 프로토콜의 계약을 안정적으로 유지할 수 있다.
라이브 프로그래밍
Smalltalk의 라이브 환경에서는 실행 중인 객체의 상태를 유지한 채 메서드를 추가하거나 수정할 수 있다.
account := BankAccount new.
account deposit: 100
개발자가 deposit: 메서드를 수정한 뒤 같은 account 객체에 다시 메시지를 보내면, 기존 잔액과 객체 관계를 유지하면서 변경된 동작을 시험할 수 있다.
Pharo는 개발자가 웹 응용 프로그램, 코드, 그래픽과 네트워크를 나타내는 살아 있는 객체와 지속적으로 상호작용하며 즉각적인 피드백을 얻는 환경으로 자신을 설명한다.[108]
라이브 프로그래밍은 다음과 같은 작업에 유리하다.
- 요구 사항을 빠르게 시험한다.
- 실제 객체 상태에서 오류를 재현한다.
- 전체 응용 프로그램을 다시 시작하지 않고 메서드를 바꾼다.
- 데이터와 화면 상태를 유지하면서 기능을 발전시킨다.
- 실행 중인 서버와 도구를 탐색한다.
- 사용자와 함께 프로토타입을 반복해서 수정한다.
오류가 발생해도 실행 문맥이 디버거에 보존될 수 있다. 개발자는 실패한 객체와 인자를 검사하고, 메서드를 수정한 뒤 해당 메시지를 다시 실행할 수 있다.
이러한 방식은 수정과 검증 사이의 시간을 줄이지만, 운영 중인 외부 시스템까지 자동으로 되돌려 주는 것은 아니다. 파일 쓰기, 결제 요청이나 네트워크 통신처럼 이미 발생한 외부 효과는 실행 문맥을 재시작하더라도 취소되지 않는다. 외부 효과가 있는 코드는 트랜잭션, 멱등성, 재시도와 보상 작업을 별도로 설계해야 한다.
강력한 개발 도구
Smalltalk은 클래스 브라우저, 작업 공간, 검사기와 디버거가 언어 및 실행 환경과 긴밀하게 결합되어 있다.
브라우저는 단순히 파일을 보여 주지 않고 실행 중인 클래스와 컴파일된 메서드를 탐색한다. 검사기는 로그용 문자열 대신 실제 객체의 인스턴스 변수와 참조 관계를 보여 준다. 디버거는 오류가 발생한 실행 문맥을 객체로 다룬다.
Smalltalk은 메시지 기반 객체 지향 언어, 그래픽 사용자 인터페이스와 프로그램 개발 환경이라는 세 측면에서 혁신적이었다고 Computer History Museum은 평가한다.[109]
개발 도구가 응용 프로그램과 같은 언어로 구현된다는 점도 장점이다. 개발자는 브라우저와 검사기 자체를 조사하고 프로젝트에 필요한 전용 도구를 만들 수 있다.
BankAccount >> gtInspectorTransactionsIn: composite
"계좌 거래를 위한 전용 검사 보기를 구성한다."
실제 확장 API는 구현체와 도구 프레임워크에 따라 다르지만, 도메인 객체에 맞는 검사·시각화 도구를 같은 환경에서 만들 수 있다는 원리는 공통적이다.
반면 도구가 구현체의 이미지와 클래스 라이브러리에 밀접하게 결합되어 있기 때문에, 한 구현체의 브라우저 확장이나 검사 보기를 다른 Smalltalk로 그대로 옮기기는 어렵다.
신속한 개발과 점진적 설계
Smalltalk은 클래스 전체나 응용 프로그램을 한 번에 완성하기보다 작은 메서드와 객체를 만들고, 실제 실행을 통해 설계를 점진적으로 발전시키는 방식과 잘 맞는다.
order := Order new.
order add: product.
order total
필요한 메시지가 아직 없으면 테스트나 작업 공간에서 먼저 사용한 뒤, 메시지 이해 실패 상태에서 메서드를 추가할 수 있다.
이러한 개발 방식은 다음과 같은 상황에 적합하다.
- 업무 규칙이 사용자 피드백에 따라 계속 바뀐다.
- 도메인의 객체와 관계를 탐색하면서 설계해야 한다.
- 초기 요구 사항만으로 전체 구조를 확정하기 어렵다.
- 그래픽 도구와 사용자 경험을 반복해서 시험해야 한다.
- 오래된 시스템의 실제 객체와 실행 흐름을 조사해야 한다.
Smalltalk의 작은 문법과 풍부한 라이브러리는 알고리즘과 도메인 모델을 비교적 짧은 코드로 표현할 수 있게 한다. 다만 짧은 코드가 자동으로 좋은 설계를 보장하지는 않는다. 동적 기능, 메서드 교체와 반영을 과도하게 사용하면 코드의 실제 동작을 추적하기 어려워질 수 있다.
반영과 확장성
클래스, 메서드, 선택자와 실행 문맥이 객체로 표현되기 때문에 프로그램은 자신의 구조를 조사하고 변경할 수 있다.
BankAccount methodDictionary keys.
BankAccount allSubclasses.
account class
이러한 반영은 다음 기능의 기반이 된다.
- 코드 브라우저
- 리팩터링
- 객체 직렬화
- 테스트 자동 생성
- ORM과 객체 데이터베이스
- 프록시와 원격 객체
- 소프트웨어 분석
- 도메인 특화 언어
- 디버거와 프로파일러
반영 기능이 언어에 자연스럽게 포함되어 있으므로 외부 컴파일러 플러그인이나 별도의 메타데이터 시스템 없이도 강력한 개발 도구를 구축할 수 있다.
반면 코드가 런타임에 메서드를 생성하거나 perform:, doesNotUnderstand:와 문자열 기반 선택자를 광범위하게 사용하면 정적 검색과 리팩터링 도구가 모든 의존성을 찾지 못할 수 있다.
object perform: selector withArguments: arguments
선택자가 실행 중 계산되면 소스 코드만으로 실제 호출 관계를 완전히 판단하기 어렵다.
객체 이미지와 지속적인 상태
객체 이미지는 클래스와 메서드뿐 아니라 개발자가 만든 객체와 작업 상태를 저장하고 다시 시작할 수 있게 한다.
이 구조의 장점은 다음과 같다.
- 개발 세션을 그대로 이어갈 수 있다.
- 복잡한 시험 데이터를 매번 다시 만들지 않아도 된다.
- 응용 프로그램과 개발 도구를 하나의 실행 상태로 배포할 수 있다.
- 가상 머신만 교체하여 여러 플랫폼에서 같은 이미지 계통을 실행할 수 있다.
- 장시간 유지되는 객체 환경을 구성할 수 있다.
그러나 이미지 안에는 실험용 객체, 임시 설정과 개발자 개인의 상태도 함께 쌓일 수 있다. 이미지 파일만으로 프로젝트를 관리하면 어떤 변경이 의도된 소스이고 어떤 변경이 우연한 실행 상태인지 구분하기 어려울 수 있다.
현대 Smalltalk 개발은 이를 보완하기 위해 패키지, Git 저장소, 자동화된 이미지 구성과 테스트를 함께 사용한다.
이미지 기반 배포는 일반적인 실행 파일이나 컨테이너와 다른 운영 방식을 요구할 수 있다. 이미지 크기, 시작 스크립트, 외부 설정, 비밀 정보와 운영 데이터의 분리도 관리해야 한다.
성능
Smalltalk의 동적 타입과 메시지 전송은 정적인 직접 호출보다 실행 시점에 더 많은 결정을 요구한다. 모든 메시지 전송에서 수신자의 클래스를 확인하고 메서드를 탐색해야 하며, 코드 블록과 객체 할당이 빈번하게 발생할 수 있다.
현대 Smalltalk 가상 머신은 다음과 같은 방식으로 이 비용을 줄인다.
- 바이트코드 해석
- JIT 컴파일
- 단형적·다형적 인라인 캐시
- 메서드 인라이닝
- 작은 정수의 즉시값 표현
- 세대별 가비지 컬렉션
- 원시 연산
- 실행 정보 기반 최적화
Squeak은 주요 플랫폼용 빠른 실행 환경을 제공하며 그래픽·상호작용 응용 프로그램을 지원한다.[110]
일반적인 업무 처리, 웹 응용 프로그램, 개발 도구와 그래픽 인터페이스에서는 현대 Smalltalk VM이 충분한 성능을 제공할 수 있다. 하지만 다음 분야에서는 네이티브 언어와 외부 라이브러리가 더 적합할 수 있다.
- 대규모 수치 계산
- GPU 중심 병렬 계산
- 하드 실시간 처리
- 운영체제 커널과 장치 드라이버
- 메모리 배치와 SIMD를 세밀하게 통제해야 하는 작업
- 플랫폼별 저수준 최적화
이러한 작업은 Smalltalk에서 전체를 직접 구현하기보다 FFI, VM 원시 연산과 C 계열 라이브러리를 연결하는 방식으로 구성할 수 있다.
동적 타입의 장점과 비용
동적 타입은 클래스 계층과 정적 인터페이스를 미리 고정하지 않고 객체의 실제 프로토콜을 기준으로 코드를 구성할 수 있게 한다.
이로 인해 다음과 같은 장점이 생긴다.
- 새로운 객체 종류를 쉽게 연결할 수 있다.
- 시험용 객체를 만들기 쉽다.
- 실행 중 클래스와 메서드를 바꿀 수 있다.
- 반영과 메타프로그래밍이 자연스럽다.
- 프로토타입을 빠르게 만들 수 있다.
반면 메시지 오타, 잘못된 인자와 누락된 프로토콜은 해당 실행 경로가 수행될 때까지 나타나지 않을 수 있다.
customer calcluateDiscount
calculateDiscount의 오타가 컴파일 과정에서 수신자의 정적 타입을 기준으로 검출되지 않을 수 있다. 실제 메시지가 실행되면 doesNotUnderstand: 오류로 드러난다.
Smalltalk 환경은 다음 도구로 이 비용을 보완한다.
- 선택자 자동 완성
- 구현자와 송신자 검색
- 단위 테스트
- 코드 분석과 린트
- 디버거
- 실행 중 객체 검사
- 선택적 타입 추론
동적 타입을 사용하는 코드베이스가 커질수록 프로토콜의 의미, 테스트와 패키지 경계를 명확히 유지하는 것이 중요하다.
생태계와 인력
현대 Smalltalk 생태계는 Java, JavaScript, Python, C#과 같은 주류 언어보다 작다. 사용할 수 있는 개발자 수, 최신 서비스용 SDK, 클라우드 플랫폼의 기본 지원과 교육 자료의 양도 상대적으로 제한적이다.
이로 인해 다음과 같은 부담이 생길 수 있다.
- Smalltalk 경험자를 채용하기 어렵다.
- 특정 외부 서비스의 공식 SDK가 없다.
- 새로운 운영체제·플랫폼 지원이 늦을 수 있다.
- 일부 라이브러리는 특정 구현체에만 존재한다.
- 유지되는 패키지와 중단된 패키지를 구분해야 한다.
- 팀 구성원이 이미지 중심 개발 방식에 적응해야 한다.
반면 VisualWorks, GemStone/S와 VA Smalltalk는 기업용 지원을 제공하고, Pharo, Squeak, Cuis와 Dolphin은 공개 공동체를 통해 개발되고 있다. GNU Smalltalk은 POSIX 환경의 스크립트와 헤드리스 처리에 초점을 둔 자유 구현체를 제공한다.[111]
생태계 규모가 작은 대신 한 환경 안에서 언어, 디버거, 객체 분석과 도구 제작을 깊게 통제할 수 있다는 장점도 있다.
구현체 간 분화
Smalltalk은 여러 방언으로 나뉘어 있으며 각 구현체의 GUI, 파일 시스템, 패키지와 라이브러리는 상당히 다르다.
기본적인 객체와 컬렉션 코드는 비교적 이식하기 쉽지만 다음 기능은 구현체 의존성이 크다.
- 사용자 인터페이스
- 네트워크
- 데이터베이스
- 파일과 경로
- FFI
- 패키지 시스템
- 네임스페이스
- 이미지 관리
- 코드 분석 도구
ANSI Smalltalk은 핵심 문법과 프로토콜을 표준화했지만 전체 플랫폼을 통일하지 않았다. 따라서 특정 Smalltalk 구현체를 선택하는 것은 컴파일러를 선택하는 것보다 런타임과 전체 응용 프로그램 프레임워크를 선택하는 것에 가깝다.
이식성을 높이기 위해서는 핵심 도메인 객체와 플랫폼 계층을 분리할 수 있다.
Application >> store: anObject
persistenceService save: anObject
persistenceService의 구현만 Pharo, VisualWorks 또는 GemStone/S 환경에 맞게 교체한다.
파일 중심 도구와의 통합
Smalltalk의 이미지 중심 개발은 Git, 코드 리뷰, 텍스트 diff와 파일 기반 빌드에 익숙한 조직에서 낯설게 느껴질 수 있다.
전통적인 이미지에서는 메서드가 각 파일에 고정되어 있지 않고 클래스의 메서드 사전에 존재한다. 메서드 소스는 변경 파일이나 외부 소스 파일, 패키지 저장소에서 관리될 수 있다.
현대 Smalltalk은 Tonel, Iceberg, Monticello, Store와 구현체별 패키지 체계를 제공하지만 다음 사항은 별도로 관리해야 한다.
- 패키지와 이미지 상태의 차이
- 메서드 확장과 소유 패키지
- 저장되지 않은 이미지 변경
- 패키지 로딩 순서
- 바이너리 이미지와 텍스트 소스의 관계
- CI에서 깨끗한 이미지 재구성
이미지 파일 전체를 Git에 커밋하는 방식보다 소스 패키지와 구성 스크립트를 관리하고, 이미지가 자동으로 재구성될 수 있게 하는 구조가 협업에 적합하다.
배포와 운영
Smalltalk 응용 프로그램은 일반적으로 해당 구현체의 가상 머신과 객체 이미지를 함께 배포한다. 이는 실행 상태와 라이브러리를 하나의 환경으로 묶을 수 있다는 장점이 있지만, 운영 조직이 익숙한 네이티브 실행 파일이나 JVM·.NET 어셈블리와 다른 관리 방식을 요구한다.
운영 시에는 다음 요소를 고려할 수 있다.
- VM과 이미지 버전의 조합
- 이미지 안의 개발 도구 제거 여부
- 외부 설정과 비밀 정보 분리
- 이미지 저장과 백업
- 로그와 모니터링
- 여러 서버 인스턴스의 코드 일관성
- 라이브 수정 사항의 소스 저장소 반영
- 이미지 마이그레이션
- 운영체제와 CPU 아키텍처 지원
Smalltalk의 라이브 수정 기능은 장애 대응에 유용할 수 있지만, 운영 이미지에서 직접 수정한 코드가 저장소와 배포 절차를 우회하면 재현성과 감사 가능성이 떨어질 수 있다. 운영 변경도 패키지와 버전 관리 체계에 다시 반영해야 한다.
적합한 활용 방향
Smalltalk의 장점이 특히 잘 나타나는 방향은 다음과 같다.
- 복잡한 업무 규칙을 객체로 모델링하는 시스템
- 요구 사항을 반복해서 구체화해야 하는 프로젝트
- 오래 실행되는 기업용 객체 시스템
- 객체 데이터베이스
- 소프트웨어 분석과 전용 개발 도구
- 프로그래밍 언어와 VM 연구
- 객체 지향 교육
- 라이브 그래픽과 대화형 환경
추가적인 통합이 많이 필요한 방향은 다음과 같다.
- 최신 모바일 플랫폼의 네이티브 앱
- 대규모 공개 클라우드 SDK에 강하게 의존하는 서비스
- 커널·드라이버와 임베디드 저수준 개발
- GPU와 SIMD 중심의 고성능 계산
- 아주 작은 단일 바이너리 배포가 필요한 환경
- 주류 언어 인력과 라이브러리를 즉시 활용해야 하는 조직
이러한 영역에서도 Smalltalk를 완전히 분리할 필요는 없다. 도메인 모델, 분석 도구나 백엔드 서비스를 Smalltalk로 만들고, 저수준 처리와 플랫폼 UI를 다른 언어의 구성 요소로 연결할 수 있다.
영향
Smalltalk의 영향은 객체 지향 프로그래밍 언어에만 한정되지 않는다. Smalltalk은 언어, 그래픽 사용자 인터페이스, 개인용 컴퓨팅, 가상 머신, 통합 개발 환경과 소프트웨어 개발 방법론을 하나의 시스템 안에서 함께 발전시켰다.
Computer History Museum은 Smalltalk-80이 독립된 구성 요소가 메시지 전송으로 상호작용하는 객체 지향 프로그래밍의 핵심 아이디어를 컴퓨터 산업 전반으로 확산시켰다고 설명한다.[112]
객체 지향 프로그래밍
클래스, 객체와 상속은 Simula에서 먼저 구현되었지만, Smalltalk은 객체 지향 프로그래밍을 언어 전체의 중심 모델로 만들었다.
Smalltalk에서 객체는 단순한 자료와 함수의 묶음이 아니라 다음 성질을 가진다.
- 내부 상태를 가진다.
- 메시지를 받는다.
- 자신의 클래스에 따라 메시지에 응답한다.
- 다른 객체에 메시지를 보내 협력한다.
- 실행 중 조사하고 변경할 수 있다.
이러한 모델은 객체 지향 프로그래밍을 대규모 프로그램의 구조와 사용자 인터페이스, 개발 도구를 함께 구성하는 방법으로 확장했다.
Smalltalk은 Objective-C, Self와 Ruby에 직접적인 영향을 주었고, Python, Java, C#과 JavaScript를 포함한 후속 언어의 객체 모델과 실행 환경에도 직접 또는 간접적으로 영향을 주었다.
Smalltalk의 메시지 중심 관점은 현대 객체 지향 설명에서 클래스와 상속에 가려지는 경우가 있다. Smalltalk에서 중요한 것은 객체의 분류 자체보다 독립된 객체가 메시지를 통해 협력한다는 점이다.
메시지 전송과 동적 디스패치
Smalltalk은 메서드 호출을 수신자에게 보내는 메시지로 설명했다.
account deposit: 100
이 표현은 실행할 함수를 고정해서 호출하는 것이 아니라, 수신자 객체가 deposit: 메시지에 어떻게 응답할지를 실행 중 결정하게 한다.
Smalltalk식 메시지 문법은 Objective-C에 직접 계승되었다.
[account deposit:100];
Ruby의 동적 메서드 탐색과 method_missing, Python의 덕 타이핑, JavaScript의 동적 속성·메서드 탐색도 객체의 구체적인 타입보다 실행 중 지원하는 동작에 의존한다는 넓은 전통과 연결된다.
순수 객체 지향 모델
Smalltalk은 숫자, Boolean, 클래스와 코드 블록을 포함해 거의 모든 값을 객체로 다루었다. 이 일관된 모델은 Ruby와 Self 같은 언어에 강하게 이어졌다.
Ruby 공식 설명은 숫자와 기본값을 포함한 모든 타입에 메서드와 인스턴스 변수를 부여하는 구조가 Smalltalk의 영향을 받은 것이라고 설명한다.[113]
Self는 Smalltalk의 메시지와 객체 모델을 계승하면서 클래스를 제거하고 프로토타입 복제와 위임으로 구조를 단순화했다. Self의 영향은 이후 JavaScript의 프로토타입 객체 모델과 동적 언어 VM 기술로 이어졌다.
코드 블록과 고차 프로그래밍
Smalltalk의 코드 블록은 실행 가능한 코드를 객체로 만들어 변수에 저장하고 인자로 전달할 수 있게 했다.
numbers
select: [ :each | each even ];
collect: [ :each | each squared ]
블록은 컬렉션 처리, 조건문, 반복, 예외 처리와 동시성에 폭넓게 사용되었다.
이러한 방식은 현대 언어의 람다 표현식, 클로저와 고차 컬렉션 API에서 일반적인 구조가 되었다.
numbers.select(&:even?).map { |each| each * each }
numbers.filter(x => x % 2 === 0).map(x => x * x);
언어별 직접적인 계보는 다르지만, 실행 가능한 동작을 값으로 전달하고 컬렉션이 반복을 관리한다는 프로그래밍 방식은 Smalltalk 환경에서 일찍부터 중심적으로 사용되었다.
가상 머신과 동적 언어 최적화
Smalltalk-80은 플랫폼 독립적인 바이트코드와 가상 머신, 객체 이미지를 결합했다. 소스 코드는 컴파일된 메서드 객체와 바이트코드로 변환되고 VM이 이를 실행했다.
이 구조는 이후 Java의 JVM, .NET의 CLR, Python 바이트코드 인터프리터와 여러 관리형 런타임과 형태상 공통점을 가진다. 다만 각 런타임의 타입 검증, 배포 단위와 객체 저장 방식은 Smalltalk과 다르다.
Smalltalk에서 파생된 Self는 동적 메시지 전송을 빠르게 실행하기 위한 적응형 컴파일과 다형적 인라인 캐시를 발전시켰다. 이러한 기술은 현대 JavaScript 엔진, JVM과 동적 언어 VM의 최적화에 영향을 주었다.
Squeak과 Pharo 계통의 OpenSmalltalk VM도 JIT 컴파일과 인라인 캐시를 사용해 Smalltalk의 동적 객체 모델을 최적화한다.
자동 메모리 관리
Smalltalk은 객체 생성이 빈번한 환경을 위해 자동 가비지 컬렉션을 기본 실행 모델로 사용했다. 개발자는 일반적인 객체의 메모리를 직접 해제하지 않고 객체 참조와 동작에 집중할 수 있었다.
이후 Java, C#, Python, JavaScript, Ruby와 여러 관리형 언어에서 자동 메모리 관리가 기본적인 기능이 되었다.
Smalltalk만이 가비지 컬렉션을 처음 만든 것은 아니며, Lisp 계열에서 먼저 발전했다. Smalltalk의 역할은 이를 순수 객체 지향 환경과 그래픽 응용 프로그램, 장시간 실행되는 이미지에 결합한 데 있다.
라이브 프로그래밍
Smalltalk은 실행 중인 프로그램을 중단된 완제품이 아니라 계속 관찰하고 변경할 수 있는 객체 시스템으로 다뤘다.
이 방식은 다음과 같은 현대 기술과 개념적으로 연결된다.
- REPL
- 핫 리로드
- 코드 교체
- 노트북 환경
- 라이브 코딩
- 대화형 디버거
- 실행 중 객체 검사
- 증분 컴파일
- 탐색적 프로그래밍
그러나 대부분의 현대 환경은 이러한 기능을 개별 도구로 제공한다. Smalltalk은 언어, 객체 상태, 개발 도구와 GUI를 하나의 이미지 안에 통합했다는 점에서 범위가 더 넓다.
Pharo는 현재도 살아 있는 객체와 지속적으로 상호작용하고 개발 과정의 모든 시점에서 즉각적인 피드백을 받는 방식을 Smalltalk 전통의 핵심으로 유지한다.[114]
그래픽 사용자 인터페이스
Smalltalk은 Alto의 비트맵 화면과 마우스를 이용해 겹치는 창, 팝업 메뉴, 텍스트 편집기, 그래픽 객체와 개발 도구를 통합했다.
Computer History Museum은 Smalltalk-80의 겹치는 창과 제목 표시 방식이 오늘날까지 그래픽 인터페이스의 표준적인 형태로 남았다고 설명한다.[115]
Smalltalk이 마우스, 창이나 GUI의 모든 구성 요소를 처음 발명한 것은 아니다. Sketchpad, NLS, Alto의 다른 응용 프로그램과 여러 연구 시스템이 함께 그래픽 인터페이스를 발전시켰다. Smalltalk의 영향은 GUI를 객체 지향 언어와 라이브 프로그래밍 환경에 깊게 통합한 데 있다.
Smalltalk과 Alto에서 발전한 상호작용 방식은 Apple Lisa와 Macintosh 개발에 영향을 주었고, 이후 그래픽 개인용 컴퓨터 환경의 확산에 기여했다.
Model–View–Controller
Smalltalk-80은 Model–View–Controller를 사용한 대표적인 초기 GUI 환경이었다. 응용 프로그램의 상태, 화면 표현과 입력 처리를 서로 다른 객체의 책임으로 나누었다.
MVC는 이후 데스크톱 GUI, 웹 프레임워크와 응용 프로그램 설계에 널리 사용되었다. 다만 현대의 MVC라는 이름은 Smalltalk-80의 원래 컨트롤러 역할과 다른 구조를 가리키는 경우가 많다.
Smalltalk의 MVC는 사용자 인터페이스 패턴뿐 아니라 객체가 역할별로 협력하도록 프로그램을 구성하는 설계 방식에도 영향을 주었다.
통합 개발 환경
Smalltalk의 클래스 브라우저, 작업 공간, 검사기와 디버거는 현대 IDE의 여러 기능을 일찍부터 하나의 환경에 결합했다.
Smalltalk 환경에서 발전한 대표적인 개발 경험은 다음과 같다.
- 클래스와 메서드 중심 탐색
- 구현자와 송신자 검색
- 선택한 코드의 즉시 평가
- 실행 중 객체 검사
- 호출 스택과 지역 변수 표시
- 디버거에서 코드 수정
- 코드 리팩터링
- 실행 상태를 유지한 점진적 개발
현대 IDE는 이러한 기능을 여러 언어에 제공하지만, 일반적으로 응용 프로그램과 별도의 외부 도구로 동작한다. Smalltalk에서는 IDE 자체가 실행 이미지의 일부이며, 개발 대상과 같은 객체 모델로 구현되었다.
리팩터링
Smalltalk의 반영 가능한 클래스와 메서드 구조는 자동 리팩터링 도구를 발전시키기에 적합했다. 1990년대 초 William Opdyke와 Ralph Johnson의 연구, Don Roberts와 John Brant가 개발한 Refactoring Browser는 동작을 유지하면서 프로그램 구조를 자동으로 바꾸는 도구를 Smalltalk 환경에서 발전시켰다.
클래스 이름 변경, 메서드 추출, 메서드 이동과 선택자 변경 같은 작업은 오늘날 여러 IDE의 기본 기능이 되었다.
리팩터링이라는 개념 자체가 Smalltalk에서 처음 나타난 것은 아니지만, Smalltalk 공동체는 이를 체계적인 개발 방법과 자동화 도구로 정립하는 데 중요한 역할을 했다.
디자인 패턴
디자인 패턴이 널리 알려지는 과정에도 Smalltalk 공동체가 중요한 역할을 했다. 《Design Patterns: Elements of Reusable Object-Oriented Software》의 예제는 C++와 Smalltalk를 함께 사용했다.
Smalltalk의 메시지·프로토콜 중심 설계는 Strategy, Observer, Composite, Decorator와 같은 객체 협력 패턴을 간결하게 표현할 수 있었다.
다만 Smalltalk에서는 코드 블록, 동적 메시지와 클래스 확장을 사용할 수 있기 때문에 정적 언어에서 별도의 클래스 구조가 필요한 패턴을 더 단순하게 구현할 수 있다.
테스트 주도 개발과 SUnit
Kent Beck은 Smalltalk 환경에서 단위 테스트 프레임워크인 SUnit을 만들었다. SUnit은 테스트 클래스, 테스트 메서드, 설정과 검증을 객체와 메시지로 구성했다.
CounterTest >> testIncrement
| counter |
counter := Counter new.
counter increment.
self assert: counter count equals: 1
SUnit의 구조는 이후 Java의 JUnit과 여러 xUnit 계열 테스트 프레임워크에 직접적인 영향을 주었다.
Smalltalk의 빠른 실행과 라이브 디버거는 작은 테스트를 작성하고 실패 상태에서 구현을 수정하는 개발 흐름과 잘 맞았다. 이는 테스트 주도 개발이 발전하는 환경적 기반 가운데 하나가 되었다.
객체 데이터베이스
Smalltalk의 객체 이미지와 지속 객체 모델은 객체 데이터베이스의 발전과도 연결되었다. GemStone/S는 Smalltalk 객체와 클래스, 참조 관계를 다중 사용자 객체 저장소에 직접 지속시키는 구조를 제공했다.
이 방식은 객체와 관계형 테이블 사이를 변환하는 객체 관계 매핑과 다른 접근을 보여 주었다. 오늘날 객체 데이터베이스는 관계형 데이터베이스만큼 널리 사용되지는 않지만, 지속 객체 그래프와 언어 통합 데이터베이스 연구에 영향을 주었다.
교육용 프로그래밍 환경
Smalltalk은 처음부터 어린이가 컴퓨터를 사용해 아이디어와 시뮬레이션을 표현할 수 있는 환경을 목표로 했다.
Squeak과 Etoys는 이 전통을 이어받아 그래픽 객체에 동작을 연결하는 교육 환경을 제공했고, 초기 Scratch는 Squeak과 Morphic을 기반으로 개발되었다.
Squeak은 현재도 교육, 멀티미디어, 게임, 연구와 상업 분야의 프로젝트에 사용되는 공개 Smalltalk 환경으로 소개된다.[116]
Smalltalk의 교육적 영향은 특정 문법보다 학습자가 실행 중인 시스템을 직접 탐색하고 수정하며 즉각적인 결과를 볼 수 있게 했다는 데 있다.
개인용 컴퓨팅
Smalltalk은 개인용 컴퓨터를 완성된 응용 프로그램을 소비하는 장치가 아니라, 사용자가 자신의 도구와 모델을 만들 수 있는 동적 매체로 보았다.
언어와 GUI, 그림, 문서, 네트워크와 프로그래밍 도구를 하나의 개인용 환경에 결합한 Smalltalk과 Alto의 연구는 이후 개인용 컴퓨팅의 발전에 영향을 주었다.
Computer History Museum은 Smalltalk이 개인용 컴퓨팅, 그래픽 사용자 인터페이스와 프로그래밍 언어를 변화시킨 환경이었다고 평가한다.[117]
상용 개인용 컴퓨터는 Smalltalk의 완전한 라이브 객체 환경보다 일반 사용자가 완성된 응용 프로그램을 쉽게 실행하는 방향으로 발전했다. 그러나 시스템을 직접 만들고 바꾸는 개인용 동적 매체라는 구상은 교육용 프로그래밍, 노트북, 라이브 코딩과 사용자 확장형 환경에서 계속 이어지고 있다.
영향의 현재적 의미
Smalltalk은 현재 주류 언어의 사용량이나 생태계 규모를 기준으로 보면 제한적인 위치에 있다. 그러나 현대 소프트웨어 개발의 여러 기본 요소는 Smalltalk이 통합된 형태로 일찍 구현했던 개념과 연결된다.
- 객체와 동적 디스패치
- 가비지 컬렉션
- 바이트코드와 가상 머신
- 클로저와 고차 컬렉션 처리
- GUI와 직접 조작
- MVC
- IDE와 객체 디버거
- 리팩터링
- 단위 테스트
- 라이브 프로그래밍
- 실행 중 반영
Smalltalk의 영향은 후속 언어가 Smalltalk 문법을 그대로 사용하는 방식보다, Smalltalk 시스템을 구성하던 각 요소가 분리되어 현대 언어와 도구에 흡수된 형태로 남아 있다.
현대 환경은 Smalltalk의 객체 지향 언어를 사용하지 않으면서도 Smalltalk이 발전시킨 IDE, 디버거, GUI 패턴, VM 최적화와 테스트 방식을 사용할 수 있다. 이 때문에 Smalltalk의 직접 사용 규모와 역사적·기술적 영향의 크기는 서로 구분해서 평가해야 한다.
관련 문서
- 객체 지향 프로그래밍
- 프로그래밍 언어
- 동적 타입
- 덕 타이핑
- 반영 프로그래밍
- 메타프로그래밍
- 메시지 전달
- 클래스
- 객체
- 상속
- 다형성
- 클로저
- 가상 머신
- 바이트코드
- JIT 컴파일
- 가비지 컬렉션
- 라이브 프로그래밍
- 통합 개발 환경
- 그래픽 사용자 인터페이스
- Model–View–Controller
- 리팩터링
- 디자인 패턴
- 테스트 주도 개발
- SUnit
- 객체 데이터베이스
- Xerox PARC
- Xerox Alto
- 앨런 케이
- 댄 잉걸스
- 아델 골드버그
- Simula
- Lisp
- Logo
- Objective-C
- Self
- Ruby
- Python
- Java
- C#
- JavaScript
- Squeak
- Pharo
- GemStone/S
- GNU Smalltalk
- Computer History Museum: Smalltalk at 50 ↩
- Computer History Museum: Dan Ingalls ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- Squeak/Smalltalk ↩
- Pharo ↩
- Computer History Museum: Xerox Alto ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- Alan Kay: The Early History of Smalltalk ↩
- Computer History Museum: Smalltalk at 50 ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- Dan Ingalls: The Evolution of Smalltalk—From Smalltalk-72 through Squeak ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- BYTE Magazine, August 1981: Smalltalk ↩
- Adele Goldberg and David Robson: Smalltalk-80—The Language and Its Implementation ↩
- ANSI Smalltalk ↩
- Dan Ingalls 외: Back to the Future—The Story of Squeak, a Practical Smalltalk Written in Itself ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- FOSDEM: Pharo—The Return of a Mythical Programming Language ↩
- Pharo by Example 9: The Pharo Object Model ↩
- Pharo Books: A Simple Reflective Object Kernel — Message Passing and Dynamic Lookup ↩
- Pharo Books: A Simple Reflective Object Kernel — Understanding Metaclasses Using Message Passing ↩
- Refactoring Support for Smalltalk Using Static Type Inference ↩
- GNU Smalltalk User’s Guide: Syntax ↩
- GNU Smalltalk User’s Guide: A Simple Overview of Smalltalk Syntax ↩
- Pharo by Example 9: Understanding Message Syntax ↩
- GNU Smalltalk User’s Guide: Code blocks ↩
- Pharo by Example 9: Method Lookup and Method Execution ↩
- GNU Smalltalk: Smalltalk Overview ↩
- Gilad Bracha, David Griswold: Strongtalk—Typechecking Smalltalk in a Production Environment ↩
- Squeak Wiki: Image Format ↩
- OpenSmalltalk ↩
- Squeak Wiki: Image Format ↩
- Squeak: Development — OpenSmalltalk VM ↩
- Squeak Wiki: Spur VM ↩
- Dan Ingalls 외: Back to the Future — The Story of Squeak ↩
- OpenSmalltalk ↩
- Pharo: Welcome to Pharo ↩
- Squeak Wiki: Basic Squeak Development Tools ↩
- Pharo Books: Testing in Pharo ↩
- Pharo by Example 9 ↩
- Pharo: Download Pharo ↩
- Draft ANSI Smalltalk Standard, Revision 1.9 ↩
- Draft ANSI Smalltalk Standard: Collection and Stream Protocols ↩
- Pharo by Example 9: Collections ↩
- Draft ANSI Smalltalk Standard: Numeric Protocols ↩
- Pharo by Example 9: Streams ↩
- Draft ANSI Smalltalk Standard: Stream and File Protocols ↩
- Deep Into Pharo: Handling Exceptions ↩
- Draft ANSI Smalltalk Standard: Exception Protocols ↩
- Draft ANSI Smalltalk Standard: Valuable Protocols ↩
- Squeak Documentation: Smalltalk-80 문헌 ↩
- Cincom Smalltalk: VisualWorks ↩
- Cincom Smalltalk: VisualWorks와 ObjectStudio ↩
- Cincom Smalltalk: Personal Use License ↩
- Cincom: Smalltalk's Past ↩
- GemTalk Systems: GemStone/S 64 Bit ↩
- GemStone/S 64 Bit Programmer's Guide ↩
- GNU Smalltalk User's Guide ↩
- GNU Smalltalk ↩
- Squeak/Smalltalk ↩
- Squeak Development Process ↩
- Pharo ↩
- Pharo Download ↩
- Object Arts: Dolphin Smalltalk 7 ↩
- Dolphin Smalltalk GitHub ↩
- Instantiations: Company History ↩
- VAST Platform ↩
- GemTalk Systems: GemBuilder for Smalltalk/VA ↩
- Squeak Wiki: History of Cuis ↩
- Cincom Smalltalk Foundation Data Sheet ↩
- Squeak/Smalltalk ↩
- Cincom Smalltalk Product Tutorials ↩
- Cincom Smalltalk Foundation Data Sheet ↩
- Cincom Smalltalk Foundation Data Sheet ↩
- GemTalk Systems: GemStone/S 64 Bit ↩
- Cincom Smalltalk Foundation Data Sheet ↩
- Pharo Books: Zinc HTTP Components ↩
- Advanced Object-Oriented Design MOOC with Pharo ↩
- Dan Ingalls 외: Back to the Future — The Story of Squeak ↩
- Squeak/Smalltalk ↩
- GNU Smalltalk ↩
- Alan Kay: The Early History of Smalltalk ↩
- Alan Kay: The Early History of Smalltalk ↩
- Apple: The Objective-C Programming Language ↩
- Apple: Cocoa Fundamentals — What Is Cocoa? ↩
- Self Handbook ↩
- David Ungar 외: Object, Message, and Performance — How They Coexist in Self ↩
- Urs Hölzle 외: Optimizing Dynamically-Typed Object-Oriented Languages With Polymorphic Inline Caches ↩
- Ruby: About Ruby ↩
- Python.org: Comparing Python to Other Languages ↩
- Guido van Rossum: The History of Python — Smalltalk의 바이트코드 영향 ↩
- Computer History Museum: Xerox Alto ↩
- Computer History Museum: Xerox Alto Source Code ↩
- Computer History Museum: Alto Neptune File Manager Screen ↩
- Computer History Museum: Xerox Alto Source Code ↩
- Computer History Museum: Introducing the Smalltalk Zoo ↩
- Computer History Museum: A Smalltalk-80 Graphical User Interface ↩
- Squeak/Smalltalk ↩
- Computer History Museum: The Lisa — Apple's Most Influential Failure ↩
- Squeak Documentation ↩
- Draft ANSI Smalltalk Standard, Revision 1.9 ↩
- Squeak Object Memory ↩
- Squeak Virtual Machine Manual Page ↩
- Squeak/Smalltalk ↩
- Pharo Documentation ↩
- GNU Smalltalk ↩
- Pharo MOOC: Live Object Programming in Pharo ↩
- Computer History Museum: Smalltalk Screenshot ↩
- Squeak/Smalltalk ↩
- GNU Smalltalk ↩
- Computer History Museum: Dan Ingalls ↩
- Ruby: About Ruby ↩
- Pharo MOOC: Live Object Programming in Pharo ↩
- Computer History Museum: A Smalltalk-80 Graphical User Interface ↩
- Squeak/Smalltalk ↩
- Computer History Museum: Making Smalltalk ↩