이번 강좌는 The Tcl Programming Language 도서의 Object Oriented Programming in Tcl 챕터를 번역한 내용임을 밝힙니다.
1. Tcl 객체 지향 프로그래밍 TclOO
이번에는 객체 지향 프로그래밍을 지원하는 TclOO에 대해 설명합니다. 객체 지향 프로그래밍이 무엇을 의미하는지, 그 장점이 무엇인지, 또는 클래스가 어떻게 설계되어야 하는지에 대해 자세히 다루지는 않습니다. 하지만, 객체 지향 프로그래밍에 대해 전혀 접해보지 않은 분들을 위해 중간중간 몇 가지 기본 개념을 간략히 설명할 것입니다.
초기 Tcl에 대한 비판 중 하나는 객체 지향 프로그래밍(OOP)을 지원하지 않는다는 것이었습니다. 그러나 이 비판은 사실과 달랐는데 Tcl은 실제로 하나가 아니라 여러 개의 객체 지향 구현을 지원했기 때문입니다. 이러한 오해는 적어도 부분적으로, 이 객체 지향 시스템들이 Tcl의 핵심 언어에 내장되어 있지 않고, 확장 패키지 형태로 구현되었기 때문이었습니다. 이 시스템들 중 일부는 상당히 널리 사용되었으며, 오늘날에도 여전히 사용되고 있습니다.
- Incr Tcl의 이름은 C++에서 따온 것이며, 설계도 C++을 연상시키는 방식입니다. C++에 익숙한 프로그래머들이 쉽게 적응할 수 있도록 만들어졌습니다. 가장 초기의 Tcl 기반 객체 지향 확장 중 하나로 널리 사용되었습니다.
- Snit(Snit’s Not Incr Tcl)는 인기 있는 객체 지향 구현으로, 특히 Tk 위젯을 만드는 데 유용합니다.
- XoTcl과 그 후속인 nx는 동적 객체 지향 프로그래밍 연구를 위해 설계된 객체 지향 구현입니다.
이러한 시스템에서 얻은 경험을 바탕으로 Tcl의 코어에 객체 지향 시스템이 구현되었고, 그것이 바로 TclOO입니다. TclOO는 Tcl 8.6 버전에 포함되었으며, Tcl 8.5에서는 확장 패키지 형태로 제공됩니다. TclOO는 독립적인 객체 지향 시스템으로도 사용할 수 있습니다. 하지만 TclOO의 목표 중 하나는 다른 객체 지향 시스템들이 그 위에 계층화될 수 있는 기반 기능을 제공하는 것이었습니다. 여기서 설명하는 Tcl 기반 객체 지향 프로그래밍은 TclOO를 바탕으로 합니다.
이 강좌의 대부분의 예제 코드는 은행 모델링을 위한 프레임워크를 기반으로 합니다. 은행에는 예금, 당좌 등 다양한 종류의 계좌가 있으며, 입금과 출금과 같은 작업을 할 수 있습니다. 이러한 작업 중 일부는 모든 계좌 유형에 공통적으로 적용되지만, 일부는 특정 계좌에만 해당됩니다. 그리고 특별한 대우를 받는 우대 고객들이 있으며, 우리는 또한 '빅 브라더'의 특정 지침을 따라야 합니다.
물론, 실제로 은행이 우리의 프레임워크를 기반으로 운영할 수는 없겠지만, 예제 설명을 위한 목적에는 충분합니다.
2. 클래스
2.1. 객체와 클래스
객체 지향 프로그래밍의 핵심은, 예상하셨겠지만 바로 객체입니다. 객체는 종종 현실 세계의 어떤 엔티티를 표현하며, 상태(데이터)와 행동(메시지에 대한 객체의 응답)를 캡슐화합니다. 대부분의 언어에서 이러한 메시지의 구현은 객체와 연관된 컨텍스트를 가진 메서드 호출, 즉 함수 호출을 통해 이루어집니다. 예를 들어, 은행 계좌를 나타내는 객체의 상태에는 현재 잔액과 계좌번호 등이 포함될 수 있습니다. 이 객체는 입금이나 출금 요청 메시지에 응답하여 동작합니다.
클래스는 특정 타입의 객체가 캡슐화하는 데이터 항목과 메서드(멤버)를 정의하는 템플릿입니다. 대부분의 경우, 클래스의 역할 중 하나는 해당 클래스의 객체를 생성(인스턴스화)하는 것입니다.
모든 객체 지향 시스템이 클래스라는 개념을 갖고 있는 것은 아니며, 반드시 필요한 것도 아닙니다. 프로토타입 기반 시스템에서는 기존 객체(프로토타입)를 "복제"하고 멤버를 정의하거나 수정함으로써 새로운 객체를 만듭니다.
TclOO는 클래스 기반 모델과 클래스 없는 모델 모두를 지원하는 기능을 제공합니다.
2.2. 클래스 생성하기
TclOO에서는 oo::class create 명령어를 사용하여 클래스를 생성합니다. 은행 계좌를 모델링하는 Account 클래스를 만들어 보겠습니다.
% oo::class create Account
::Account
이 명령어는 은행 계좌를 표현하는 객체를 생성할 수 있는 새로운 클래스 Account를 만듭니다.
Account 클래스는 사실 또 하나의 Tcl 커맨드로 생성될 뿐이며, 반드시 글로벌 네임스페이스가 아니라 우리가 원하는 어떤 네임스페이스에서도 생성할 수 있습니다. 예를 들어,
oo::class create bank::Account
또는
namespace eval bank {oo::class create Account}
와 같이 하면 bank 네임스페이스에 새로운 Account 클래스를 생성하게 되며, 이는 글로벌 네임스페이스에 있는 Account 클래스와는 완전히 별개의 클래스가 됩니다.
하지만 아직 Account 클래스에는 클래스 정의가 없으므로, 해당 클래스의 객체에 대한 상태나 동작이 정의되어 있지 않습니다. 이러한 상태와 동작은 하나 이상의 클래스 정의 스크립트를 통해 설정됩니다. 이 강좌에서는 이러한 정의 스크립트의 내용을 점진적으로 추가할 생각입니다.
클래스 정의 스크립트는 추가 인자로 oo::class create 명령어에 전달할 수 있습니다. 형식은 다음과 같습니다.
oo::class create CLASSNAME DEFINITIONSCRIPT
또는, 다음과 같은 형태로 하나 이상의 oo::define 명령어를 통해 정의할 수도 있습니다.
oo::define CLASSNAME DEFINITIONSCRIPT
따라서 아래의 두 문장
oo::class create CLASSNAME DEFINITIONSCRIPT
와
oo::class create CLASSNAME
oo::define CLASSNAME DEFINITIONSCRIPT
는 서로 동등합니다.
이 강좌에서는 이 두 가지 형태의 클래스 정의 방식을 모두 볼 수 있습니다. 일반적으로 Tcl은 여러분의 프로그래밍 스타일에 맞춰 유연하게 사용할 수 있습니다.
2.3. 클래스 제거하기
클래스도 객체이기 때문에, 모든 객체와 마찬가지로 삭제할 수 있습니다.
% Account destroy
위를 실행하면 다음과 같은 일들이 발생합니다:
- Account 클래스의 정의가 삭제됩니다.
- Account 클래스를 상속하거나 믹스인하는 모든 클래스가 삭제됩니다.
- 삭제된 모든 클래스에 속한 객체들도 모두 삭제됩니다.
이처럼 클래스를 제거하는 기능은 보통의 코드에서는 흔히 사용되지 않지만, 인터랙티브 개발이나 디버깅 과정에서 깨끗한 상태로 초기화할 때는 유용하게 쓰일 수 있습니다.
Account 클래스를 계속 사용할 것이므로, 다음과 같이 다시 생성해줍니다.
% oo::class create Account
::Account
2.4. 데이터 멤버 정의하기
간단한 예제에서 계좌 객체의 상태는 해당 계좌를 고유하게 식별하는 계좌번호와 현재 잔액을 포함합니다. 이 정보를 저장할 데이터 멤버가 필요하며, 이를 oo::define 명령에 전달하는 클래스 정의 스크립트를 통해 정의합니다.
% oo::define Account {
variable AccountNumber Balance
}
이렇게 하면 클래스에 대해 객체별 변수로 데이터 멤버가 정의됩니다. AccountNumber와 Balance는 클래스의 모든 메서드 내에서 별도의 지정이나 선언 없이 사용할 수 있게 됩니다.
여러 개의 variable 문을 사용할 수 있으며, 각 문에서 하나 이상의 데이터 멤버를 정의할 수 있습니다.
데이터 멤버는 반드시 클래스 정의 스크립트에서 variable로 선언할 필요는 없습니다. 이후에 보여드릴 my variable 명령을 사용하여 메서드 내에서 선언할 수도 있습니다.
참고로 클래스 정의 내의 variable 문은 데이터 멤버의 이름만 정의하고 값을 지정하지 않는 반면, 네임스페이스에서 사용하는 variable 명령은 이름뿐 아니라 초기값까지 정의합니다.
2.5. 메서드 정의하기
데이터 멤버를 정의한 후에는 Account 객체의 동작을 구성하는 메서드를 정의해보겠습니다. Account 객체는 현재 잔액을 조회하거나, 입금 및 출금을 요청받을 때 이에 응답해야 합니다.
메서드는 variable과 마찬가지로 클래스 정의 스크립트 내부에서 method 명령을 통해 정의합니다.
oo::define Account {
method UpdateBalance {change} {
set Balance [+ $Balance $change]
return $Balance
}
method balance {} { return $Balance }
method withdraw {amount} {
return [my UpdateBalance -$amount]
}
method deposit {amount} {
return [my UpdateBalance $amount]
}
}
보시는 것처럼, 메서드는 Tcl의 proc으로 프로시저를 정의하는 방식과 정확히 동일하게 정의됩니다. 메서드도 임의의 개수의 인자를 받을 수 있으며, 마지막에 여러 인자를 args 변수로 모을 수도 있습니다. 프로시저와의 차이점은 메서드가 호출되는 방식과, 메서드가 실행되는 컨텍스트에 있습니다.
메서드 컨텍스트
메서드는 해당 객체의 네임스페이스 컨텍스트에서 실행됩니다. 즉, Balance와 같이 variable로 정의된 객체의 데이터 멤버들이 메서드의 스코프 내에 있으므로, 위의 메서드 정의에서 보았듯이 별도의 지정 없이 직접 참조할 수 있습니다.
메서드 컨텍스트에서는 self, next, my 등 여러 명령어들도 사용할 수 있는데, 이러한 명령어들은 메서드 내부에서만 호출할 수 있습니다. 예를 들어, 현재 실행 중인 메서드와 같은 객체 컨텍스트의 다른 메서드를 참조할 때는 my를 사용합니다.
메서드 가시성
메서드 정의에서 또 하나 중요한 점은 메서드의 가시성(visibility)입니다. exported(내보내기)된 메서드는 객체의 컨텍스트 외부에서 호출할 수 있는 메서드입니다. 반면, private(비공개) 메서드는 객체 내부의 다른 메서드에서만 호출할 수 있습니다. 메서드 이름이 소문자로 시작하면 기본적으로 exported 메서드가 됩니다. 예를 들어, deposit과 withdraw는 exported 메서드이며, UpdateBalance는 그렇지 않습니다. 메서드의 가시성은 oo::define 클래스 정의 스크립트 내에서 export와 unexport 명령을 사용하여 변경할 수 있습니다.
아래와 같이 하면 UpdateBalance도 exported 메서드가 됩니다.
oo::define Account {export UpdateBalance}
메서드 삭제
메서드 정의는 oo::define 클래스 정의 스크립트 내에서 deletemethod 명령으로 언제든지 삭제할 수 있습니다.
클래스에서 메서드를 삭제하는 것은 거의 사용되지 않지만, 객체에서 메서드를 삭제하는 것은 객체의 특수화(specialization)에서 때때로 유용하게 활용됩니다.
oo::class create C {method print args {puts $args}}
C create c
c print some nonsense
oo::define C {deletemethod print}
c print more of the same
2.6. 생성자와 소멸자
은행 계좌 작업을 시작하기 전에, 객체가 생성될 때 Account 객체를 초기화하고 삭제될 때 필요한 정리 작업을 수행할 방법을 제공해야 합니다.
이 작업들은 특별한 이름을 가진 메서드인 constructor(생성자)와 destructor(소멸자)를 통해 이루어집니다. 이들은 일반 메서드와 두 가지 점에서만 다릅니다:
- 명시적으로 호출할 수 없습니다. 객체가 생성될 때 constructor 메서드가 자동으로 실행되며, 객체가 삭제될 때 destructor 메서드가 자동으로 실행됩니다.
- destructor 메서드는 다른 메서드와 달리 인자 목록이 없습니다. 실행할 스크립트만 인자로 받습니다.
간단한 예시를 보겠습니다:
oo::define Account {
constructor {account_no} {
puts "Reading account data for $account_no from database"
set AccountNumber $account_no
set Balance 1000000
}
destructor {
puts "[self] saving account data to database"
}
}
생성자와 소멸자는 모두 선택사항입니다. 정의하지 않으면 TclOO가 빈 메서드를 자동으로 생성합니다.
2.7. unknown 메서드
모든 객체에는 unknown이라는 이름의 메서드가 있습니다. 이 메서드는 해당 객체에서 해당 이름의 메서드가 정의되어 있지 않을 때 실행됩니다.
unknown 메서드는 다음과 같이 정의합니다:
oo::define CLASSNAME {
method unknown {target_method args} {…implementation…}
}
unknown 메서드는 호출된 메서드의 이름을 첫 번째 인자로, 호출 시 넘겨진 인자들을 그 뒤에 전달받습니다.
기본 구현은 oo::object(모든 객체의 루트)에서 상속되어 모든 객체에 적용되며, 오류를 발생시킵니다. 하지만 클래스나 객체에서 이 메서드를 오버라이드하여 다른 동작을 하도록 할 수 있습니다.
실제 예시로 TWAPI의 COM 클라이언트 구현이 있습니다. COM 컴포넌트에서 내보내는 속성(property)이나 메서드는 미리 알 수 없고, 동적으로 바뀔 수도 있습니다. TclOO 기반 COM 객체 래퍼는 unknown 메서드를 정의하여, 처음 호출되는 메서드 이름을 COM 컴포넌트에 조회합니다. 만약 찾을 수 있다면 함수 테이블의 인덱스를 반환하고, 그 인덱스를 ComCall 메서드를 통해 호출합니다.
예시 구현은 다음과 같습니다.
oo::define COMWrapper {
method unknown {method_name args} {
set method_index [COMlookup $method_name]
if {$method_index < 0} {
error "Method $method_name not found."
}
return [my ComCall $method_index {*}$args]
}
}
2.8. 기존 클래스 수정하기
앞에서 살펴본 것처럼, oo::define을 사용해 클래스를 점진적으로 수정할 수 있습니다. 클래스에 대해 거의 모든 것을 변경할 수 있습니다. 즉, 메서드나 데이터 멤버의 추가/삭제, 슈퍼클래스나 믹스인의 변경 등 다양한 수정이 가능합니다.
그렇다면 이미 생성된 객체는 클래스가 수정될 경우 어떻게 될까요? 답은, 기존 객체들도 자동으로 수정된 클래스 정의를 "보게" 됩니다. 예를 들어, 새로운 메서드를 추가하면 기존 객체에서도 해당 메서드를 바로 사용할 수 있습니다. 믹스인이나 슈퍼클래스를 추가하면 객체의 메서드 탐색 순서도 적절히 변경됩니다.
하지만 클래스를 수정할 때는 주의가 필요합니다. 기존 객체들은 새 클래스가 기대하는 모든 상태(데이터 멤버 등)를 가지고 있지 않을 수 있습니다. 예를 들어, 새로운 생성자는 기존 객체에 대해 실행되지 않으므로, 일부 데이터 멤버가 초기화되지 않을 수 있습니다. 따라서 수정된 클래스 코드에서는 이러한 상황을 처리해줘야 합니다.
3. 객체 다루기
모델을 정의했으니 이제 은행의 운영을 시작하며 객체가 어떻게 사용되는지 살펴보겠습니다.
3.1. 객체 생성하기
클래스의 객체는 클래스 자체에서 제공되는 두 가지 내장 메서드 중 하나를 호출하여 생성합니다.
- `create` 메서드는 지정한 이름으로 객체를 생성합니다.
- `new` 메서드는 생성된 객체에 대해 이름을 자동으로 생성합니다.
% set acct [Account new 3-14159265]
Reading account data for 3-14159265 from database
::oo::Obj70
% Account create smith_account 2-71828182
Reading account data for 2-71828182 from database
::smith_account
객체를 생성하면 생성자가 자동으로 호출되어 객체가 초기화됩니다. 생성된 객체는 Tcl 커맨드 이므로, 어떤 네임스페이스에서든 생성할 수 있습니다.
% namespace eval my_ns {Account create my_account 1-11111111}
Reading account data for 1-11111111 from database
::my_ns::my_account
% Account create my_ns::another_account 2-22222222
Reading account data for 2-22222222 from database
::my_ns::another_account
`my_account`와 `my_ns::my_account`는 서로 다른 객체입니다.
3.2. 객체 삭제하기
Tcl에서 객체는 다른 언어처럼 가비지 컬렉션되지 않으며, 반드시 내장된 destroy 메서드를 호출하여 명시적으로 삭제해야 합니다. 이 과정에서 객체의 소멸자(destructor)도 실행됩니다.
% my_ns::my_account destroy
::my_ns::my_account saving account data to database
삭제된 객체에 대해 어떤 작업을 시도하면 오류가 발생합니다.
% my_ns::my_account balance
invalid command name "my_ns::my_account"
객체는 클래스나 포함된 네임스페이스가 삭제될 때도 함께 삭제됩니다.
% namespace delete my_ns
::my_ns::another_account saving account data to database
% my_ns::another_account balance
invalid command name "my_ns::another_account"
이처럼 삭제된 객체는 더 이상 사용할 수 없으며, 커맨드를 실행하면 오류가 발생합니다.
3.3. 메서드 호출하기
Tcl에서 객체는 다음과 같은 앙상블(ensemble) 명령 형태로 동작합니다.
OBJECT METHODNAME args...
즉, 객체 외부 코드에서 객체의 메서드를 호출할 때 이 형태를 사용합니다.
% $acct balance
1000000
% $acct deposit 1000
1001000
앞서 설명한 것처럼, 같은 객체 컨텍스트 내의 다른 메서드를 호출할 때는 my라는 별칭을 사용하여 현재 객체를 참조합니다.
따라서, deposit 메서드 내부에서 UpdateBalance를 호출할 때는 다음과 같이 작성합니다:
my UpdateBalance $amount
3.4. 데이터 멤버 접근하기
데이터 멤버는 객체 외부에서 직접 접근할 수 없습니다. 값을 읽거나 수정하려면 balance와 같은 메서드를 정의하여 호출자가 접근할 수 있도록 해야 합니다. 많은 객체지향(OOP) 순수주의자들은 물론, 비순수주의자들조차도 이러한 방식이 바람직하다고 생각합니다.
하지만 Tcl의 특성상, introspection을 통해 객체의 private 네임스페이스를 알 수 있으므로 변수에 직접 접근하는 것도 가능합니다.
% set [info object namespace $acct]::Balance 5000
5000
% $acct balance
5000
이러한 방식은 캡슐화를 깨뜨리므로 권장되지 않습니다. 그러나 일부 TclOO 기반 OO 시스템에서는 내부 객체 네임스페이스를 직접 노출하지 않고 구조화된 방식으로 변수 접근 기능을 제공하기도 합니다(여기서는 다루지 않음).
더 좋은 대안은 프로그래머가 명시적으로 accessor 메서드를 작성하지 않아도, public 변수에 대해 자동으로 접근자(accessor) 메서드를 정의하는 것입니다. 이에 대한 한 가지 구현은 Lifecycle Object Generators 논문에서 설명되어 있습니다.
4. 상속(Inheritance)
객체지향(OOP) 시스템의 핵심 특징은 상속입니다. 상속은 파생 클래스(서브클래스)가 기반 클래스(슈퍼클래스)의 동작을 확장하거나 변경하여 특수화(specialization)할 수 있게 해줍니다.
예를 들어, 은행 모델에서는 저축계좌(SavingsAccount)와 당좌계좌(CheckingAccount) 클래스를 각각 정의할 수 있습니다. 이들은 기본 Account 클래스를 상속하여 잔액(balance), 입금(deposit) 및 출금(withdrawal) 메서드를 공유하며, 각 클래스별로 추가 기능을 구현할 수도 있습니다. 예를 들면, 당좌계좌에는 수표 발행 기능을, 저축계좌에는 이자 지급 기능을 추가할 수 있습니다.
상속의 목적은 is-a 관계를 모델링하는 데 있습니다. 즉, 당좌계좌는 은행 계좌이고, 은행 계좌가 예상되는 모든 곳에서 사용할 수 있습니다. 이런 is-a 관계가 상속과 믹스인(mixin) 등의 다른 구조 중 어느 것을 사용할지 결정하는 기준이 됩니다.
SavingsAccount와 CheckingAccount를 정의해보겠습니다. 이번에는 oo::define이 아니라 oo::class 명령으로 클래스를 한 번에 정의합니다.
oo::class create SavingsAccount {
superclass Account
variable MaxPerMonthWithdrawals WithdrawalsThisMonth
constructor {account_no {max_withdrawals_per_month 3}} {
next $account_no
set MaxPerMonthWithdrawals $max_withdrawals_per_month
}
# monthly_update: 월별 이자 지급
method monthly_update {} {
my variable Balance
my deposit [format %.2f [* $Balance 0.005]];
set WithdrawalsThisMonth 0
}
method withdraw {amount} {
if {[incr WithdrawalsThisMonth] > $MaxPerMonthWithdrawals} {
error "You are only allowed $MaxPerMonthWithdrawals withdrawals a month"
}
next $amount
}
}
oo::class create CheckingAccount {
superclass Account
# cash_check: 수표 발행 기능(예시)
method cash_check {payee amount} {
my withdraw $amount
puts "Writing a check to $payee for $amount";
}
}
클래스 정의에서 superclass 명령을 사용하여 SavingsAccount와 CheckingAccount가 Account를 상속하도록 합니다. 이 명령만으로도 두 클래스는 Account와 동일한 동작(메서드, 변수 등)을 가지게 되고, 추가 선언을 통해 동작을 확장하거나 변경할 수 있습니다.
4.1. 파생 클래스의 메서드
기본 클래스에서 사용할 수 있는 메서드는 파생 클래스(서브클래스)에서도 모두 사용할 수 있습니다. 이외에도 cash_check나 monthly_update와 같이 파생 클래스에만 존재하는 새로운 메서드를 정의할 수 있습니다.
파생 클래스에서 기본 클래스와 동일한 이름의 메서드를 정의하면, 그 메서드는 오버라이드되어 파생 클래스 객체에서 해당 메서드를 호출하면 파생 클래스의 메서드가 실행됩니다. 예를 들어, SavingsAccount의 withdraw 메서드는 Account의 withdraw 메서드를 오버라이드합니다. 하지만 여기서는 원래 기능을 완전히 대체하는 것이 아니라, 조건을 추가하여 원래 메서드도 그대로 사용하고 싶습니다. 이럴 때는 next 명령을 사용하여, 현재 메서드 체인에서 다음 슈퍼클래스의 같은 이름의 메서드를 호출할 수 있습니다. 이렇게 하면 코드 중복 없이 확장만 할 수 있습니다. next를 호출하는 위치는 메서드의 시작이나 끝에 한정되지 않고, 어디든 가능합니다.
이와 유사하게 생성자(constructor)와 소멸자(destructor)도 체인 방식으로 동작합니다. 만약 파생 클래스가 생성자를 정의하지 않으면, 객체 생성 시 기본 클래스의 생성자가 호출됩니다. 파생 클래스가 생성자를 정의했다면, 해당 생성자가 먼저 실행되며, 다음에 next를 사용하여 필요할 때 기본 클래스 생성자를 호출할 수 있습니다. 소멸자도 동일하게 동작합니다.
- 파생 클래스는 기본 클래스의 메서드를 상속받고, 오버라이드가 가능합니다.
- next 명령을 사용해 슈퍼클래스의 메서드(혹은 생성자, 소멸자)를 호출할 수 있습니다.
- next는 메서드 내부 어디서든 호출할 수 있습니다.
4.2. 파생 클래스의 데이터 멤버
파생 클래스(derived class)는 클래스 정의 내에서 `variable`이나, 메서드 내에서 `my variable`을 사용해 새로운 데이터 멤버를 정의할 수 있습니다. 부모 클래스에서 정의된 데이터 멤버도 파생 클래스 내에서 접근할 수 있지만, 파생 클래스의 정의나 메서드에서 `variable` 선언이나 `my variable` 구문을 통해 해당 멤버를 스코프에 포함시켜야 합니다. 예를 들어, monthly_update 구현에서 직접 변수 참조를 사용했지만, 데이터 은닉과 캡슐화 측면에서는 직접 참조보다는 메서드를 통해 접근하는 것이 더 바람직합니다. 따라서 아래와 같이 작성하는 것이 좋습니다:
my deposit [format %.2f [* [my balance] $rate]]
새로 정의한 계좌 클래스를 사용해 봅시다.
% SavingsAccount create savings S-12345678 2
Reading account data for S-12345678 from database
::savings
% CheckingAccount create checking C-12345678
Reading account data for C-12345678 from database
::checking
% savings withdraw 1000
999000 <-- 기본 클래스의 메서드(예: withdraw)를 오버라이딩함
% savings withdraw 1000
998000
% savings withdraw 1000
You are only allowed 2 withdrawals a month
% savings monthly_update
0
% checking cash_check Payee 500
Writing a check to Payee for 500 <-- 파생 클래스에서 새로 정의한 cash_check 메서드 사용
% savings cash_check Payee 500
unknown method "cash_check": must be balance, deposit, destroy, monthly_updat... <-- 저축계좌(savings)에는 수표(cash_check) 기능이 없음
4.3. 다중 상속(Multiple Inheritance)
은행이 증권 거래 서비스도 제공하는 금융 회사라고 가정해봅시다. 이에 따라 증권 계좌에 해당하는 BrokerageAccount 클래스를 정의합니다.
oo::class create BrokerageAccount {
superclass Account
method buy {ticker number_of_shares} {
puts "Buy high"
}
method sell {ticker number_of_shares} {
puts "Sell low"
}
}
이제 회사는 고객이 더 쉽게 주식 시장에 "참여"할 수 있도록, 당좌 계좌(CheckingAccount)와 증권 계좌(BrokerageAccount)의 기능을 모두 가진 현금 관리 계좌(Cash Management Account, CMA)를 만들기로 합니다. 이를 시스템에서 모델링하려면 다중 상속을 사용하여 해당 클래스가 둘 이상의 부모 클래스를 상속받도록 할 수 있습니다.
oo::class create CashManagementAccount {
superclass CheckingAccount BrokerageAccount
}
CMA 계좌는 모든 기능을 사용할 수 있습니다:
% CashManagementAccount create cma CMA-00000001
Reading account data for CMA-00000001 from database
::cma
% cma cash_check Payee 500
Writing a check to Payee for 500
% cma buy GOOG 100
Buy high
다중 상속은 OO(객체지향) 분야에서 논란이 있는 주제이지만, TclOO는 이 기능과 믹스인(mixin)을 모두 제공하며, 설계 선택은 프로그래머에게 맡깁니다. 이것이 올바른 방식입니다.
5. 객체 특수화(Specializing objects)
다음으로 설명할 객체 특수화(object specialization)는 C++ 같은 클래스 기반 객체지향 언어에 익숙한 독자에게는 다소 새로운 개념일 수 있습니다. 이런 언어에서는 객체에 연결된 메서드는 객체가 속한 클래스(들)에 정의된 메서드로 한정됩니다.
하지만 TclOO에서는, 개별 객체에 대해 클래스에서 정의된 메서드를 오버라이드(재정의), 숨김, 삭제하거나 새로운 메서드를 추가하는 등 추가적인 "특수화"가 가능합니다. 실제로 TclOO에서 지원하는 특수화에는 forwarding, filter, mix-in 등의 다양한 기능도 포함되지만, 이들은 아직 다루지 않았으므로 여기서는 생략합니다. 심지어 객체의 클래스를 변경하는 것도 가능합니다.
객체 특수화는 `oo::objdefine` 명령을 사용합니다. 이는 클래스에 대해 사용하는 `oo::define`과 유사하지만, 클래스가 아니라 객체를 인자로 받는다는 점이 다릅니다. `oo::define` 스크립트에서 사용했던 method, variable 등 대부분의 명령을 `oo::objdefine`에서도 동일하게 사용할 수 있습니다.
5.1. 객체별 메서드(Object-specific methods)
예를 들어, 은행 시스템에서 세무 당국의 명령으로 개별 계좌를 동결해야 하는 상황을 가정해봅시다. 계좌를 동결(freeze)하면 모든 거래가 거부되어야 하며, 나중에 계좌를 해제(unfreeze)하는 기능도 필요합니다. 아래 코드는 이를 구현한 예시입니다.
proc freeze {account_obj} {
oo::objdefine $account_obj {
method UpdateBalance {args} {
error "Account is frozen. Don't mess with the IRS, dude!"
}
method unfreeze {} {
oo::objdefine [self] { deletemethod UpdateBalance unfreeze }
}
}
}
freeze 프로시저는 Account 객체를 받아서, 해당 객체에 대해 `oo::objdefine`을 사용해 UpdateBalance 메서드를 오버라이딩합니다. 이제 이 객체에서 UpdateBalance가 호출되면 오류가 발생하게 되어, 실질적으로 모든 거래를 차단하게 됩니다.
또한 unfreeze라는 새로운 메서드를 정의해서, 필요할 때 객체에서 호출하면 동결을 해제할 수 있습니다. 동결 해제는 아래와 같이 프로시저로도 구현할 수 있습니다.
proc unfreeze {account_obj} {
oo::objdefine $account_obj {deletemethod UpdateBalance}
}
이 방식이 더 명확하지만, 객체 내부에 정의된 메서드를 통해서도 객체의 정의를 변경할 수 있음을 보여주기 위해 unfreeze 메서드도 예시로 들었습니다.
다음은 추가 설명이 필요한 사항입니다.
self 명령어
self 명령은 메서드 내부에서만 사용 가능하며, 파라미터 없이 호출하면 현재 객체의 이름을 반환합니다.
따라서 `oo::objdefine [self] {...}`처럼 작성하면 해당 객체 자체를 수정하게 됩니다.
self의 다른 활용법도 이후에 다룹니다.
클래스 변수의 가시성
클래스에서 정의한 변수는 객체 특수화(oo::objdefine)로 추가한 메서드에서는 자동으로 스코프에 포함되지 않습니다.
해당 변수에 접근하려면 반드시 `my variable` 구문을 사용해 스코프에 가져와야 합니다.
deletemethod의 동작
oo::objdefine에서 deletemethod는 오브젝트에 특화된 메서드만 삭제합니다.
클래스에 정의된 메서드는 영향을 받지 않으므로, 오버라이드가 해제되면 원래 클래스의 메서드가 다시 활성화됩니다.
아래는 작동 예시입니다. 현재 smith_account는 자유롭게 인출 가능합니다.
% smith_account withdraw 100
999900
법원 명령으로 smith_account를 동결합니다.
% freeze smith_account
Mr. Smith가 전액 인출 시도합니다.
% smith_account withdraw [smith_account balance]
Account is frozen. Don't mess with the IRS, dude!
다른 고객 계좌는 영향이 없습니다.
% $acct withdraw 100
4900
동결 해제 후 다시 인출 시도합니다.
% smith_account unfreeze
% smith_account withdraw 100
999800
이렇게 객체별로 메서드를 정의할 수 있는 기능은 매우 유용합니다. 예를 들어, 캐릭터를 객체로 모델링하는 컴퓨터 게임을 만든다고 가정해봅시다. 캐릭터의 움직임을 결정하는 물리 엔진 등과 같은 공통적인 특성은 클래스 정의로 캡슐화할 수 있습니다. 하지만 각 캐릭터가 가지는 "특수 능력"은 클래스에 포함시키기 어렵고, 캐릭터마다 별도의 클래스를 만드는 것도 지나치게 번거로운 작업이 될 수 있습니다.
이럴 때 각 캐릭터의 객체에 객체별 메서드로 특수 능력을 추가할 수 있습니다. 이를 통해 캐릭터별로 고유한 행동을 손쉽게 부여할 수 있고, 특수 능력을 일시적으로 잃거나 복구하는 상황도 여러 조건문이나 복잡한 상태 관리 없이 객체 특수화 메커니즘으로 간단하게 구현할 수 있습니다.
5.2. 객체의 클래스 변경하기
동적 OO 언어의 특징답게, TclOO에서는 `oo::objdefine`을 통해 객체의 클래스를 변경할 수도 있습니다. 예를 들어, 저축계좌(SavingsAccount)를 당좌계좌(CheckingAccount)로 변경할 수 있습니다.
% set acct [SavingsAccount new C-12345678]
Reading account data for C-12345678 from database
::oo::Obj81
% $acct monthly_update
0
이 계좌에서 수표(cash_check)를 시도하면 다음과 같이 실패합니다.
% $acct cash_check Payee 100
unknown method "cash_check": must be balance, deposit, destroy, monthly_updat...
하지만 다음과 같이 객체의 클래스를 바꿀 수 있습니다:
% oo::objdefine $acct class CheckingAccount
이후에는 수표(cash_check)가 정상 작동합니다.
% $acct cash_check Payee 100
Writing a check to Payee for 100
반면, monthly_update 메서드는 더 이상 사용할 수 없습니다(해당 클래스에 정의되지 않았으므로).
% $acct monthly_update
unknown method "monthly_update": must be balance, cash_check, deposit, destro...
% $acct destroy
::oo::Obj81 saving account data to database
이처럼 객체의 클래스를 변경("morphing")할 때는 두 클래스 간 데이터 멤버 구조가 다를 수 있으므로 주의해야 합니다.
Lifecycle Object Generators 논문에서는 이러한 객체 클래스 변경(morphing)의 활용 예시로 상태 기계(state machine)를 들고 있습니다. 상태 기계에서 각 상태는 해당 상태의 동작을 구현하는 클래스로 모델링됩니다. 상태가 변경될 때, 상태 기계 객체는 자신의 클래스를 변경하여 새로운 상태에 해당하는 클래스를 사용하게 됩니다. 이 방식으로 객체의 행동을 동적으로 변경할 수 있습니다.
구현 세부 사항은 해당 논문(Lifecycle Object Generators)을 참고하면 됩니다.
6. 믹스인(Mixins) 활용하기
앞서 상속을 이용해 클래스를 확장하는 방법을 살펴봤습니다. 이제 클래스(와 객체)의 동작을 확장하거나 변경하는 또 다른 방법인 믹스인(mixin)을 알아봅니다.
믹스인은 언어마다 다양한 방식으로 설명되지만, 저자의 관점에서는 관련 기능을 묶어서 하나 이상의 클래스나 객체에 쉽게 확장할 수 있도록 하는 패키지입니다. 일부 언어에서는 다중 상속으로 이 목적을 달성하기도 하지만, 여기서는 믹스인 예시를 먼저 살펴보겠습니다. 전자 자금 이체(EFT) 기능을 생각해봅시다. 모든 저축계좌(SavingsAccount)에는 EFT가 있지만, 당좌계좌(CheckingAccount)에는 선택적으로만 EFT가 적용됩니다. 여러 구현 방법이 있지만, 여기서는 믹스인 방식을 선호합니다.
TclOO에서 믹스인은 클래스처럼 정의합니다. 이론적으로는 모든 클래스가 믹스인으로 사용될 수 있지만, 믹스인은 "기능(역할/role)"을 추가하는 개념입니다.
oo::class create EFT {
method transfer_in {from_account amount} {
puts "Pretending $amount received from $from_account"
my deposit $amount
}
method transfer_out {to_account amount} {
my withdraw $amount
puts "Pretending $amount sent to $to_account"
}
}
모든 당좌계좌에 믹스인 적용:
% oo::define CheckingAccount {mixin EFT}
% checking transfer_out 0-12345678 100
Pretending 100 sent to 0-12345678
% checking balance
999400
이제 모든 CheckingAccount 객체에서 전자이체가 가능합니다.
참고로 클래스 정의를 변경(예: 믹스인 추가)하면 해당 클래스로 생성된 기존 객체들도 영향을 받습니다. 따라서 CheckingAccount 클래스에 믹스인을 추가하면 이미 생성된 checking 객체 역시 자동으로 새 기능을 지원하게 됩니다.
특정 저축계좌에만 믹스인 적용:
% oo::objdefine savings {mixin EFT}
% savings transfer_in 0-12345678 100
Pretending 100 received from 0-12345678
1003090.0
% savings balance
1003090.0
EFT 클래스는 실제로 "계좌"를 알 필요가 없습니다. deposit과 withdraw 메서드만 있으면, EFT 믹스인을 아무 클래스나 객체에 추가할 수 있습니다. 즉, BrokerageAccount 같은 다른 계좌 클래스에도 EFT를 믹스인할 수 있습니다.
6.1. 다중 믹스인(Multiple Mixins) 사용하기
하나의 클래스나 객체에 여러 믹스인 클래스를 동시에 적용할 수 있습니다. 예를 들어, 전자 청구서(BillPay) 기능을 믹스인 클래스로 구현했다면, 아래처럼 EFT와 함께 CheckingAccount에 믹스인할 수 있습니다.
한 줄로 여러 믹스인 적용:
oo::define CheckingAccount {mixin BillPay EFT}
여러 줄로 순차적으로 믹스인 적용:
oo::define CheckingAccount {
mixin EFT
mixin -append BillPay
}
이렇게 하면 CheckingAccount 클래스의 모든 객체는 BillPay와 EFT 두 가지 기능을 모두 사용할 수 있습니다.
위 예시에서 사용된 `-append` 옵션의 의미는 다음과 같습니다. 기본적으로 `mixin` 명령은 기존 믹스인 구성을 덮어씁니다.
따라서 여러 번 `mixin` 명령을 사용할 때 `-append` 옵션 없이 작성하면 가장 마지막에 지정한 클래스(BillPay)만 CheckingAccount에 믹스인됩니다.
oo::define CheckingAccount {
mixin EFT ; # 기존 믹스인 구성은 EFT로 설정됨
mixin BillPay ; # 기존 믹스인 구성을 BillPay로 덮어씀 → 결과적으로 BillPay만 적용
}
반면, `-append` 옵션을 사용하면 기존 믹스인 구성에 새로운 클래스를 추가할 수 있습니다.
oo::define CheckingAccount {
mixin EFT
mixin -append BillPay ; # EFT와 BillPay 모두 적용됨
}
6.2. 믹스인 vs. 상속
TclOO는 다양한 OO 시스템을 구축할 수 있도록 다양한 기능을 제공합니다. 이 가운데 믹스인과 상속은 때때로 비슷한 효과를 내므로, 어떤 기능을 선택해야 할지 고민하게 됩니다.
상속 대신 믹스인을 선택한 이유
EFT 기능을 CheckingAccount에 믹스인하는 대신, 슈퍼클래스로 추가해 다중 상속을 사용할 수도 있고, 아예 CheckingAccount를 수정하거나 파생 클래스를 만들어 transfer 기능을 추가할 수도 있습니다. 하지만 직접 상속하거나 클래스를 수정하는 것은, 여러 계좌 타입에 적용할 수 있는 기능을 매번 중복 구현하게 되므로 바람직하지 않습니다.
상속의 한계
상속은 "is-a" 관계를 나타냅니다. 즉, "CheckingAccount is-a account with transfer features"라는 표현은 어색합니다. EFT는 실제 객체라기보다는 계좌가 가질 수 있는 "기능 세트"에 가깝고, 현실적으로는 체크박스 옵션처럼 동작합니다. 이런 클래스는 믹스인으로 모델링하는 것이 더 자연스럽습니다.
믹스인의 장점
믹스인은 개별 객체에 특정 기능을 개별적으로 제공할 수 있습니다. 즉, 특정 SavingsAccount에만 EFT 기능을 추가할 수 있지만, 다중 상속으로는 이런 객체별 특수화가 어렵습니다.
TclOO의 메서드 우선순위
TclOO에서 믹스인으로 구현된 메서드는 객체에 정의된 메서드보다 먼저 호출됩니다. 반면, 상속된 메서드는 객체 메서드보다 나중에 호출됩니다. 예제에서는 믹스인이 새 메서드만 추가했기 때문에 이 차이가 중요하지 않았지만, 오버라이드가 있을 경우 우선순위가 달라질 수 있습니다.
이러한 이유로, 본 예시에서는 믹스인을 사용하는 것이 더 적합했습니다.
7. Filter Methods
만약 Mr. Smith가 다시 수상한 행동을 한다면, 그의 계좌의 모든 활동을 감시하고 기록해야 합니다. 이를 위해 각 메서드를 oo::objdefine로 특수화하여 로그를 남기고 원래 메서드를 호출할 수도 있습니다. 하지만 객체, 클래스(및 슈퍼클래스), 오브젝트 믹스인, 클래스 믹스인에 정의된 모든 메서드에 대해 반복적으로 작업해야 하므로 매우 번거롭고 오류가 발생하기 쉽습니다. 또한, Tcl이 동적 언어이므로 메서드가 새로 추가될 때마다 계속 작업을 반복해야 합니다.
필터 메서드(filter method)는 훨씬 더 간단한 해결책을 제공합니다. 필터 메서드는 일반 메서드처럼 정의하고, filter 명령으로 필터임을 표시하면, 해당 객체의 모든 메서드 호출 시 필터가 가장 먼저 실행됩니다.
활동을 추적하고 싶은 계좌 객체에 필터 메서드를 추가합니다.
oo::objdefine smith_account {
method Log args {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
return [next {*}$args]
}
filter Log
}
이제 계좌의 모든 행동이 자동으로 기록됩니다.
% smith_account deposit 100
Log(1): 2-71828182 ::Account deposit: 100
Log(2): 2-71828182 ::Account UpdateBalance: 100
999900
출력에서 알 수 있듯이, deposit에서 내부적으로 호출된 UpdateBalance까지 모두 재귀적으로 로깅됩니다. 필터 메서드는 재귀적으로 호출될 수 있음을 주의해야 하며, `info level`을 통해 호출 계층(stack level)을 확인하고, `self target`으로 실제 호출된 메서드명을 알 수 있습니다.
필터 메서드의 추가 사항은 다음과 같습니다.
- 여러 개의 필터를 지정할 수 있으며, 메서드 체인처럼 연결됩니다.
- 모든 메서드 호출에 대해 필터가 실행되므로, 일반적으로 필터 메서드는 가변 인자(`args`)로 정의합니다.
- 필터 메서드는 객체에 정의할 수도 있고, 클래스에 정의하면 해당 클래스의 모든 객체에 영향을 줍니다.
- 예시의 Log 메서드는 대문자로 시작하므로, 외부에서 직접 호출되지 않습니다. 필터 메서드가 반드시 private일 필요는 없습니다.
- 필터 메서드는 일반적으로 `next`를 통해 원래 메서드를 호출하지만, 호출하지 않아도 되고, 결과를 변형하거나 원하는 값을 반환할 수도 있습니다.
- 생성자, 소멸자, unknown 메서드 호출에는 필터가 적용되지 않습니다.
7.1. 필터 클래스 정의하기
필터 선언은 필터 메서드가 정의된 클래스와 반드시 동일할 필요가 없습니다. 즉, 필터 기능을 별도의 "필터 클래스"로 만들어 두고,
필요한 때에 이를 "클라이언트" 클래스나 객체에 믹스인하고, 적절하게 필터를 설치/제거할 수 있습니다.
아래는 필터 클래스 활용 예입니다. 먼저 이전에 객체에 직접 정의했던 Log 메서드와 필터를 제거합니다.
% oo::objdefine smith_account {
filter -clear 1 ; 기존 필터 모두 삭제
deletemethod Log ; Log 메서드도 삭제
}
참고로 '-clear` 옵션은 현재 설정된 모든 필터를 제거합니다.
이제 로깅 기능을 클래스로 정의합니다.
% oo::class create Logger {
method Log args {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
return [next {*}$args]
}
}
특정 계좌(smith_account)에만 로깅을 적용하고 싶으므로, 해당 객체에 Logger를 믹스인하고 Log를 필터로 지정합니다.
% oo::objdefine smith_account {
mixin Logger
filter Log
}
% smith_account withdraw 500
Log(1): 2-71828182 ::Account withdraw: 500
Log(2): 2-71828182 ::Account UpdateBalance: -500
999400
이렇게 하면 이전과 동일하게 모든 행동이 로그로 기록됩니다. 하지만 이번엔 로깅을 클래스로 추상화했기 때문에, 추가적인 동작(필터, 로깅 등)을 여러 클래스나 객체에 쉽게 적용할 수 있습니다.
7.2. 필터는 언제 사용할까
필터는 오버라이딩 후 체이닝(chaining)으로도 대체할 수 있고, 반대로 오버라이드(예: 계좌 동결 기능)도 필터로 대체할 수 있습니다. 하지만 일반적으로 어느 방식을 써야 할지는 명확한 경우가 많습니다. 다음은 필터 사용에 대한 일반적인 규칙입니다:
여러 메서드에 후킹(hook)이 필요하다면
개별 메서드를 오버라이드하는 것보다 필터 메서드를 사용하는 것이 훨씬 쉽습니다.
필요한 경우, 필터 내부에서 `self target`을 사용하여 특정 메서드만 선택적으로 후킹할 수도 있습니다.
메서드가 객체의 핵심 기능이 아니라 '관찰자(observer)' 역할을 한다면
필터 메서드가 더 적합합니다.
예를 들어, 활동 로깅이나 모니터링처럼 객체의 본래 동작을 감시하는 경우입니다.
필터 메서드는 항상 메서드 체인에서 앞쪽(최우선)에 위치한다는 점도 고려해야 합니다.
따라서, 호출 우선순위가 중요할 때 필터가 적합할 수 있습니다.
8. 메서드 체인(Method Chains)
지금까지 살펴본 바와 같이, 객체에서 메서드가 호출될 때 실제로 실행되는 코드는 여러 위치에서 올 수 있습니다. 즉, 메서드 코드는 다음 중 하나에서 나올 수 있습니다.
- 객체 자체에 정의된 메서드
- 객체의 클래스나 슈퍼클래스(상속 계통)
- 믹스인(mixin) 클래스
- 포워딩(forwarded) 메서드
- 필터(filter) 메서드
- unknown method 핸들러
TclOO는 메서드 호출 시, 특정 순서로 탐색해서 실행할 코드를 찾습니다. 찾은 첫 번째 코드를 실행하며, 이 코드 내에서는 `next` 명령어을 통해 체인(chain)을 따라 다음 코드를 호출할 수 있습니다. 이렇게 하면 여러 계층의 메서드 코드가 순차적으로 실행될 수 있습니다.
8.1. 메서드 체인 순서
정확한 검색 순서와 이 메서드 체인의 구성에 대해서는 'next' 명령어의 레퍼런스 문서를 참고하세요. 여기서는 다중 상속, 믹스인, 필터, 객체별 메서드가 포함된 클래스 계층 구조를 예시로 간단히 설명합니다. 실제로 메서드를 호출하지 않을 것이므로 메서드 정의는 비워둡니다.
oo::class create ClassMixin { method m {} {} }
oo::class create ObjectMixin { method m {} {} }
oo::class create Base {
mixin ClassMixin
method m {} {}
method classfilter {} {}
filter classfilter
method unknown args {}
}
oo::class create SecondBase { method m {} {} }
oo::class create Derived {
superclass Base SecondBase
method m {} {}
}
Derived create o
oo::objdefine o {
mixin ObjectMixin
method m {} {}
method objectfilter {} {}
filter objectfilter
}
Derived 클래스의 객체 o를 생성했으며, 이 객체는 두 개의 부모 클래스로부터 상속받고 있습니다. 모든 부모 클래스에서 m 메서드를 정의했습니다. 또한 클래스 수준과 객체 수준 모두 믹스인이 적용되어 있고, 클래스와 객체 모두 필터가 정의되어 있습니다.
그렇다면 m 메서드의 메서드 체인은 어떻게 될까요? 다행히도 man 페이지를 보면서 직접 계산할 필요 없이, introspection 기능인 `info object call` 명령어를 통해 확인할 수 있습니다.
% print_list [info object call o m]
filter objectfilter object method
filter classfilter ::Base method
method m ::ObjectMixin method
method m ::ClassMixin method
method m object method
method m ::Derived method
method m ::Base method
method m ::SecondBase method
출력 결과를 보면, 필터 메서드가 가장 먼저 체인에 위치함을 알 수 있습니다.
`info object call` 명령어는 특정 객체에 대해 특정 메서드를 호출할 때의 메서드 체인을 리스트로 반환합니다. 리스트의 각 요소는 4개의 항목을 가진 서브리스트입니다.
- 타입: 일반 메서드의 경우 `method`, 필터 메서드의 경우 `filter`, unknown facility를 통해 호출된 경우 `unknown`입니다.
- 메서드 이름: 출력에서 볼 수 있듯이, 실제 호출에 사용된 이름과 다를 수 있습니다.
- 메서드의 소스: 예를 들어, 메서드가 정의된 클래스 이름 등
- 구현 타입: `method` 또는 `forward`일 수 있습니다.
체인에 있는 모든 메서드가 자동으로 호출되는 것은 아닙니다. 리스트에 등장한 메서드가 실제로 호출될지 여부는 앞선 메서드가 `next` 명령어를 통해 호출을 전달하는지에 달려 있습니다.
8.2. 정의되지 않은 메서드의 메서드 체인
객체에 정의되지 않은 메서드의 경우, 메서드 체인은 어떻게 될까요? 같은 방법으로 확인할 수 있습니다.
% print_list [info object call o nosuchmethod]
filter objectfilter object method
filter classfilter ::Base method
unknown unknown ::Base method
unknown unknown ::oo::object {core method: "unknown"}
예상대로, unknown 메서드가 정의되어 있다면 해당 메서드가 호출됩니다. 주목할 점은, 모든 TclOO 객체의 조상인 루트 객체 `oo::object`에는 미리 정의된 unknown 메서드가 포함되어 있다는 것입니다.
8.3. 클래스의 메서드 체인 조회
앞서 예시에서는 객체의 메서드 체인을 보여주었습니다. 클래스에 대해서도 객체가 아닌 클래스를 대상으로 동작하는 `info class call` 명령어가 있습니다.
% print_list [info class call Derived m]
filter classfilter ::Base method
method m ::ClassMixin method
method m ::Derived method
method m ::Base method
method m ::SecondBase method
이 명령어를 통해 클래스 `Derived`의 m 메서드에 대한 메서드 체인을 확인할 수 있습니다.
8.4. 메서드 컨텍스트 내에서 메서드 체인 검사
메서드 컨텍스트 내부에서는 `self call` 명령어가 현재 객체에 대해 `info object call`과 거의 동일한 정보를 반환합니다.
추가로, 메서드 컨텍스트에서 `self call`을 사용하면 현재 메서드가 메서드 체인에서 어디에 위치하는지 찾을 수 있습니다. 이 명령어는 두 개의 요소로 이루어진 쌍(pair)을 반환합니다. 첫 번째 요소는 `info class call` 명령어가 반환하는 메서드 체인 리스트와 동일하고, 두 번째 요소는 그 리스트에서 현재 메서드의 인덱스입니다.
예제를 보면 더 명확해집니다.
% catch {Base destroy}
0
% oo::class create Base {
constructor {} {puts [self call]}
method m {} {puts [self call]}
}
::Base
% oo::class create Derived {
superclass Base
constructor {} {puts [self call]; next}
method m {} {
puts [self call]; next
}
}
::Derived
% Derived create o
{{method <constructor> ::Derived method} {method <constructor> ::Base method}} 0
{{method <constructor> ::Derived method} {method <constructor> ::Base method}} 1
::o
% o m
{{method m ::Derived method} {method m ::Base method}} 0
{{method m ::Derived method} {method m ::Base method}} 1
1 Clean up any previous definitions
생성자(constructor)에 대해서는 특별한 형태 `<constructor>`가 사용됩니다. 소멸자(destructor)도 마찬가지로 `<destructor>` 형태를 가집니다.
참고로 생성자(constructor)와 소멸자(destructor)의 메서드 체인은 `self call` 명령어를 통해서만 확인할 수 있으며, `info class call` 명령어로는 조회할 수 없습니다.
8.5. 메서드 체인에서 다음 메서드 찾기
때때로 메서드 구현시 자신이 메서드 체인에서 마지막인지, 아니면 다음에 어떤 메서드가 호출될지를 알고 싶을 수 있습니다. 이러한 정보는 메서드 컨텍스트 내에서 `self next` 명령어를 통해 얻을 수 있습니다.
아래는 바로 전에 정의한 Derived 클래스의 m 메서드를 수정한 예시입니다.
% oo::define Derived {
method m {} { puts "Next method in chain is [self next]" }
}
% o m
→ Next method in chain is ::Base m
위 예시에서 볼 수 있듯, `self next`는 다음 메서드 체인에서 호출될 클래스(또는 객체)와 메서드 이름(생성자는 `<constructor>`, 소멸자는 `<destructor>`)을 쌍(pair)으로 반환합니다. 만약 현재 메서드가 체인에서 마지막이라면, 빈 리스트가 반환됩니다.
여기서 중요한 점은, 다음에 호출될 메서드가 출력되었지만 실제로 호출되지는 않는다는 것입니다. 이는 Derived의 m 메서드가 더 이상 next를 호출하지 않기 때문입니다.
`self next`는 다음과 같은 중요한 문제를 해결해줍니다. 예를 들어, 어떤 기능을 믹스인 클래스로 패키징하려고 한다고 가정합시다. 기능 자체는 중요하지 않지만, 일반적으로 사용할 수 있고 어떤 클래스에도 믹스인할 수 있도록 설계한 것입니다(예: 로깅 또는 트레이싱).
% oo::class create GeneralPurposeMixin {
constructor args {
puts "Initializing GeneralPurposeMixin";
next {*}$args
}
}
::GeneralPurposeMixin
% oo::class create MixerA {
mixin GeneralPurposeMixin
constructor {} {puts "Initializing MixerA"}
}
::MixerA
% MixerA create mixa
Initializing GeneralPurposeMixin
Initializing MixerA
::mixa
여기까지는 정상적으로 동작합니다. 이제 동일한 믹스인을 사용하는 다른 클래스를 정의해보겠습니다.
% oo::class create MixerB {mixin GeneralPurposeMixin}
::MixerB
% MixerB create mixb
Initializing GeneralPurposeMixin
no next constructor implementation
문제가 발생했습니다. 에러 메시지에서 알 수 있듯이, GeneralPurposeMixin 클래스는 next를 호출해서 믹스인하는 클래스의 생성자를 실행하려고 합니다. 하지만 MixerB 클래스는 생성자가 없으므로 호출할 다음 메서드(생성자)가 존재하지 않아 에러가 발생합니다.
이럴 때 `self next`가 도움이 됩니다. GeneralPurposeMixin의 생성자를 다음과 같이 수정해보겠습니다.
% oo::define GeneralPurposeMixin {
constructor args {
puts "Initialize GeneralPurposeMixin";
if {[llength [self next]]} {
next {*}$args
}
}
}
% MixerB create mixb
Initialize GeneralPurposeMixin
::mixb
이제 모든 것이 잘 동작합니다. 실제로 호출할 다음 메서드가 있을 때만 next를 호출하도록 했기 때문입니다.
8.6. 메서드 호출 순서 제어
앞서 살펴본 예시들에서, 메서드는 `next` 명령어를 사용하여 메서드 체인에서 자신의 다음 메서드를 호출할 수 있었습니다. 하지만 다중 상속, 믹스인, 필터 등이 섞인 상황에서는 상속받은 메서드들의 호출 순서를 직접 제어해야 할 때가 있습니다. 이럴 때는 메서드 체인 순서대로만 동작하는 `next` 명령어로는 제어가 어렵습니다.
이런 경우에 사용할 수 있는 명령어가 바로 `nextto`입니다. `nextto`는 `next`와 비슷하지만, 첫 번째 인자로 호출할 다음 메서드를 구현하는 클래스 이름을 지정할 수 있습니다.
nextto CLASSNAME ?args?
여기서 CLASSNAME은 메서드 체인에서 뒤쪽에 등장하는 메서드를 구현한 클래스의 이름이어야 합니다.
언제 사용할까요? 예를 들어, 두 개의 부모 클래스가 있고 각각의 생성자가 서로 다른 인자를 받는 상황을 생각해보세요. 이때 next를 사용하면 부모 클래스의 생성자 인자가 다르기 때문에 호출이 불가능합니다.
이럴 때 `nextto`를 사용하면 각 부모 클래스의 생성자를 원하는 인자로 직접 호출할 수 있습니다. 아래 예시를 참고하세요.
oo::class create ClassWithOneArg {
constructor {onearg} {puts "Constructing [self class] with $onearg"}
}
oo::class create ClassWithNoArgs {
constructor {} {puts "Constructing [self class]"}
}
oo::class create DemoNextto {
superclass ClassWithNoArgs ClassWithOneArg
constructor {onearg} {
nextto ClassWithOneArg $onearg
nextto ClassWithNoArgs
puts "[self class] successfully constructed"
}
}
이제 아래와 같이 호출해도 충돌 없이 잘 동작합니다.
% [DemoNextto new "a single argument"] destroy
Constructing ::ClassWithOneArg with a single argument
Constructing ::ClassWithNoArgs
::DemoNextto successfully constructed
9. 분석(Introspection)
Tcl의 다른 부분들과 마찬가지로, TclOO는 깊고 폭넓은 분석(introspection) 기능을 제공합니다. 이 기능은 동적 객체 시스템 프로그래밍, 런타임 디버깅 및 트레이싱, 계층형 객체지향 시스템 구축에 매우 유용합니다.
클래스와 객체의 분석 작업은 어떤 컨텍스트에서든 info class 및 info object 앙상블(ensemble) 명령어를 통해 수행할 수 있습니다. 이 명령어들은 클래스나 객체에 대한 다양한 정보를 반환하는 서브커맨드(subcommand)를 가지고 있습니다. 또한, self 명령어는 객체의 메서드 컨텍스트 내부에서 해당 객체에 대한 분석 작업에 사용할 수 있습니다.
9.1. 객체 열거하기
`info class instances` 명령어는 지정한 클래스에 속한 객체들의 리스트를 반환합니다.
% info class instances Account
::oo::Obj70 ::smith_account
% info class instances SavingsAccount
::savings
위 예시에서 볼 수 있듯이, 이 명령어는 지정한 클래스에 직접 속한 객체만 반환하며, 상속을 통해 속한 객체는 반환하지 않습니다.
패턴 인자를 추가로 지정할 수도 있는데, 이 경우 `string match` 명령어 규칙에 따라 이름이 패턴과 일치하는 객체만 반환됩니다. 이는 예를 들어, 네임스페이스를 사용해 객체를 구분할 때 유용할 수 있습니다.
% info class instances Account ::oo::*
::oo::Obj70
9.2. 클래스 열거하기
클래스도 TclOO에서는 객체이므로, 객체를 열거할 때 사용한 명령어를 그대로 클래스 열거에도 사용할 수 있습니다.
% info class instances oo::class
::oo::object ::oo::class ::oo::Slot ::Account ::SavingsAccount ::CheckingAcco...
여기서는 모든 클래스(또는 클래스 객체)가 소속된 클래스인 `oo::class`를 인자로 넘깁니다. 반환된 리스트에는 다음과 같은 흥미로운 요소들이 있습니다:
- `oo::class` 자체가 반환됩니다. 모든 클래스 객체가 속한 클래스인 동시에, 자기 자신도 클래스이므로 자기 자신의 인스턴스입니다.
- `oo::object`도 반환됩니다. 이는 객체 계층의 루트 클래스이자, 동시에 클래스이므로 역시 `oo::class`의 인스턴스가 됩니다.
이처럼 `oo::object`와 `oo::class` 사이의 순환적이고 자기참조적인 관계는 다소 이상하게 보일 수 있지만, TclOO에서 모든 프로그래밍 구조가 일관되게 동작하도록 해줍니다. 이는 많은 객체지향 시스템에서 공통적으로 나타나는 특징이기도 합니다.
앞서와 마찬가지로, 반환되는 클래스의 이름이 특정 패턴과 일치하도록 제한할 수도 있습니다.
% info class instances oo::class *Mixin
::ClassMixin ::ObjectMixin ::GeneralPurposeMixin
9.3. 클래스 관계 분석(Introspecting class relationships)
`info class superclasses` 명령어는 클래스의 직접적인 슈퍼클래스(부모 클래스)들을 반환합니다.
% info class superclasses CashManagementAccount
::CheckingAccount ::BrokerageAccount
% info class superclasses ::oo::class
::oo::object
여기서 `oo::object`가 `oo::class`의 슈퍼클래스임을 알 수 있습니다.
반대로, `info class subclasses` 명령어는 지정한 클래스를 직접 상속하는 서브클래스(자식 클래스)들을 반환합니다.
% info class subclasses Account
::SavingsAccount ::CheckingAccount ::BrokerageAccount
또한, 믹스인(mixin) 목록을 반환하는 명령어도 있습니다. `info class mixin`을 사용하면 됩니다.
% info class mixin CheckingAccount
::EFT
9.4. 클래스 소속 확인하기
객체가 속한 클래스를 확인하려면 `info object class` 명령어를 사용합니다.
% info object class savings
::SavingsAccount
이 명령어를 이용해 객체가 특정 클래스에 속하는지(상속도 포함하여) 확인할 수도 있습니다.
% info object class savings SavingsAccount
1
% info object class savings Account
1
% info object class savings CheckingAccount
0
객체에 믹스인된 클래스 목록을 확인하려면 `info object mixins` 명령어를 사용합니다. 이는 `info class mixins`와 유사합니다.
% info object mixins savings
::EFT
객체의 메서드 컨텍스트 내에서는 `self class` 명령어를 통해 현재 실행 중인 메서드를 정의한 클래스를 반환받을 수 있습니다. 이는 객체가 속한 클래스와 다를 수 있습니다. 아래 예시를 참고하세요.
% catch {Base destroy}
0
% oo::class create Base {
method m {} {
puts "Object class: [info object class [self object]]"
puts "Method class: [self class]"
}
}
::Base
% oo::class create Derived { superclass Base }
::Derived
% Derived create o
::o
% o m
Object class: ::Derived
Method class: ::Base
참고로 `self class` 명령어는 객체에 직접 정의된 메서드(즉, 클래스가 아닌 특정 객체에만 존재하는 메서드) 내에서 호출할 경우 실패합니다. 왜냐하면 그 경우에는 해당 메서드와 연결된 클래스가 존재하지 않기 때문입니다.
9.5. 메서드 열거하기
클래스나 객체에서 구현된 메서드 목록은 각각 `info class methods`와 `info object methods` 명령어로 조회할 수 있습니다. 옵션을 사용하여 상속된 메서드와 private 메서드의 포함 여부를 제어할 수 있습니다.
# CheckingAccount에서 직접 정의되고 export된 메서드 목록
% info class methods CheckingAccount
cash_check
# CheckingAccount에서 직접 정의된 export 및 private 메서드 목록
% info class methods CheckingAccount -private
cash_check
# CheckingAccount, 그 상위 클래스, 믹스인에서 정의되고 export된 모든 메서드 목록
% info class methods CheckingAccount -all
balance cash_check deposit destroy transfer_in transfer_out withdraw
# CheckingAccount, 그 상위 클래스, 믹스인에서 정의된 모든 export 및 private 메서드 목록
% info class methods CheckingAccount -all -private
<cloned> UpdateBalance balance cash_check deposit destroy eval transfer_in tr...
# 객체 smith_account에서 정의된 export 및 non-export 메서드 목록
% info object methods smith_account -private
필터로 지정된 메서드 목록은 info class filters 또는 info object filters 명령어로 조회할 수 있습니다.
% info object filters smith_account
Log
9.6. 메서드 정의 조회하기
특정 메서드의 정의를 조회하려면 `info class definition` 명령어를 사용합니다. 이 명령어는 메서드의 인자 목록과 본문(body)을 쌍(pair)으로 반환합니다.
% info class definition Account UpdateBalance
change {
set Balance [+ $Balance $change]
return $Balance
}
여기서 주의할 점은, 조회하려는 메서드는 반드시 지정한 클래스에 직접 정의되어 있어야 하며, 상위 클래스나 믹스인에서 정의된 메서드는 조회되지 않습니다.
생성자와 소멸자 정의는 각각 `info class constructor`와 `info class destructor` 명령어를 통해 조회합니다.
% info class constructor Account
account_no {
puts "Reading account data for $account_no from database"
set AccountNumber $account_no
set Balance 1000000
}
9.7. 메서드 체인 조회하기
`info class call` 명령어는 특정 메서드에 대한 메서드 체인을 조회합니다. 메서드 컨텍스트 내에서는 `self call` 명령어가 유사한 정보를 반환하며, `self next`는 체인에서 다음 메서드 구현을 식별합니다.
이들 명령어와 관련된 내용은 앞서 '8. 메서드 체인(Method Chains)' 에서 자세히 설명했습니다.
9.8. 필터 컨텍스트 분석(Introspecting filter contexts)
메서드가 필터로 실행될 때, 실제로 호출되는 대상 메서드(타겟 메서드)를 아는 것이 유용할 때가 많습니다. 이 정보는 `self target` 명령어로 얻을 수 있으며, 반드시 필터 컨텍스트 내에서만 사용할 수 있습니다. 반환값은 `[선언자, 대상 메서드 이름]` 쌍(pair)입니다.
예를 들어, 이전과 달리 모든 트랜잭션을 로그하지 않고, 인출(withdraw)만 로그하고 싶다면 아래처럼 Log 명령어를 정의할 수 있습니다.
oo::define Logger {
method Log args {
if {[lindex [self target] 1] eq "withdraw"} {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
}
return [next {*}$args]
}
}
이제 인출만 로그됩니다.
% smith_account deposit 100
999500
% smith_account withdraw 100
Log(1): 2-71828182 ::Account withdraw: 100
999400
또한, 필터 메서드 내부에서는 필터 자체에 대한 정보도 얻을 수 있는데, 이는 `self filter` 명령어로 확인할 수 있습니다.
아래와 같이 Log 필터를 다시 정의해보겠습니다.
% oo::define Logger {
method Log args {
puts [self filter]
return [next {*}$args]
}
}
% smith_account withdraw 1000
::smith_account object Log
::smith_account object Log
998400
위에서 볼 수 있듯이, `self filter` 명령어는 세 개의 항목이 들어 있는 리스트를 반환합니다:
- 필터가 선언된 클래스 또는 객체의 이름 (필터 메서드가 정의된 클래스와 다를 수 있음)
- `object` 또는 `class` (필터가 객체에 선언되었는지, 클래스에 선언되었는지)
- 필터의 이름
참고로 위 예시에서 두 개의 출력 라인이 나타나는 이유는, 필터가 각 메서드 호출 시마다 실행되기 때문입니다. 즉, Log 메서드는 withdraw 메서드가 호출되기 전에 한 번, 그리고 withdraw 메서드가 내부적으로 UpdateBalance 메서드를 호출할 때 또 한 번 실행됩니다.
9.9. 객체 식별(Object identity)
특정 상황에서는 객체가 자신의 메서드 컨텍스트에서 자신의 정체성을 알아야 할 필요가 있습니다.
객체는 두 가지 방식으로 식별할 수 있습니다:
- 객체 메서드를 호출하는 데 사용된 명령 이름(command name)
- 객체 상태가 저장되는 고유 네임스페이스(namespace)
첫 번째는 `self object` 명령어(혹은 간단히 `self`)로 반환됩니다. 객체의 명령 이름이 필요한 경우는 예를 들어,
- 객체 메서드를 콜백에 넘겨야 할 때
- 객체가 "즉석에서" 재정의되는 경우, 그 이름을 `oo::objdefine`에 넘겨야 할 때
자세한 예시는 "5.1 객체별 메서드(Object-specific methods)" 섹션을 참고하세요.
두 번째, 객체와 연관된 고유 네임스페이스는 메서드 컨텍스트 내에서는 `self namespace` 명령어로, 그 외에서는 `info object namespace` 명령어로 얻을 수 있습니다.
% oo::define Account {method get_ns {} {return [self namespace]}}
% savings get_ns
::oo::Obj76
% set acct [Account new 0-0000000]
Reading account data for 0-0000000 from database
::oo::Obj106
% $acct get_ns
::oo::Obj106
% info object namespace $acct
::oo::Obj106
객체를 `new`로 생성하면 네임스페이스가 객체 명령 이름과 일치함을 볼 수 있습니다. 하지만 이는 구현상의 부수 효과일 뿐, 이에 의존해서는 안 됩니다. 실제로 Tcl 명령과 마찬가지로 객체 명령도 이름을 바꿀 수 있습니다.
% rename $acct temp_account
% temp_account get_ns
::oo::Obj106
위에서 볼 수 있듯, 객체 명령 이름과 네임스페이스가 더 이상 일치하지 않습니다. 명령 이름을 바꿔도 네임스페이스는 바뀌지 않습니다.
9.10. 데이터 멤버 열거하기
`info class variables` 명령어는 클래스 정의 내에서 `variable` 문으로 선언된 변수 목록을 반환합니다. 이 변수들은 클래스의 메서드에서 자동으로 접근할 수 있게 됩니다.
% info class variables SavingsAccount
MaxPerMonthWithdrawals WithdrawalsThisMonth
여기서 반환된 변수들은 지정된 클래스에서 `variable` 문으로 직접 선언된 변수들만 포함합니다. 예를 들어, `Balance` 변수는 상위 클래스인 `Account`에서 정의되었기 때문에 위 명령에서 나타나지 않습니다.
객체 단위로 변수를 열거하고 싶다면 두 가지 명령어가 있습니다:
- `info object variables`: 객체 정의에서 `variable`로 선언된 변수 목록을 반환합니다. 아직 초기화되지 않았다면 존재하지 않을 수도 있습니다.
- `info object vars`: 객체의 네임스페이스에서 현재 존재하는 모든 변수 목록을 반환합니다. 정의 방식은 고려하지 않습니다.
아래 두 명령의 결과 차이점을 살펴보세요.
% info object variables smith_account
% info object vars smith_account
Balance AccountNumber
첫 번째 명령은 해당 객체에서 `variable`로 선언된 변수 목록(여기서는 없음)을 반환합니다. 두 번째 명령은 객체의 네임스페이스에 현재 존재하는 변수들을 반환합니다. 변수들이 클래스에서 선언된 것인지 여부는 중요하지 않습니다.
10. References
DKF2005
TIP #257: Object Orientation for Tcl, Fellows et al, http://tip.tcl.tk/257. The Tcl Implementation Proposal describing TclOO.
WOODS2012
Lifecycle Object Generators, Woods, 19th Annual Tcl Developer’s Conference, Nov 12-14, 2012. Describes a number of useful OO patterns built on top of TclOO.