SmalltalkObjectsandDesign:Chapter 19
- 제 19 장 프레임워크 (하나의 무거운 예제)
프레임워크 (하나의 무거운 예제)
객체 지향 프레임워크는 패턴만큼이나 유행하고, 어느 곳이든 개발자들은 그것의 빌드를 시도한다. 패턴과 마찬가지로 너무나 많은 것들이 프레임워크로 불리면서 의미가 모호해져 그 중요성을 인식하기가 힘들어졌다.
프레임워크는 소프트웨어 애플리케이션에 전반적인 골격이다. 동일한 프레임워크를 둘러싸고 하나 이상의 애플리케이션을 빌드하는 수도 있지만 프레임워크의 기본 구조와 기법에 의해 형성될 것이며, 이는 동일한 개구리 골격에서 외모와 식성이 다른 개구리들을 형성할 수 있는 것과 같다. 개구리를 빌드하려면 개구리 프레임워크를 사용한다; 뱀을 빌드하려면 뱀 프레임워크를 사용한다.
기술적 용어로 프레임워크란 서로 다른 프로젝트에서 재사용 가능한 코드의 body를 의미한다. 필자가 이미 이야기한 예제는 사용자 인터페이스를 위한 오리지널 MVC 프레임워크에 해당한다. MVC는 추상 클래스 Model, View, Controller와 그들 간 상호작용으로 구성된다. 이러한 상호작용에서 가장 유명한 것으로 모델이 전달하는 방송(broadcast)을 들 수 있다. Smalltalk-80을 사용하는 대개 모든 애플리케이션 개발자는 이 3개의 추상 클래스 중 하나 또는 그 이상으로부터 상속을 통해 MVC를 재사용한다.
반면 클래스에 대해 재사용 가능한 라이브러리라고 해서 모두 프레임워크의 자격이 되는 것은 아니다. 예를 들어, 컨테이너 클래스의 라이브러리를 프레임워크라고 부르거나 사용자 인터페이스 위젯의 라이브러리를 프레임워크라고 부르는 사람은 없다. 그렇다면 차이는 무엇일까? 우선 MVC와 같은 프레임워크는 애플리케이션에 구조를 부여하고, 위젯 또는 컨테이너의 라이브러리는 그렇지 않다는 점을 들 수 있겠다.
또 다른 차이가 있는데 Erich Gamma는 이를 헐리우드 원칙이라 부른다:[1] "연락하지 마세요; 저희가 연락하겠습니다." 프로그래머들은 라이브러리에 함수의 호출을 작성하는 것에 익숙하다. (흔히 프로그래머들이 API, 즉 애플리케이션 프로그래밍 인터페이스를 호출한다고 말한다.) 헐리우드 원칙은 이러한 관계를 뒤집는다: 프레임워크가 호출하는 코드를 프로그래머가 작성한다. 프로그래머는 프레임워크가 무엇을 호출할 것인지 미리 알아야 한다-그의 코드는 프레임워크의 기대를 따라야 한다. 객체 지향적 기대치에 따른 프레임워크는 서브클래스를 빌드하고 subclassResponsibility(순수 가상) 메서드를 오버라이드하기 위한 추상 클래스를 일부 포함할 것이다. 프레임워크의 이용 시 항상 추상 클래스로부터의 서브클래싱이 수반된다; 컨테이너나 위젯 클래스의 일반 라이브러리를 이용 시에는 그렇지 않다.
다시 말해, 프레임워크는 빠진 요소들을 명시한다. 당신이 이러한 요소들을 공급하면 프레임워크는 그 요소들이 작업 애플리케이션으로서 함께 작동하도록 만든다. 서로 다른 요소들을 공급함으로써 다른 애플리케이션을 생성할 수 있다. 프레임워크는 애플리케이션 자체를 제외한 애플리케이션의 모든 기구를 제공한다.
프레임워크에 대한 개념을 이해하는 최선의 방법은 예제를 연구하는 것이다. 클라이언트/서버 애플리케이션의 개발을 위한 기구를 제공하는 프레임워크의 주요 부분들을 몇 가지 소개하고자 한다. 이 프레임워크는 네트워크상에 있는 컴퓨터로부터 비객체 데이터를 사용하는 스몰토크 애플리케이션의 구성을 간소화한다. 이와 같은 프레임워크는 클라이언트/서버 애플리케이션의 개발에 들이는 전반적인 노력에서 상당한 부분을 차지한다; 처음부터 이를 개발하는 데에 필요로 하는 비용과 전문성은 애플리케이션의 모델 및 뷰 객체를 개발 시 드는 비용을 초과한다.
문제
클라이언트에서 스몰토크를 지원하고 서버에서 비객체 데이터를 지원하기 위한 프레임워크라면 몇 가지 기본적인 문제를 강조해야 한다. 이러한 문제는 클라이언트/서버 컴퓨팅의 기본 use case 로 생각할 수 있겠다.
- 실체화: 일반적으로 파일 내 레코드나 관계형 데이터베이스에서 행의 형태로 된 전형적인 비객체 데이터를 객체 지향 언어가 처리할 수 있는 객체로, 또는 그 반대로 변형.
- 정체성 관리: 많아 봐야 하나의 객체 버전이 클라이언트 워크스테이션에 상주하도록 보장한다. 다시 말해, 객체가 이전에 한 번 실체화되지 않았다면 실체화는 두 번째 객체 복사본을 생성해선 안 된다.
- 검색: 어떤 기준에 일치하는 객체를 하나 또는 그 이상 검색.
- 업데이트하기: 클라이언트 워크스테이션에서 객체의 상태를 변경하고 그러한 변경내용을 적절한 서버로 다시 캐스케이드(cascade)하는 것. (새 객체의 생성 및 저장 문제와 비슷하다.)
아래 그림은 문제를 요약해준다:
이제 프로그래머가 클라이언트/서버 프레임워크로 넘겨주었으니 애플리케이션, 가령 대출금을 관리하는 은행가를 위한 애플리케이션을 개발한다고 가정해보자. 위의 문제 각각에 대해 무엇을 해야 하는지 이해하고자 할 것이다. 결과를 이해하려면 관계형 데이터베이스에 관한 몇 가지 사실을 이해할 필요가 있을 것이다:
- 데이터는 테이블에 저장된다. 테이블의 필드나 열에는 "LoanNumber," "OutstandingAmount," "Collateral"과 같은 이름이 있다. 테이블의 각 행은 관계형 데이터를 포함하는데, 특정 대출 번호, 주요 금액, 신규 대출 일자가 해당하겠다.
- 관계형 데이터의 조작을 위한 표준 언어는 SQL, 구조화 조회 언어라 부른다. 모든 관계형 데이터베이스 시스템은 동적 SQL을 지원한다; 동적 SQL문은 전송될 때마다 데이터베이스에 의해 재해석되어야 한다. 일부 데이터베이스 시스템은 선컴파일된 SQL의 형태를 지원하기도 하는데, 이 형태는 우선 컴파일되고 나면 데이터베이스에 바인딩되며 (bound to), 그에 따라 동적 SQL보다 빠르게 실행된다. 이러한 SQL 유형의 예제로 데이터베이스 시스템의 DB2 군에 대한 IBM의 정적 SQL을 들 수 있겠다. (이번 장을 이해하기 위해 SQL 언어의 세부 내용을 알아야 할 필요는 없다.)
실체화
클라이언트/서버 애플리케이션에서 핵심은 실체화다-서버측에서 플랫, 비객체 데이터 형태로부터 클라이언트 워크스테이션에서 객체를 생성하는 행위.
프레임워크가 하는 일. 프레임워크는 플랫 데이터(레코드)를 객체로, 또는 그 반대로 변환하기 위해 레코드로부터의 객체 패턴(220 페이지)을 사용한다. BusinessObject 란 이름의 추상 클래스는 패턴에 의해 생성된 객체를 나타내며, Broker 라는 이름의 추상 클래스는 서버로 레코드를 전송하고 그로부터 수신하기 위한 알고리즘을 캡슐화한다.
브로커의 디자인은 전체적인 클라이언트/서버 아키텍처에 따라 좌우된다. 어떤 브로커는 서버로 동적 또는 정적 SQL 호출을 전송할 수 있다; 또 어떤 브로커는 서버 측에서 데이터를 처리할 때 실행되는 프로그램이나 프로시저로 호출을 보내기 위해 APPC 또는 TCP/IP와 같은 통신 프로토콜을 사용하는 수도 있다. 다시 말해, 어떤 브로커들은 SQL 브로커이고 또 어떤 브로커들은 트랜잭션 브로커에 해당한다. 이러한 브로커들의 private한 행위는 서로 다르므로 프레임워크는 그에 대해 서로 다른 추상 클래스를 갖는다. (브로커의 다양성에 관한 상세한 정보는 254페이지의 논평을 참고하라.)
애플리케이션 디자이너가 정적 SQL을 지원하는 브로커의 클래스를 사용하기로 결정했다고 치자. 이 추상 클래스에 대한 이름은 SQLBroker이다. 이 클래스는 데이터베이스에 바인딩되는 정적 SQL문의 패키지를 보관하는 DBPackage라는 또 다른 추상 클래스와 협력한다.
프레임워크는 브로커, 패키지, 비즈니스 객체들의 클래스의 특정 조합이 함께 작용하여-예: LoanBroker는 Customer 객체를 실체화하는 데에 효과가 없을 것이다-이러한 클래스들을 연결하는 Broker>>objectClass와 Broker>>package란 이름의 subclassResponsibility 메서드를 선언함을 알아야 한다.
프로그래머가 하는 일. 그는 추상 클래스의 구체적 서브클래스, 가령 LoanBroker, LoanPackage, Loan을 빌드하고, LoanBroker에 이러한 메서드와 클래스를 연결한다:
objectClass
"Answer the class of business objects I broker"
^Loan
그리고:
package "Answer the the package of SQL statements I need"
^LoadPackage instance
그리고 나면 프레임워크는 이러한 클래스들이 함께 올바르게 작동하여 loan 객체를 실체화하도록 보장한다.
❏ 위의 메서드에서 instance 메시지의 기능을 설명하라.
해답: 워크스테이션에는 LoanPackage의 인스턴스가 하나만 존재해야 한다. 따라서 패키지는 솔리테르이며 (230 페이지) instance는 LoanPackage의 유일한 인스턴스를 리턴하는 클래스 메서드의 (관습적) 이름이다.
실체화에 수반되는 클래스를 강조하면 다음과 같다:
objectClass와 package 메서드는 팩토리 메서드 패턴(219 페이지)의 예제에 속함을 눈치챌지도 모르겠다. 팩토리 메서드는 프레임워크 빌딩의 가장 기본적(bread-and-butter; 직역하면 버터 바른 빵) 패턴이다.
객체 정체성 관리하기
클라이언트/서버 시스템에 따라오는 위험으로, 동일한 business 객체를 나타내는 두 개의 구분된 객체의 실체화를 들 수 있다. 이러한 상황은 두 개의 복사본을 독립적으로 변경하는 위험으로 사용자를 노출시키는데, 객체 정체성에 심각하게 위배되는 행위가 될지도 모른다. (객체 정체성에 관한 논의는 73페이지부터 시작된다.)
어떻게 이러한 일이 발생할 수 있을까? 미지불 금액이 $50,000 이상인 모든 loans를 검색한 결과로 loan 객체가 실체화한다고 가정하자. 시간이 지나고 어떤 거물(tycoon)에게 대출한 모든 대출금을 두 번째로 검색하면 동일한 loan이 실체화된다. 애플리케이션을 유의하여 디자인하지 않으면 클라이언트 워크스테이션에서 두 개의 loan 객체가 이제 동일한 loan을 나타낼 것이다. 또 다른 예로, 은행가가 둘 중 한 객체에서 부수적 단어를 ("...통나무집, 수돗물, 가려진 현관...") 업데이트하고, 나머지 객체에서 납기 기간을 연장한다고 가정하자. 이제 둘 중 어떤 loan 객체도 은행가가 의도한 데이터를 갖고 있지 않으며, 서버로 어떤 loan(들)이 다시 할당된다 하더라도 혼동을 피할 수 없다.
프레임워크가 하는 일. Broker는 그것이 실체화한 모든 객체에 대한 dictionary를 포함한다. Dictionary 내 모든 엔트리는 객체에 대한 유일한 기술자를 키(key)로 가지며, 객체 자체를 값으로 가진다. 따라서 100,000개의 loans는 서버 데이터베이스로 보관되고, LoanBroker는 그 중 29개를 실체화하며, 29개 엔트리는 LoanBroker의 dictionary에 위치할 것이다. 그들 각각은 키-어쩌면 loan의 대출 번호-와 loan 객체 자체에 해당하는 값으로 구성된다.
은행가가 다른 loan 객체를 요청할 경우 프레임워크는 loan의 대출 번호가 dictionary에 이미 위치한 29개의 키 중 하나에 해당하는지 검사한다. 일치할 경우, 다른 복사본을 실체화해서는 안 되는데, 만일 그럴 경우 또 위와 같이 바람직하지 못한 시나리오가 발생한다.
이 모든 것이 올바로 작동하기 위해선 그 business 객체들에게 무엇을 키로 사용해야 하는지 브로커가 분명히 알아야 한다. 따라서 BusinessObject 클래스는 identityKey라 불리는 subclassResponsibility 메서드를 갖고 있다. 브로커는 이 메서드를 이용해 그것의 dictionary 내 엔트리를 관리하고, 특히 dictionary에 business 객체가 이미 존재하는지 결정한다.
프로그래머가 하는 일. 프로그래머는 loan의 대출 번호를 단순히 리턴하는 Loan>>identityKey 메서드를 작성한다. 나머지는 프레임워크가 책임진다.
프레임워크가 하는 일. 이제까지 했던 작업의 반대는 바로 정리(cleaning up)가 되겠다: 프레임워크는 실체화된 객체들에 대한 브로커의 dictionary로부터 언제 엔트리를 제거해야 하는가? 객체에 대한 브로커의 dictionary는 오랜 사용 후에 너무나 방대하게 증가하여 워크스테이션의 메모리를 초과하기 때문에 프레임워크는 제거(removal)를 무시할 여력이 없다. 여기서 프레임워크가 애플리케이션에서 business 객체를 더 이상 필요로 하지 않을 때를 인식하는 데에 어려움이 있다; 즉, 애플리케이션에서 다른 business 객체나 뷰를 더 이상 참조하지 않을 때를 말한다. 이 때 객체를 브로커의 dictionary에서 안전하게 제거할 수 있다.
이러한 도전과제는 쓰레기 수집(garbage collection) 문제와 비슷하게 들린다 (187 페이지). 하지만 완전히 같은 상황은 아닌데, 우리에겐 또 다른 전개가 있기 때문이다: 스몰토크의 쓰레기 수집기는 브로커의 dictionary가 객체에 대한 참조를 해제(release)하기 전에는 객체를 쓰레기로 인식하지 않는다; 반면 dictionary는 그 객체가 쓰레기임을 알 때까지 참조를 절대로 해제하지 않는다.
다행히도 최근에 발매된 주요 스몰토크들은 약한 참조(weak reference)라는 기능으로 메모리 관리를 확장한다. 객체로의 약한 참조는 쓰레기 수집기에 중요하지 않은 참조를 의미한다. 일반적인 참조는 강한 참조다; 이는 쓰레기 수집기가 여전히 객체가 필요하다는 사실을 알기 위해 사용하는 참조이다.
우리는 브로커들이 강한 참조 대신 약한 참조를 사용하도록 하여 쓰레기 수집기에게 브로커가 방해되지 않도록 디자인하고자 한다. 이를 위해선 브로커가 WeakDictionary-이를 비롯해 앞에 "weak"가 붙은 컬렉션 클래스들은 다른 객체를 약하게 참조할 수 있는 객체 유형만 해당-로 알려진 특수 dictionary을 사용해야 한다. 쓰레기 수집기는 약한 dictionary가 객체로의 참조를 갖고 있는지 여부를 신경 쓰지 않는다; 강한 참조가 아니라 약한 참조이기 때문이다.
위의 예제에서 약한 dictionary에 29개의 엔트리가 있고, 각 엔트리는 대출 번호와 그에 해당하는 loan 객체로 구성되며, 사용자가 loans 중 하나에 열린 뷰를 모두 닫기로 결정했다고 치자. 이 loan 객체에는 강한 참조가 없지만 약한 dictionary부터의 하나의 약한 참조가 남는다. 약한 참조들은 쓰레기 수집기를 중단시키지 않는다; 객체를 쓰레기로 간주하고 그 메모리를 회수하여 결국 약한 dictionary로부터 참조를 제거한다. 우리로선 원하는 효과를 얻은 셈이다: 일반 Dictionary 대신 WeakDictionary를 이용함으로써 브로커는 어떤 애플리케이션 객체도 참조하지 않는 객체를 자동으로 해제한다[2].
프로그래머가 하는 일. LoanBroker 내에 약한 참조의 모든 로직은 Broker에서 상속될 것이기 때문에 identityKey만 작성한다.
정체성 관리의 주요 부분은 다음과 같다:
검색 (필터링)
무언가를 검색하는 일은 인간의 기본적인 활동이다. 때로는 우리가 무엇을 찾는지 정확히 알아서 때로는 그저 잡기를 (대출 번호가 334455인 loan), 때로는 컬렉션을 보길 원한다 (미지급액이 $50,000 이상인 모든 loans).
프레임워크가 하는 일. 사용자 또는 애플리케이션이 바람직한 객체를 식별하는 키를 알고 있는 첫 번째 사례로 시작해보자. Broker는 argument를 키로 갖고 그 키를 가진 business 객체를 리턴하는 objectWithKey:와 같은 구체적 메서드를 제공한다.
프로그래머가 하는 일. Broker에서 완전한 범용성으로 코드를 한 번 작성할 수 있기 때문에 broker 서브클래스에 objectWithKey:를 오버라이드하지 않는다. 반면 이 메서드를 호출하는 경우를 많이 발견할 것이다.
예를 들어, 모든 loan 객체가 고객 번호를 포함한다고 가정하자. 즉, loan 객체는 대출금을 빌려간 고객을 식별하는 키를 포함한다. 사용자는 loan을 조사하면서 덩달아 고객도 검사하길 원할지도 모른다. 따라서 사용자가 어떤 버튼을 누르면 프로그래머의 코드는 고객에게 응답하는 CustomerBroker에게 고객의 키를 argument로서 전달하는 objectWithKey: 메시지를 전송하여 클릭에 응답한다.
아니면 프로그래머가 customer 객체에 대한 프록시를 디자인하여 (224 ff. 페이지) 리스트 위젯에 표시할 만큼 충분한 정보를 제공하는 getters로 프록시를 채울지도 모른다. 머지않아 사용자는 그 중 하나의 프록시 뒤에서 전체 customer 객체를 보길 원한다; 이후 이 프록시는 CustomerBroker에게 objectWithKey: 메시지를 전송한다. 프록시는 그것이 의미하는 customer의 고객 번호를 알고 있으므로, 메시지는 이 키를 argument로서 전달한다.
전체 customer 객체를 잡기 위한 두 가지 시나리오에서 주요 부분은 다음과 같다:
이 이야기는 프레임워크가 프로그래머에게 subclassResponsibility 메서드를 오버라이드하도록 요구할 뿐만 아니라 프로그래머가 호출할 것으로 보이는 구체적 메서드를 이용 가능하게 만든다는 데에 교훈이 있다. 프로그래머는 그러한 메서드에 포함되었을지 모를 까다로운 코드를 검사할 필요가 없다; 프레임워크가 올바로 처리했다고 믿는다. 다시 전문 용어를 사용해 말하자면, 프레임워크는 대개 화이트박스 재사용으로 특징화되지만 일부 블랙박스 재사용을 제공하기도 한다. (이 용어는 253 페이지를 참고한다.)
프레임워크가 하는 일. 그 외 검색 유형은 필터라는 이름으로 통한다: 프레임워크는 사용자로부터 기술(description)을 받아서 그 기술에 일치하는 모든 객체에 대해 서버를 필터링한다. 이는 처음엔 분명하지 않았던 객체의 두 클래스를 소개할 기회가 되기 때문에 프레임워크 디자이너에게는 흥미로운 문제다. (그러한 발견을 구체화라고 부른다는 사실을 기억하라. 216 페이지를 참고한다.) 첫 번째 클래스는 Search라고 명명된 추상 클래스다; 이 클래스는 발견된 객체들의 기술(description)을 캡슐화한다. 두 번째는 BusinessObjectList로, 검색이 리턴하는 것이다.
Search 객체들은 기본적인 사용 가능성 문제를 해결한다: 사용자는 검색의 결과를 본 후에 종종 작은 방식으로 객체의 기술을 조정하여 다시 검색하길 원한다. 프레임워크는 Search 객체를 유지하기 때문에 사용자는 쉽게 원본 기술(original description)로 접근하여 수정하고 다시 검색을 발생시킬 수 있다.
BusinessObjectList는 CorderedCollection의 주요 메서드를 지원하도록 디자인되었기 때문에 리스트 위젯을 덧붙이는 데에 있어 OrderedCollection만큼 괜찮다-리스트 위젯이 아는 한 BusinessObjectList와 OrderedCollection은 다형성이다. 하지만 BusinessObjectList도 클라이언트/서버 성능 문제를 해결하는 행위를 지원한다: 사실상 검색은 너무나 많은 객체를 리턴하여, 결국 서버에서 클라이언트로 데이터를 모두 이동하는 시간이 허용하기 힘들 정도로 너무 길어지거나, 객체가 클라이언트의 메모리를 너무 많이 소모하게 된다. 따라서 search 객체는 일치하여 리턴하는 객체의 수를 제한하도록 서버에게 요청하는데, 가령 첫 번째부터 50개의 business 객체까지로 제한하여 이러한 객체들을 포함하도록 BusinessObjectList를 생성한다. 사용자가 그 다음 50개 객체를 보고 싶다면 BusinessObjectList가 스크롤할 수 있는데, 여기서 스크롤은 사실 search 객체에게 서버로부터 다음으로 일치하는 50개의 business 객체를 요청함을 의미한다. 따라서 BusinessObjectList는 검색 기술에 일치하는 객체를 제한된 수만큼 보유하지만, 필요 시 다른 것을 이용해 스스로 보충할 만큼 똑똑하다. 이것은 전체 리스트가 아니지만 전체 리스트로 접근할 수는 있으며, 우리는 이를 가상 리스트라고 부르고 이 리스트의 모든 business 객체로 도달하기 위해서는 메타 스크롤해야 한다.
프로그래머가 하는 일. LoanSearch와 같이 Search 의 구체적 서브클래스를 빌드하고, loans에게 중요한 검색 기준을 처리하도록 LoanSearch를 맞춤설정한다.
따라서 필터링 또는 검색에서 주요 부분은 다음과 같다:
Search 객체는 실제로 서버로부터 객체를 검색하기 위해 broker와 협력해야 한다는 사실을 주목하라. 또한 BusinessObjectList는 내용을 생성한 search 객체를 알아야지만 메타 스크롤을 실행 시 이 검색을 재실행할 수 있음을 주목한다.
업데이트하기
사용자가 서버에서 데이터를 업데이트하도록 허용하는 애플리케이션은 두 가지 기본 시나리오를 지원해야 한다:
1. 사용자가 내용을 변경하고 그대로 결정한다.
2. 사용자가 내용을 변경한 후 다시 생각하여 파기하길 원한다.
프레임워크가 하는 일. 사용자는 business 객체를 본래 상태로 되돌리길 원하기 때문에 프레임워크는 객체의 원본 데이터를 어딘가 유지해야 한다. 실체화에 사용하는 레코드로부터의 객체 패턴은 (220 페이지) 원본 데이터와 사용자의 변경 내용을 저장할 저장소를 제공한다. 패턴은 데이터의 ByteArray에 더해 변환된 서브객체의 캐시까지 수용한다. (223 페이지의 그림은 하나의 구현부를 보여주는데, 바이트 배열(byte array)과 캐시 모두 레코드 객체에 저장되어 있다.)
ByteArray는 원본 데이터를 저장하고 캐시는 현재 객체를 저장하는데, 이 객체가 ByteArray로부터 변환에 의해 캐시 저장된(cached) 객체인지 아니면 사용자가 이러한 객체를 수정하였는지는 상관없다. 사용자가 캐시에서 변경 내용을 추진할 때에만 프레임워크는 캐시 저장된 객체를 ByteArray 내에 미가공(raw) 데이터로 변환하고 데이터를 서버로 전송한다. 사용자가 만일 변경내용을 파기할 경우 프레임워크는 단순히 캐시를 비우는데, 이럴 경우 원본 ByteArray를 현재 데이터로 효과적으로 표현한다.
프로그래머가 하는 일. 블랙박스 재사용의 또 다른 예제다. 사용자가 변경 내용을 적용하거나(committing) 되돌리는(reverting) 버튼을 클릭할 수 있다고 가정하면, 프로그래머는 각각 Loan>>update 또는 Loan>>revert를 호출함으로써 클릭 이벤트에 응답하는 코드를 작성한다. 이러한 메서드는 사실 BusinessObject에서 상속되므로 나머지는 프레임워크가 한다.
업데이트와 복귀의 디자인에서 주요 부분은 다음과 같다:
모델-뷰 또는 관찰자 패턴(228 페이지)에 일치하도록 뷰의 loan은 여기서 model이라는 인스턴스 변수에 의해 표현된다.
요약
프레임워크를 사용하는 프로그래머는 자유를 구속당한 것과 같을 느낌일 것이다. 그는 프로그래밍의 전형적인 선택의 자유를 잃는다. 헐리우드 원칙으로 인해 그는 "로직을 배치해서 시작해야지,"가 아니라 "어디서부터 시작해야만 하지?"라는 생각을 먼저 하게 된다[3]. 지금 객체 지향 프로그래밍을 이야기하고 있으므로 이 질문에 대한 답은 프레임워크의 추상 클래스와 그들의 subclassResponsibility 메서드를 검색함으로써 답한다.
객체 지향의 프레임워크에는 또 다른 특징들이 있다:
- 프레임워크는 화이트박스 재사용뿐만 아니라 컨테이너 클래스의 라이브러리를 사용하거나 API를 호출하여 얻는 익숙한 블랙박스 재사용도 제공한다. 프레임워크 사용자는 추상 클래스를 이해하고, 그들의 subclassResponsibility 메서드에서 무엇을 기대하는지를 이해하는 반면, container 사용자는 캡슐화된 add:와 remove: 메서드를 호출하기만 한다. 화이트박스 재사용은 헐리우드 원칙을 따른다: 프레임워크는 당신의 코드를 호출할 때 기대치를 가질 것이며, 그 기대치가 무엇인지 이해하기 위해 프레임워크를 충분히 살펴보는 것은 당신의 일이다. 당신이 foobar라는 메서드를 오버라이드하도록 보장하는 것만큼 간단하겠지만 그 대상이 무엇이든 당신은 그것을 이해해야 한다.
- 프레임워크의 사용 방법을 학습하는 데에는 노력이 필요하다. 추상 클래스와 그들의 subclassResponsibility 메서드를 학습하기 전에는 화이트박스 재사용을 할 수 없다. 추상 클래스는 서로 상호작용하기 때문에 애플리케이션을 작성하기 이전에 하나 이상의 클래스를 학습해야 하는 것이 보통이다. 반면 블랙박스 재사용은 주로 한 번에 하나의 클래스에서 발생한다. 오늘은 Set를 사용하고 내일은 SortedCollection을 사용할 가능성이 높다; 그들 간 상호작용은 이해하지 못해도 괜찮다.
- 프레임워크의 개발에는 시간이 걸린다. 프레임워크를 하나 이상의 프로젝트에 사용해보기 전에는 그것이 재사용 가능한지 알 수가 없으므로-재사용 가능성의 정의-프로젝트를 전달하는 것과 관련해 프레임워크를 최소 두 번은 빌드해야 한다. 프레임워크는 연속된 프로젝트에서 개선되면서 점점 향상된다. 이번 장에서 소개한 클라이언트/서버 프레임워크는 시작은 대충 만들었으니 다년 간에 걸쳐 진화했으며, 끝없이 계속해서 개선되고 있다.
- 프레임워크의 개발은 힘들다. 애플리케이션 프로그래머의 작업을 단순화시키기 위해서는 프로그래머가 원치 않는 일들을 모두 수행할 수 있어야 한다. 클라이언트/서버 프레임워크의 경우, 데이터베이스와 객체 간 변환, 통신 및 트랜잭션 관리, 이벤트 처리와 창 관리, 재사용 가능성, 심지어 성능 최적화까지 해당하겠다. 특히 비즈니스 문제가 아닌 기술적 어려움들은 모든 애플리케이션 프로그래머가 고심하기보다는 재사용 가능한 프레임워크에게 남겨두는 편이 낫다.
클라이언트/서버 컴퓨팅에서 개념적 및 실용적 고찰사항에 관한 맥락은 [Orfail et al. 1994]를 참고한다. 이번 장에서 간략하게 살펴본 클라이언트/서버 프레임워크를 좌우하는 원칙들은 [Wolf and Liu 1995]의 주제이다. 실제 프레임워크는 MFW(실체화 프레임워크)로 알려진다. 하지만 여기서 소개한 개념, 클래스와 메서드의 이름, 알고리즘은 명료성을 위해 단순화한 것으로, MFW에서 사용되는 것과 정확히 일치하지 않으며, 이번 장의 내용은 프레임워크의 전체 범위를 다루진 않았음을 명심해야 한다.
객체 지향 프레임워크는 컴퓨팅의 어떤 측면이든 다룰 수 있다: CommonPoint는 그래픽부터 시작해 National Lanague Support to I/O 장치 드라이버로의 문서 편집에 이르기까지 문제를 처리하기 위한 수많은 관련 C++ 프레임워크로 구성된다 [Lewis et al. 1995]. Accounts는 일반 ledger account(외상 계정), inventory(재고 목록), investment account(투자 자금)와 같은 것들을 유지하는 비즈니스 애플리케이션을 빌드하기 위한 스몰토크 프레임워크다 [Johnson 1995]. MacApp는 C++ 또는 Object Pascal을 이용해 매킨토시의 모양과 느낌(look and feel)을 갖는 애플리케이션을 생성하기 위한 프레임워크다 [Schmucker 1986; Lewis et al. 1995]. 그리고 MVC는 역사가들이 흔히 특정 주요 사건에 대해 말하듯이 세계에서 가장 오래 지속되는 운영 프레임워크다.
논평: 브로커의 다양성
컴퓨팅 세계에서 브로커 종류의 수는 어머어마하다. 브로커는 모든 분산 컴퓨팅 아키텍처의 중심에 위치한다.
서버 프로그램으로 작동하고 표준 정보 단위-레코드, CORBA 객체, Network OLE 객체...-로 작동하는 브로커는 트랜잭션 브로커로 알려진다. 트랜잭션 브로커는 관계형 데이터베이스와 호환된다: 서버 데이터가 관계형일 경우 서버 프로그램은 데이터를 표준 정보 단위로 변환하고 그 반대로도 작용한다. 트랜잭션 브로커는 컴퓨팅 노드들 간 로직을 분할한다; 따라서 그것을 기반으로 한 분산 시스템은 클라이언트를 서버로부터 분리하여 시스템의 장기간 유연성을 향상시킨다.
클라이언트 애플리케이션의 관점에서 보면 broker 객체는 객체 데이터베이스처럼 기능한다: 객체를 검색하여 업데이트하고, 그 객체에 대한 동시 접근성(concurrent access)의 관리 등 기능을 한다. 브로커와 객체 데이터베이스를 교대로 생각해보면 어렵지 않게 객체 데이터베이스를 분산 객체에 대한 대안적 접근법으로 상상할 수 있다. 따라서 클라이언트/서버와 분산 객체 컴퓨팅에서 우위 선점(dominance)을 위해 객체 데이터베이스는 트랜잭션 브로커와 경쟁할 것을 예상해야 한다.
트랜잭션 브로커의 장점에도 불구하고 사실상 SQL 브로커가 더 많이 사용된다. SQL 브로커는 서버 측에서 다른 언어로 추가 프로그램을 작성하도록 요구하지 않기 때문에 작동하는 애플리케이션을 더 빨리 생성한다. 하지만 서버측에서 관계형 데이터베이스를 필요로 하며, 관계형 테이블의 구조에 클라이언트를 밀접하게 연결시킨다-캡슐화의 정신에 대조된다. 길게 보면 트랜잭션 브로커는 개념의 증명이나(proofs-of-concept) 프로토타입에는 유용하게 남겠지만 확장 가능한 고성능 애플리케이션으로 대체될 것이다.
논평: (때때로) 구매는 상속을 능가하다
프레임워크는 상속과 화이트박스 재사용의 세계에서 꼭 필요한 자체의 역할이 있음을 증명한다. 또한 우리 모두는 상속의 매력을 즐기는 데에 적응해왔다-우리가 원하는 것에 가까운 기능을 제공하는 클래스에서 상속을 통해 특수화된 객체를 생성한다. 상속은 객체 지향 프로그래밍에서 가장 장점이 많은 기법 중 하나로, 지나치게 사용하기 쉬운 기법이기도 하다. 하지만 시간이 지나면서 훌륭한 디자인은 상속은 줄이고 구매를 늘인다 (제 9장). 즉, 점차적으로 블랙박스 재사용은 늘이고 화이트박스 재사용은 줄이는 방법을 인식하는 것이다.
한 가지 예제가 있다. Loan 객체의 데이터는 customer 객체의 데이터와 다르기 때문에 기본이 되는 레코드 내 필드를 비롯해 필드의 크기는 분명히 다르다. 따라서 Record 클래스의 두 서브클래스, 즉 LoanRecord와 CustomerRecord를 생성하여 각각 적절한 필드별 접근자(field-by-field accessor)를 지원하도록 하는 것이 솔깃한 방법인데, 이러한 접근자로는 LoanRecord에 대해 atCollateral과 atCollateralPut:이 있고, CustomerRecord에는 atName과 atNamePut:이 있다.
이러한 간단한 디자인은 효과가 있긴 하나 매력은 없다. 첫째, 클래스를 급증시켜 business 객체의 각 클래스마다 구분된 레코드 클래스를 필요하게 만든다. 둘째, 상속이 기본적으로 행위적(behavioral)이지 않다; 오히려 대출의 이차적 기술(collateral description)이나 고객 이름과 같은 내부 데이터 속성을 기반으로 한다. 흥미로운 객체들은 데이터보다는 행위로 특징지어져야 한다. 사실 여기서는 각 서브클래스에 나머지와 전혀 공통점이 없는 메서드 전체 무리가 동반되는 것처럼 보인다. 어떤 메서드도 재사용, 공유 또는 오버라이드되지 않고, 어떤 메서드도 subclassResponsibility 메서드의 후보자에 해당하지 않는다. 마지막, 어쩌면 가장 걱정스러운 조짐은 이러한 레코드 클래스들이 다형성이 전혀 눈에 띄지 않는 상속 계층구조를 형성한다는 점이다.
이러한 조짐들은 그 대신 또 다른 비상속 기반의 해결책을 고려하도록 장려한다. 그렇다면 하나의 구체적 Record 클래스를 구성 가능하게 디자인하여 그 인스턴스들이 loan 또는 customer로서 기능을 하도록 상상해보자. Loans와 customers에 대한 레코드는 서로 다른 크기와 내용을 갖지만 행위적으로 동일하도록 디자인한다. 특히 그 필수 public 선택자들은 at:과 at:put:으로, 첫 번째 키워드 파라미터는 #Collateral이나 #Name과 같은 필드명으로 되어 있다. 이러한 방식으로 우리는 레코드 서브클래스의 전체 계층구조 디자인을 제거하면서 애플리케이션으로부터 클래스를 비롯해 수많은 메서드를 다듬는 것이다. 이렇게 상속이 없는 디자인은 레코드로부터의 객체 패턴과 일치한다 (223 페이지의 그림 참고).
이렇게 상속 기반의 디자인으로부터 벗어난 진행(progression)의 예제를 두 가지 더 소개하겠다:
- 플러그 가능한 뷰는 프로그래머가 위젯의 서브클래스를 생성할 필요 없이 위젯을 구성하도록 해준다 (140 페이지).
- 초기 객체 지향의 예외처리 방식에서는 예외의 클래스 계층구조를 배치하긴 했으나 모든 예외 객체의 필수 행위는 동일했다. 따라서 최신 예외 방식은 (IBM Smalltalk도 포함) 예외의 인스턴스 계층구조를 사용한다. 이 방식은 클래스 빌드에 문제가 될 소지를 미연에 방지한다.
이러한 주장들은 무슨 수를 써서라도 상속을 회피하도록 유도할지도 모르지만, 상속 디자인은 그 안에서 다형성을 발견하는 한 바람직하게 남는다는 사실을 기억하라. 따라서 위에서 소개된 단점은 이번 장에서 살펴본 BusinessObject나 Broker와 같은 주요 추상 클래스에는 적용되지 않는다. 오버라이드된 메서드와 다형적 행위는 (예: Broker>>objectClass와 BusinessObject>>identityKey 메서드) 이러한 추상 클래스의 서브클래스에서 많이 찾아볼 수 있으며, 이 상속 기반의 디자인이 전적으로 유용하며 가치가 있음을 확신시켜줄 것이다.
Notes
- ↑ Erich는 Xerox PARC의 프로그래머들이 이러한 용어를 만들었다고 말한다.
- ↑ 이 문제에는 약한 참조 외에도 다른 해답이 있지만 모두 만족스럽지 못하다. 이 해답들은 business 객체로의 참조를 기록하는 일을 수반하는데, 이는 쓰레기 수집기의 작업을 복제하는 것과 맞먹는다. 이 프레임워크의 C++ 버전에는 쓰레기 수집이 결여되어 있기 때문에 약한 참조 또한 결여되므로 상당히 크고 정교한 이 작업에 방해가 될 수밖에 없다.
- ↑ 헐리우드 원칙은 프로그래밍 추세를 보여준다: 애플리케이션 프로그래머들은 다른 영역의 안내서와 기대치에 따른 코드를 점점 더 많이 작성한다. 이 추세의 또 다른 예제로 제 12장에서 작성한, 이벤트와 콜백에 응답하는 핸들러를 들 수 있겠다. 그 외에 초기 중앙 컴퓨터 시스템 프로그램에서 사용자 출구의 객체 지향 버전에 해당하는 훅(hook) 메서드가 있다 (66페이지 aside 참고). 당신에겐 핸들러나 훅 메서드를 작성해야 할 의무가 없기 때문에 이들은 헐리우드 원칙의 변형(variation)을 나타내며, 호출이 들어올 때 당신이 없어도 된다. 반면 subclassResponsibility 메서드는 의무적이다ㅡ호출이 들어올 때 그곳에 있지 않으면 문제가 발생할 것이다.