SmalltalkObjectsandDesign:Chapter 12
- 제 12 장 창 만들기(Building Windows)
창 만들기(Building Windows)
이번 장은 IBM Smalltalk 환경에서의 창(window) 빌드를 다룰 것이다. 간단한 추상 클래스의 도움을 약간 빌려 처음부터 창을 빌드할 예정이다. 이는 사용자 인터페이스 프로그래밍의 핵심으로 향할 것이다. 이번 장에서 사용되는 특정 클래스와 메서드는 IBM Smalltalk 에만 적용되며 다른 dialects 에선 적용되지 않는다. 윈도잉 프레임워크는 Smalltalk dialect 마다 매우 차이가 있으므로 다른 dialect 로 재해석하기란 거의 불가능하다.
이번 장에서 복잡한 창을 만드는 기법들을 사용하는 것은 비효율적이다. 다수의 복잡한 창을 구성하기 위해 오늘날 프로그래머들은 대부분 VisualAge 또는 WindowBuilder Pro (IBM Smalltalk용) 또는 VisualWorks와 (Smalltalk-80용) 같은 GUI (그래픽 사용자 인터페이스) 빌더를 사용한다. 이러한 빌더들 중 일부는 프로그래머가 사용자 인터페이스의 행위를 미세하게 조정하고자 할 때 창 컴포넌트(window components) 계층을 모호하게 만들기도 하지만 창 구성의 지루한 측면들을 간소화시킨다는 장점이 있다. 이 계층이 바로 본 장의 주제이며, GUI 빌더를 사용하든 사용하지 않든 어느 정도 이 주제를 습득하는 것이 바람직하겠다.
IBM Smalltalk 에서 처음으로 간단한 창을 빌드하기 전에 Motif 에 대해 조금 학습하고 넘어가야 한다. Motif 는 GUI 를 빌드하기 위한 표준 프로그래밍 인터페이스이다. 이는 컴퓨팅 대기업들의 협력단체인 Open Software Foundation(OSF) 에서 개발한 용어다. Motif 는 객체 지향 프로그래밍 시스템이 아니다. 대신 다수의 기본 함수들로 구성된다.
IBM Smalltalk 의 사용자 인터페이스 컴포넌트로는 이름과 arguments 가 Motif 를 상당히 잘 모방하는 메서드와 클래스로 이루어진 계층이 포함된다. 따라서 이러한 클래스들은 Motif 를 지원하는 시스템과 강한 관련성을 보이는데, 많은 UNIX 시스템이 이에 포함되는 것으로 밝혀졌다. 하지만 OS/2 와 Windows는 Motif 를 지원하지 않는다; 대신 자체 창 매니저(window manager)가 구비되어 있다. 그럼에도 불구하고 이러한 플랫폼들을 비롯해 UNIX 에서 IBM Smalltalk 는 공통된 Motif 와 같은(Motif-like) 계층을 통합함으로써 계층에 빌드된 애플리케이션들이 세 가지 플랫폼들-UNIX, Windows, 그리고 OS/2-간에 이식이 쉽도록 보장한다. 게다가 이러한 Motif 계층은 궁극적으로 기본이 되는 플랫폼의 윈도잉 시스템으로 번역하므로 사용자 인터페이스의 모양과 느낌이 플랫폼의 본래 모양과 느낌을 따른다.
Motif에 관해 알아야 할 것
전형적인 Motif는 자체가 다른 표준 X-Windows 의 맨 상위에 빌드된다. X-Windows 인터페이스는 Xlib 또는 X/Library라 불리는 함수 호출의 컬렉션으로 구성된다. Motif는 X toolkit의 일례다; X toolkits는 Xt Intrinsics (X toolkit Intrinsics)라 불리는 인터페이스에 빌드되고 결과적으로 Xlib로부터 빌드된다. 따라서 전형적인 Motif 계층은 다음과 같은 모습이다:
IBM Smalltalk는 Motif[1] 서비스처럼 생긴 스몰토크 클래스와 메서드 계층을 나타내지만 운영체제의 GUI 서비스 최상위에 빌드된다. 다시 말해, IBM Smalltalk Motif 계층은 기본 플랫폼이 무엇을 제공하든, raw OS/ 또는 Windows GUI 서비스, 또는 실제 Motif toolkit 이나 위의 그림과 같은 Motif window manager(mwm) 중 무엇이든 그 곳에 빌드된다는 말이다. IBM Smalltalk의 그림은 다음과 같다:
두 가지 다른 축약어-Cw와 Cg-는 IBM Smalltalk의 UI 클래스에 공통적으로 발생한다. "Cw"는 공통 위젯(common widget)을 의미한다. Widget[2]이란 X 프로그래머들이 여느 UI 컴포넌트-버튼, 스크롤바, 텍스트 필드, 라벨 등-든 사용하는 단어다. IBM Smalltalk에서 Motif의 위젯에 상응하는 것은 공통 위젯이라고 알려져 있는데, 그들의 클래스명 앞에는 "Cw"가 붙어 있다. 이와 유사하게 그래픽-그리기, 비트맵, 폰트, 색상 팔레트-을 위한 Xlib 내 함수들은 공통 그래픽(common graphics)을 뜻하는 "Cg"라는 단어가 앞에 붙어 스몰토크 클래스로 구성되어 왔다.
처음으로 빌드할 창은 'Hello San Francisco' 를 표시하는 텍스트 위젯을 포함할 것이다. 아래에 myWindow 라고 표기된 이 창은 물론 간단할 테지만 인스턴스 변수 관계로 얽힌 여러 개의 Motif 위젯에 의존한다:
텍스트 위젯이 그림에서 맨 아래에 위치함을 볼 수 있다. 이는 CwForm의 자식임을 주목한다. 폼(form)은 컨테이너 위젯으로 주로 여러 개의 다른 위젯을 포함할 것이지만, 당신의 애플리케이션에선 'Hello San Francisco' 문자열을 표현하기 위해 CwText라는 하나의 텍스트 위젯만 포함할 것이다. 그림에서 최상위 수준에는 CwTopLevelShell가 위치한다. 이름에서 알 수 있듯이 다른 모든 위젯들 위에 "shell"을 형성하는 Motif 위젯이다. Main window(CwMainWindow) 또한 컨테이너다; 그 폼과 더불어 애플리케이션이 필요로 할 경우 메뉴 바를 포함할 수 있다.
하단 좌측에 위치한 callback 객체도 주목하라. 콜백, 좀 더 정확히 말해 콜백 핸들러는 창의 크기조정이나 창 노출과 같은 이벤트에 의해 트리거된다. 위젯은 적절한 콜백 핸들러를 실행하여 이러한 이벤트에 응답할 수 있다. 콜백에 관해서는 추후에 좀 더 논하겠다.
위의 그림은 도식적이다. 실제 인스턴스 변수명은 다소 가독성이 덜하다. 예로, cwChild, cwParent, xmNChildren과 같은 이름을 사용한다.
위 그림에서 각 Motif 와 같은(Motif-like) 공통 위젯은 유사하지만 좀 더 기본적인(primitive) 객체를 캡슐화하고, 이는 다시 window handle이라 알려진 실제 운영체제의 기본 객체를 캡슐화한다:
이러한 기본 객체들은 공통 위젯을 실제 운영체제로 결합한다. 다행히도 필요한 코드를 모두 공통 위젯 수준에서 작성할 수 있으며, 나머지는 자신의 명령대로 실행하기 위해 올바르게 캡슐화되었음을 신뢰할 수 있다.
위젯 리소스
위젯은 각 애플리케이션마다 맞춰 조정되어야 한다. 맞춤설정(Customization)은 위젯의 위치부터 버튼 누름과 같은 이벤트에 대한 응답 방식까지 모두를 아우른다. 위젯을 맞춤설정하기 위해서는 적절한 맞춤설정 메시지를 위젯으로 전송해야 한다. 맞춤설정은 위젯의 리소스를 설정하는 것으로도 알려져 있다. 여기 위젯 w에 대한 리소스를 설정하는 예를 소개하겠다:
w topAttachment: XmATTACHFORM | w의 상단면을 그 폼의 상단면에 부착한다. |
w bottomAttachment: XmATTACHPOSITION; bottomPosition: 10 "percent" |
w의 하단면을 그 폼의 상단면에서 1/10만큼 아래로 부착한다. |
w topAttachment: XmATTACHWIDGET; topWidget: anotherWidget |
w의 상단면을 anotherWidget이라는 다른 위젯(하단면)으로 부착한다. |
w editmode: XmMULTILINEEDIT | w가 CwText의 인스턴스라고 가정하고, 텍스트의 한 행이 아니라 다수의 행을 처리하도록 하라. |
w value: self myMethod | w가 CwText의 인스턴스라고 가정하고, 그것이 포함한 텍스트를 myMethod가 리턴하는 문자열로 설정하라. |
w items: self yourMethod | w가 CwList의 인스턴스라고 가정하고, 리스트의 항목들을 yourMethod가 리턴하는 문자열의 컬렉션으로 설정하라. |
w addEventHandler: ButtonPressMask receiver: self selector: #pushMe:clientData:callData: clientData: nil; |
w가 CwPushButton의 인스턴스라고 가정하고, 그 응답을 pushMe:clientData:callData:라는 이름의 메서드가 정의한 버튼 누름으로 설정하라. |
특이한 모양의 상수, XmATTACHFORM, ButtonPressMask 등등은 pool dictionary 에서 정의되는데, 이는 다음 절에서 설명하겠다.
살펴보기: pool dictionaries
외부 시스템으로 통신하는 소프트웨어라면 모두 외부 시스템이 사용하는 것과 동일한 저수준 표시기를 (주로 비트나 정수로 된 플래그 또는 마스크) 사용해야 한다. 예를 들어, OS/2에서 짙은 파란색(dark blue)은 9로 표시되고 적색은 2로 표시된다; OS/2에서 실행되는 어느 소프트웨어든 동일한 색상을 표시하려면 이와 동일한 숫자를 사용해야 한다. 물론 가능한 한 숫자는 적게 생각하고 ClrDarkblue나 ClrRed와 같은 이름으로 지칭하길 원할 것이다. 이와 유사하게 XmATTACHWIDGET 와 같은 Motif 리소스 값은 임의 정수(arbitrary integer)보다 기억하기 쉽다. (XmATTACHWIDGET은 Motif에서 3이다.)
스몰토크 pool dictionaries는 이와 같은 상수를 번들(bundle)하는 데에 편리한 객체들이다. Pool dictionaries는 'ClrDarkblue'와 같은 문자열을 키로 가지고 9와 같은 값을 가지는 dictionary들이다. (IBM Smalltalk에서 pool dictionaries는 키(key)로 문자열만 받아들이는 특수 클래스 EsPoolDictionary의 인스턴스들이다.) 예를 들어, 전역 변수 PlatformConstants는 색상 상수를 비롯해 다수의 다른 상수들도 포함하는 pool dictionary를 가리킨다. 전역 변수 CwConstants는 Motif 위젯에 대한 리소스 값을 포함하는 pool dictionary를 일컫는다.
여기까지 우리는 pool dictionary를 특이한 방식으로 사용하진 않았다. 그 엔트리 중 하나로 접근하고 싶다면 다른 dictionary의 엔트리를 참조할 때 아래와 같은 문을 작성해야 한다:
widget topAttachment: (CwConstants at: 'XmATTACHWIDGET')
그러면 3 을 argument 로 전달하는 효과가 발생하지만, 물론 코드에 3 을 작성하고싶은 생각은 없다.
Pool dictionary의 매력은 그것을 클래스 정의의 일부로 명시할 때 나타난다:
Object subclass: #MyClass
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: 'CwConstants '
이제 메서드 내 어떤 엔트리로 접근하고 싶다면 아래와 같이 간단하게 작성할 수 있다:
widget topAttachment: XmATTACHWIDGET
dictionary를 명시하거나 at: 메시지를 사용할 필요가 없는 것이다! 스몰토크 컴파일러는 pool dictionaries에서 'XmATTACHWIDGET' 문자열에 일치하는 키를 검색한다. CwConstants에서 키를 찾으면 관계(association)를 컴파일된 메서드로 컴파일한다.
간단하게 말하자면, pool dictionaries는 엔트리를 참조하고자 할 때마다 dictionary 이름을 입력해야 하는 수고를 덜어줌으로써 코드를 좀 더 간결하게 만든다.
Pool dictionaries의 특징을 몇 가지 들어보겠다:
- 키가 문자열로 되어 있다.
- 값이 주로 정수이다. (항상 그런 것은 아니다. Pool dictionary PlatformFunctions를 검사한 후 그 엔트리 중 하나를 더블 클릭하라.)
- 전역 변수가 그것을 참조한다. (그렇지 않으면 클래스를 정의할 때 그것을 명시하는 이름이 없다.)
- Dictionary는 사전에 완전히 지정(fully populated)되어 있어야 한다. (키가 없을 경우 그것을 사용하는 메서드는 컴파일할 수 없다.)
Pool dictionaries에 대한 이해를 증진시키기 위해서는 몇 가지 간단한 예제를 시도해보라:
❏ ButtonPressMask의 기본 값은 무엇인가? ClrYellow는 어떤가?
❏ 아래와 같이 pool dictionary를 구성하라:
Smalltalk declarePoolDictionary: #MyPool.
MyPool at: 'ABC' put: 55.
이제 Object의 서브클래스 MyClass를 빌드하되, MyPool을 pool dictionary로 명시할 것을 주의한다. 아래 인스턴스 메서드를 작성하라:
test
" Test by displaying:
MyClass new test
"
^ABC
결과를 예측하고 예상을 확인하기 위해 실험을 행하라. ABC 대신 XYZ를 리턴하는 테스트 메서드를 작성한다면 어떻게 될까?
기술적 호기심: pool dictionary는 일반 dictionary, 즉 Dictionary의 인스턴스로서 생(life)을 시작할 수 있다. 단, 이를 pool dictionary로서 사용하는 즉시 IBM Smalltalk는 자동으로 이것을 EsPoolDictionary의 인스턴스로 변환한다.
연습: 첫 번째 창
첫 번째 창의 목표는 아래와 같다:
❏ 새 애플리케이션을 생성하거나 기존의 애플리케이션에 하나를 추가하라. EsBaseTools를 포함하도록 선행조건(prerequisite) 애플리케이션을 변경하여 EtWindow 클래스로 접근할 수 있도록 하라. HelloWindow 서브클래스를 EtWindow로 추가하라.
❏ 검사를 위해 example 클래스 메서드를 작성하라:
example
"
HelloWindow example
"
^HelloWindow new open
❏ 자신이 원하는 문자열로 답을 하는 인스턴스 메서드를 작성하라:
myHello
^' Hello San Francisco'
❏ 추상 클래스에는 그 서브클래스들이 특정 의무를 충족시킬 것이란 기대가 함께 온다는 사실을 기억하라. 추상 클래스 EtWindow의 경우, 그 서브클래스들은 createWorkRegion 메서드를 구현해야 한다. 자신의 HelloWindow클래스에 이 메서드를 작성하라:
createWorkRegion
| textWidget|
textWidget := workRegion
createText: 'My text widget'
argBlock:
[:w | w
editMode: XmMULTILINEEDIT;
value: self myHello;
leftAttachment: XmATTACHFORM;
rightAttachment: XmATTACHFORM;
topAttachment: XmATTACHFORM;
bottomAttachment: XmATTACHFORM].
textWidget manageChild
이 메서드는 실제보다 더 엄청나 보인다. 중요한 메시지들은 블록 내 리소스 설정 메시지들이다. 마지막 섹션에서 메시지의 목적을 인지할 수 있어야 한다.
❏ 마지막으로 자신의 example 메서드를 실행하여 HelloWindow 클래스를 시험하라.
연습: 잔고를 표시하는 창
제 8장에서 개발한 bank account(은행 계좌)에 간단한 사용자 인터페이스를 추가하려고 한다. 트랜잭션 로그와 현재 잔고를 각각 포함하는 구분된 위젯과 함께 account를 표시하는 창을 빌드하고 싶다. 세 가지 단계로 진행할 것이다: 첫째, 잔고만 표시하는 창, 다음으로 트랜잭션 로그만 표시하는 창, 마지막으로 두 위젯을 이용해 잔고와 로그를 모두 표시하는 창.
❏ 가장 먼저, 초반에 실행한 account 연습 시 작성한 것과 같은 적절한 애플리케이션을 먼저 선택하여 EtBaseTools를 포함하도록 선행조건(prerequisite)을 변경한다. 서브클래스 BalanceWindow를 EtWindow로 추가하라. 이 연습과 앞의 "Hello" 연습의 차이는 창이 account 객체를 알아야 한다는 필요성이겠다. 이를 위해선 BalanceWindow에 account라는 인스턴스 변수를 생성하라.
❏ BalanceWindow에 이 클래스 메서드를 작성하여 테스트 사례를 준비하라:
example
"
BalanceWindow example
"
^BalanceWindow new openOn: Account example.
이러한 테스트 사례는 91 페이지에서 개발한 Account의 클래스 메서드 example을 재사용한다.
❏ Account를 argument로 취하는 openOn: 인스턴스 메서드가 필요하게 될 것을 주목한다. 이 메서드를 작성하라. 두 가지 작업만 수행할 것이다: 인스턴스 변수를 설정하고 창을 여는 작업 (self open을 통해).
❏ myHello 대신 계좌 잔고를 리턴하는 적절한 메서드를 작성하라. 리턴된 객체는 문자열이어야 한다는 점을 잊지 마라. 다시 말해, 정수 잔고(balance)는 문자열로 변환할 것을 잊지 마라.
❏ 마지막으로 createWorkRegion 메서드를 작성하고, example 메서드를 실행하여 검사하라.
연습: 트랜잭션 로그를 표시하는 창
이번 연습에서는 하나의 컬렉션을 완전히 새로운 컬렉션으로 변형해야 한다. 루프를 작성하는 것도 한 가지 방법이 되겠다. 하지만 객체 지향 개발자들이 컬렉션을 처리하기 위해 루프를 쓰는 경우는 드물다. 풍부한 클래스 라이브러리가 컬렉션의 내용을 처리하는 강력한 메서드를 (심지어 iterators라 불리는 객체도) 갖고 있기 때문이다. 스몰토크에서 Collection>>collect:는 컬렉션에서 각 요소를 처리하고 결과가 되는 객체를 새 컬렉션으로 넣는다.
❏ 아래 행을 하나씩 표시하라:
(OrderedCollection with: 3 with: 2 with: 1) collect: [:x | x squared].
(SortedCollection with: 3 with: 2 with: 1) collect: [:x | x squared].
❏ 앞의 연습에서 밟은 단계를 흉내내라: LogWindow라는 클래스부터 시작해 example 클래스 메서드를 작성하고, openOn: 인스턴스 메서드를 작성하라.
❏ 당신의 createWorkRegion 메서드는 계좌 잔고를 표시하는 메서드와 비슷해야 한다. 하지만 그것은 텍스트 위젯을 빌드했고, 이번 것은 리스트 위젯을 빌드해야 한다. 따라서 코드 일부는 아래와 같아야 한다:
listWidget := workRegion
createList: 'My log widget'
argBlock:
[:w |
w
items: self myLog;
leftAttachment: XmATTACHFORM;
...
].
createWorkRegion 메서드를 작성하라.
❏ 코드를 완성하기 위해서는 자신의 createWorkRegion 메서드에 명시된 myLog 인스턴스 메서드를 작성해야 한다. myLog는 collect:를 이용해 트랜잭션 로그를 문자열의 OrderedCollection로 변형해야 한다.
❏ 자신의 창이 트랜잭션을 시간 순으로 표시하는지 검사하기 위해선 example 메서드를 실행하라.
연습: 두 위젯을 모두 포함하는 창
❏ AccountWindow라는 클래스를 빌드하라. 이 창은 앞에 소개한 연습들의 개념을 결합하여 잔고와 트랜잭션을 모두 표시해야 한다.
평가: 창 만들기
Motif를 이용해 간단한 창을 만들어내는 데에 무엇이 필요한지 충분히 경험했을 것이다. 일반적으로는 적절한 추상적 window 클래스에서 서브클래싱한다. EtWindow를 예로 들 수 있는데, 이는 IBM Smalltalk에서 당신이 사용하는 모든 일상생활의 브라우저와 툴에 대한 추상적 window 클래스이기 때문이다. 이론상 EtWindow는 고객에게 배달될 제품에서 슈퍼클래스로서 사용하기에 최선의 선택은 아니다. 그 이유는 개발 당시 (development-time) windows의 기반으로 디자인되었기 때문이며, 제품의 코드는 개발 당시 코드에 의존해선 안 되기 때문이다. 사실 Et는 "Envy Tools"를 의미하는데, 여기서 Envy는 팀 프로그래밍 기능의 묶음으로서, 다른 스몰토크 환경에서도 이용 가능하다.
아니면 WidgetWindow 추상 클래스에서 시작할 수도 있는데, 이는 CwExamples 애플리케이션과 그 선행조건을 로딩하면 쉽게 발견할 수 있다. WidgetWindow를 슈퍼클래스로서 사용하기 위해서는 연습에서 작성한 createWorkRegion 대신 createWindow 메서드를 작성해야 할 것이다.
또 다른 시작점으로, WbApplication이라는 클래스를 (공개 애플리케이션 WbApplicationFramework의 일부) 들 수 있는데, 이는 제 3사 제품 WindowBuilder Pro의 추상적 window 클래스이다; createWorkRegion 대신 addWidget을 오버라이드하면 된다. 따라서 WbApplication은 수동으로 만든 창뿐만 아니라 WindowBuilder Pro GUI 빌더가 생성한 창의 슈퍼클래스 역할을 할 수 있다.
또는, 자신만의 간단한 추상 클래스를 작성함으로써 시작할 수 있는데, 여기에는 최상위 수준의 셸(shell), 메인 창, 창에서 즐기고 싶은 다른 표준 장치를 생성하기 위한 코드도 작성해야겠다.
이는 모두 창을 만드는 데에 타당한 접근법들이다. 심지어 조심스럽게 위치하는 다수의 위젯으로 구성된 정교한 창들을 빌드할 때에도 사용할 수 있다. 하지만 앞서 언급했듯이 그러한 창을 설계할 때는 GUI 빌딩 툴을 사용하는 편이 아마 가장 나을 것하다. IBM Smalltalk의 경우 VisualAge 또는 WindowBuilder Pro가 될 것이다. VisualAge는 시각적 프로그래밍의 매력을 제공하는데, 그말인즉슨 스몰토크의 코드를 작성하지 않고도 사용자 인터페이스와 모델 컴포넌트를 연결하여 작동하는 로직을 생성할 수 있음을 의미한다. WindowBuilder Pro는 더 적은 수의 객체 계층을 사용하므로 효율성은 물론이거니와 Motif 위젯으로 작업해야 할 경우 위젯으로의 접근성까지 얻게 되는 장점이 있다.
IBM Smalltalk에서 Motif 프로그래밍에 관한 추가 내용을 훌륭하게 기술한 제품 매뉴얼로 [IBM 1995]와 [Objectshare 1995]가 있다.
콜백과 이벤트
여기까지 당신의 창은 어떤 입력에도 응답하지 않기 때문에 그다지 실용적이지 않을 것이다. 실용적인 창은 마우스 액션이나 운영체제로부터의 다른 이벤트에 (또는 X 이벤트) 응답한다. 여기에는 키보드의 입력, 마우스 이동, 창의 크기 조정이나 노출, 창에 포커스 주기 등이 포함된다. 이벤트는 운영체제가 물리적 장치로부터 감지한 자극을 당신의 위젯으로 전달하는 것으로 생각할 수 있다. (X-Windows에서 이러한 서비스를 책임지는 시스템의 컴포넌트는 X 서버로 알려진다.)
위젯은 이벤트에 민감하지 않은 한 이벤트에 응답하지 않는다. 위젯을 이벤트에 민감하도록 만들기 위해서는 이벤트 핸들러를 구축해야 한다. 이벤트 핸들러는 다음으로 소개할 연습에서 작성하겠다.
때로는 고수준 또는 "인위적" 이벤트로 생각하는 편이 편리할지 모른다. P를 들어, 버튼 위젯을 두 개의 이벤트, 즉 버튼 누름에 이어 버튼 해제로 구성된 "클릭"으로 민감화(sensitize)하길 원하는 것은 합리적이며, 두 개의 이벤트는 모두 위젯 위에서 발생해야 한다. 클릭은 프로그래머의 추상화 수준을 올바른 순서와 장소에서 발생하는 마우스 누름 및 해제에 대한 미가공 하드웨어의 문제로 올려 놓는다. Motif와 다른 윈도잉 시스템에서 이러한 인위적 이벤트는 콜백이라고 알려져 있다. (CwPushButton 위젯의 클릭에 대한 콜백은 activateCallback 또는 XmNactivateCallback으로 알려진다.)
프로그래머는 이벤트에 대해 이벤트 핸들러를 작성하고, 콜백에 대해서는 콜백 핸들러를 작성한다. 이벤트 핸들러와 콜백 핸들러 모두 예상한 자극(stimuli)이 발생할 때마다 실행되는 특수 메서드이다. "콜백"이라는 이름을 붙인 이유는 자극이 발생 시 운영체제는 "당신의 위젯을 다시 불러서(call back)" 위젯에게 그것의 핸들러를 실행할 수 있는 기회를 제공하기 때문이다. 콜백은 누군가 나중에 당신에게 전화할 것을 알고 대기하는 것과 비슷하다: 콜백이 발생할 것을 기대하고 준비하지만 언제 발생할지는 확실히 모른다.
이벤트와 콜백은 유사하다. 프로그래머는 핸들러를 구축함으로써 위젯을 이벤트나 콜백으로 감각화하고, 운영체제는 자극이 발생할 때 다시 부른다. 이러한 차이는-이벤트는 저수준이고 콜백은 고수준-Motif 규칙이다. 다른 객체 지향 환경에서 "콜백"이란 단어는 저수준이냐, 고수준이냐와 상관없이 모든 자극에 적용된다. 사실 스몰토크 프로그래머들은 스몰토크 외부로부터 시작된 스몰토크 메서드의 호출에 대해서는 모두 "콜백"이란 용어를 사용한다.
콜백이 (또는 이벤트가) 전형적인 호출과 다른 점은 무엇일까? 일부는 정도와 관련된다. 우선 첫째로, 대부분 콜백은 초기 자극(primitive stimuli)을 나타내므로 전형적인 호출보다 적은 정보를 전달한다 (argument가 더 적고 간단함). 하지만 주요 개념적 차이는 동시성과 관련이 있다. 뷰가 콜백 핸들러를 구축하고 나면 다른 코드의 실행을 계속한다; 즉, 블록하고 콜백을 기다리지 않는단 말이다. 대신 콜백은 비동기식으로 발생하므로, 뷰는 예측할 수 없는 미래에서 어떤 일이든 실행할 수 있다. 뷰의 관점에서 보면 콜백은 절차적 특색보단 이벤트 위주의 특색을 띤다.
준비
CgDrawable는 (그리고 그 서브클래스 CgWindow) CgGC의 인스턴스인 그래픽 컨텍스트에 의해 그 위에 "그려진" 그래픽을 표시할 수 있다. 그래픽 컨텍스트는 펜이나 브러시와 같은 그리기 툴로 생각할 수 있다. CgDrawable 그리기 메서드 몇 가지만 예로 들어보겠다:
CgDrawable>>drawPoint:x:y:
CgDrawable>>drawLine:x1:y1:x2:y2:
CgDrawable>>drawRectangle:x:y:width:height:
❏ 이러한 메서드 각각에서 어떤 객체가 첫 번째 키워드 argument일 것으로 예상하는가? 확실치 않다면 CgDrawable 클래스의 브라우징을 통해 답을 확인하라.
연습: 마우스 이벤트 처리
이 연습의 결과로, 마우스로 "끄적거릴 수" 있는 창이 생성될 것이다.
❏ CgExamples 애플리케이션의 이미지가 존재하지 않을 경우 이미지로 로딩하라. (표준 환경에서 설치하려면 System Transcript에서 시작해 Smalltalk tools>Load Features...를 선택한 후 Smalltalk Programming Examples를 선택한다. 전문가 환경에서 로딩하려면 Application Manager 창에서 Applications > Available > Application 옵션을 차례로 선택하라.) 이제 CgSingleDrawingAreaApplication 추상 클래스로 접근할 수 있다. 이 추상 클래스에는 그리기가 가능한 위젯과 (CgWindow) 거기에 빌드된 그래픽 컨텍스트가 있다; 따라서 그 구체적 서브클래스는 "그리기"가 가능하다.
❏ 추상 클래스 내 어떤 인스턴스 변수가 그래픽 컨텍스트를 참조하는가? 어떤 인스턴스 변수가 그리기 가능한 위젯을 참조하는가?
❏ Application Manager로부터 코드를 작성하기 위한 새 애플리케이션을 생성하라. CgExamples를 포함하도록 애플리케이션의 선행조건을 변경하라.
❏ 서브클래스 DoodleWindow를 CgSingleDrawingAreaApplication으로 추가하라. 두 개의 인스턴스 변수, oldX와 oldY를 추가하라. 이 인스턴스 변수들을 이용해 마우스가 이동 시 마우스의 이전 위치를 유지할 것이다.
❏ 애플리케이션을 시험하기 위해 클래스 메서드를 작성하라:
example
"
DoodleWindow example
"
^DoodleWindow new open
❏ 상속된 buttonMotion:과 buttonPress: 이벤트 핸들러를 살펴보자. 이들이 기대하는 argument는 이벤트로서, 사실상 CwMotionEvent 또는 CwButtonEvent의 인스턴스라는 점을 주목하라. 이벤트가 발생한 지점의 x와 y 좌표를 얻기 위해 어떤 메서드를 사용할 수 있는가?
❏ 마지막으로, buttonMotion:과 buttonPress: 이벤트 핸들러를 오버라이드하고 자신의 창을 검사하라.
도전적 연습: 동적 업데이트
❏ 계좌(account) 창은 트랜잭션 리스트와 잔고를 모두 표시하지만 새로운 트랜잭션을 추가할 방법을 제공하지 않기 때문에 여전히 상대적으로 비활성적이다. 창을 확장시켜 사용자가 새 트랜잭션을 생성하여 처리할 수 있도록 하라. 두 개의 UI 디자인 중 하나를 선택할 수 있다:
- 창에 버튼을 추가한다. 사용자가 버튼을 클릭하면 다른 창이나 대화상자가 열리면서 사용자가 새로운 트랜잭션 일자와 금액을 입력할 수 있도록 해준다. 사용자가 대화상자를 완료하면 본래 창이 새로고침되어 업데이트된 잔고와 트랜잭션 로그가 나타난다.
- 창에 일자 필드, 금액 필드, 버튼을 추가한다. 사용자가 일자와 금액을 완성하고 버튼을 누르면 잔액과 트랜잭션 로그가 새로고침된다.
이 연습이 그렇게 쉽지만은 않다. GUI 빌더가 없이는 건전한 탐색과 실험을 필요로 한다. 혹시 막히면 주위에 숙련된 스몰토크 프로그래머의 도움을 받는 것도 좋은 방법이다.
요약
객체 지향 사용자 인터페이스 프로그래머는 이벤트 위주의 특색을 띤다. 프로그래머는 시스템 이벤트 또는 콜백으로부터 핸들러를 구축하고, 이벤트가 발생할 때 명시된 핸들러가 실행된다. 일반적으로 이러한 이벤트는 화면의 어떤 지점을 마우스로 클릭하는 것과 같은 명시적인 사용자 액션에 의해 결정된 시간과 장소에서 발생한다. 따라서 이벤트 위주의 프로그래밍 모델은 사용자를 자유롭게 해주는 경향이 있고, 그들이 원하는 때, 원하는 액션을 실행하도록 해준다. 이를 충족시켜서 사용자가 진행 전에 고정된 상황을 처리하도록 강요하는 경우가 거의 없는 사용자 인터페이스를 non-modal 사용자 인터페이스라 부른다. (객체를 이용해 프로그래밍한 모든 사용자 인터페이스가 non-modal에 해당하는 것은 아니다. 가령, 버튼을 클릭하거나 메뉴를 선택하면 다음으로 진행되기 전에 사용자가 의무적으로 완성해야 하는 대화상자를 제시하는 사용자 인터페이스는 객체로 프로그래밍된 것처럼 보이지만 그렇게 느끼진 않을 것이다.)
이제 애플리케이션이 서브함수 트리로 분해되는 절차적 또는 함수 지향의 프로그래밍 모델을 살펴보자. 이 모델에 대한 전형적인 사용자 인터페이스에서는 상호작용이 대부분 어마어마한 메뉴의 계층구조를 통해 발생한다. 이러한 유형의 사용자 인터페이스는 애플리케이션을 직접적으로 사용자의 통제 하에 놓는데, 사용자는 심리적으로 구속되었다는 느낌을 받는다. 그러한 상황에서 진행하거나 벗어나기 위해서는 사용자가 엄격하게 정해진 방식대로 행동해야만 하는 사용자 인터페이스는 modal 사용자 인터페이스의 극단적 형태이다.
이벤트와 콜백은 Smalltalk-80에서 시작되었는데, 여기서 위젯은 뷰로 알려졌다. 이벤트에 대해 여러 핸들러를 지원함으로써 뷰(view)는 여러 문제에 맞춤설정할 수 있었다. 따라서 플러그 가능한 뷰(pluggable view)라 불린다: 특수화된 행위를 가진 뷰가 필요한 경우 이러한 뷰들 중 하나를 전체 애플리케이션 창으로 플러그하여 그에 특수화된 핸들러를 구축하는 것으로 충분했다. 플러그 가능한 뷰가 없다면 원하는 행위를 얻기 위해 완전히 새로운 뷰 클래스를 추가해야 했다. 다시 말해, 플러그 가능한 뷰를 "구매하여" 맞춤설정하거나, 다른 뷰로부터 "상속"하여 맞춤설정할 수 있겠다. 최근에는 위젯의 다양성과 그 플러그 가능성만으로 충분하기 때문에 대부분 일반 애플리케이션의 경우 새로운 위젯 클래스를 생성하지 않아도 된다.
여태까지는 운영체제를 거치는 이벤트나 콜백-즉, 스몰토크 외부에서 발생하는 자극-에만 중점을 두었다. 이와 비슷한 논리의 흐름이 스몰토크의 영역 내에서 엄격하게 발생하기도 한다. 예를 들어, MVC 또는 MV 방송(broadcast)은 콜백과 같다. 뷰가 그것의 update 메서드, 즉 가까운 모델이나 문제에 따라 작성된 핸들러를 구축하고, 이 핸들러는 모델이 방송을 이용해 뷰를 다시 부를 때마다 실행된다. 일부 방송 매커니즘은 update만 이용하는 대신 arguments와 함께 메시지 클러스터를 부르도록 해준다. VisualAge와 VisualSmalltalk는 그러한 매커니즘을 지원한다. (예: 125 페이지의 표) 프로그래머와 프레임워크는 종종 model-to-model 방송에 이러한 매커니즘을 사용한다. (하지만 그러한 사용 접근법은 조심스럽게 실행해야 하는데, 방송을 너무 많이 이용할 경우 성능을 상당히 저하시킬 수 있기 때문이다.)
마지막으로, 오늘날 GUI 빌더들은 UI를 빌드해야 하는 수고를 덜어주긴 하지만, 디자인은 여전히 필요하다. 훌륭한 UI 디자인은 위젯을 창으로 조립하는 것 이상으로, 다음 장의 주제에 해당한다. 강력한 GUI 빌더의 사용자들은 종종 훌륭한 모델-뷰 분리로부터 타락하기도 한다. 힘과 속도에 열중한 나머지 적절한 모델 객체를 생각하지 않고 데이터베이스 요소들과 같은 저수준 컴포넌트로 사용자 인터페이스 위젯을 직접 연결하곤 한다.
심지어 GUI 빌더가 없이도 동일한 과실로 고통 받기도 한다. HelloWindow의 빌드를 서두르면서 모델 클래스에 대해 생각할 시간이 없었던 것이다. 우리는 표준 뷰 성능을 제공하는 추상 슈퍼클래스로부터 서브클래싱하는 사용자 인터페이스 프로그래밍 스타일을 선택했다. 우리는 애플리케이션을 창의 종류로 생각했다. 어떤 모델 클래스도 애플리케이션의 로직을 캡슐화하지 않았다. 그러한 실추는 흥미로운 애플리케이션 로직을 갖고 있지 않은 HellowWindow와 같은 간단한 예제에서는 용서가 된다. 이는 조용히 잘못된 방향으로 가는 단계다; 일체식 창 클래스가 애플리케이션 로직으로 차 있는 애플리케이션을 빌드하기 시작한 경우에만 위험하다. account 창과 관련해 우리는 애플리케이션의 행위를 캡슐화한 구분된 모델 클래스(Account)를 사용함으로써 이러한 실수를 피할 수 있었다.