객체 지향 프로그래밍이란 무엇인가?
이제 대략 10년 전이라면 들어 보지도 못했을 고급 주제에 이르렀습니다. 오늘날 '객체 지향 프로그래밍은 표준이 되었습니다. 자바와 파이썬 같은 언어에는 그 개념이 엄청나게 내장되어서 어느 곳에서든 객체를 만나지 않고서는 별로 할 수 있는 일이 없을 정도입니다. 그래서 도대체 객체 지향 프로그래밍이란 무엇인가?
이 책들은 목록 아래로 내려갈 수록 학문적으로 정밀해지고 내용이 더 깊어집니다. 일반 프로그래머의 목적을 위해서라면 첫번째 책이 적당합니다. 좀 프로그래밍에 초점을 두고 있다면 티모시 버드(Timothy Budd)가 지은 Object Oriented Programming (2판)을 읽어 보세요. 이 책은 여러 언어로 객체 지향 프로그래밍 테크닉을 보여줍니다. 객체 지향의 이론과 원리를 디자인 수준뿐만 아니라 코드 수준에서 전범위에 걸쳐 다루는 다른 책들에 비하여 훨씬 더 강력하게 프로그램을 작성하는데 주안점을 두고 있습니다. 마지막으로 OO를 주제로 모든 정보를 얻으려면 다음 http://www.cetus-links.org 웹 사이트에 가보세요:
지금 당장 시간도 없고 이 모든 책과 링크를 볼 생각도 없다면 그 개념을 짧게 설명해 드리겠습니다. (고지: 어떤 사람들은 OO가 어려워서 다른 사람들의 '깨닫음'을 즉시 파악할 수 없다고 생각합니다. 이런 범주에 있는 분이라고 할지라도 걱정하지 마세요. 실제로 '빛을 보지 않아도' 객체를 사용할 수 있습니다.)
마지막 요점 하나: OO 언어가 아니라도 코딩 관례를 통하여 객체 지향 디자인을 구현하는 것이 가능합니다. 그러나 보통 추천할 만한 전략이 아니라 최후로 의지해야 할 옵션입니다. 문제가 OO 테크닉에 잘 맞는다면 OO 언어를 쓰는 것이 제일 좋습니다. 파이썬과 VBScript 그리고 JavaScript를 포함하여 대부분의 현대 언어는 OOP를 아주 잘 지원합니다. 말씀 드린 바와 같이 모든 예제에 파이썬을 사용하겠습니다. 그리고 기본 개념만 VBScript와 JavaScript로 약간 설명을 덧붙여서 보여드리겠습니다.
데이터와 함수 - 함께
객체는 데이터와 그 데이터를 연산하는 함수 집단입니다. 객체 안에 데이터와 함수가 함께 묶여 있으므로 프로그램 안에서 객체를 이리저리 건넬 수 있습니다. 그러면 자동으로 데이터 속성(attributes)뿐만 아니라 그 연산(operations)에도 접근할 수 있습니다. 이렇게 데이터와 함수를 조합하는 것이 바로 객체 지향 프로그래밍의 정수이며 캡슐화(encapsulation)라고 부릅니다. (어떤 프로그래밍 언어는 데이터를 그 객체의 사용자에게 보여주지 않습니다. 그리하여 객체의 메쏘드를 통하여 데이터에 접근하도록 요구합니다. 이 테크닉은 그럴 듯하게 데이터 은닉(data hiding)이라고 부릅니다. 그렇지만, 어떤 상황에서는 데이터 은닉과 캡슐화는 서로 교환해 사용해도 됩니다.)
캡슐화의 한 예로, 문자열 객체는 문자열을 저장하기도 하고 그 연속열을 연산하는 메쏘드(methods)도 제공합니다 - 검색, 격변환, 길이 계산 등등.
객체는 메시지 건네기(message passing) 은유를 사용합니다. 한 객체는 메시지를 또다른 객체에 보내고 수신 객체는 자신의 연산중 하나, 즉 메쏘드(method)를 실행함으로써 응답합니다. 그래서 메쏘드는 상응하는 메시지를 받으면 그를 소유한 객체에 의하여 요청(invoked)됩니다. 이를 표현하는데 사용되는 다양한 표기법이 있습니다. 그러나 가장 일반적인 방법은 모듈에 있는 항목을 접근할 때의 표기법을 흉내내는 것입니다 - 점 표기법이 그것입니다. 그리하여, 가상의 위젯 클래스에 대하여:
w = Widget() # 위젯 실체를 새로 만든다.
w.paint() # 'paint'라는 메시지를 그 실체에 전송한다.
이렇게 하면 위젯 객체의 paint 메쏘드가 요청됩니다.
클래스 정의하기
데이터 유형이 다양하듯이 객체도 유형이 다양할 수 있습니다. 개성이 동일한 이런 객체 집단은 집단적으로 클래스( class)라고 부릅니다. 클래스를 정의하고 그 클래스의 실체(instances)를 만들 수 있습니다. 이것이 진짜 객체입니다. 이런 객체를 가리키는 참조점들을 프로그램에서 변수에 저장할 수 있습니다.
더 잘 설명할 수 있도록 구체적인 예제를 살펴봅시다. 문자열을 - 메시지 텍스트를 - 담고 있는 메시지 클래스와 그 메시지를 인쇄할 메쏘드를 만들겠습니다.
class Message:
def __init__(self, aString):
self.text = aString
def printIt(self):
print self.text
고지 1: 이 클래스의 메쏘드 중 __init__ 이 있습니다. 이는 구성자(constructor)라고 부르는 특수 메쏘드입니다. 이름이 그런 이유는 새로운 실체가 생성되거나 구성될 때 호출되기 때문입니다. 어떤 변수든지 이 메쏘드 안에 할당되면 (그러므로 파이썬에서 생성되면) 그 새로운 실체에만 유일하게 존재할 것입니다. 파이썬에는 이와 같은 특수 메쏘드가 많습니다. 거의 모두 이름의 형태가 __xxx__ 인 것으로 구별됩니다. 구성자가 호출되는 정확한 타이밍은 언어마다 다릅니다. 파이썬에서 init은 실체가 실제로 메모리에 생성된 후 호출됩니다. 다른 언어에서 구성자는 실제로 실체 그 자체를 돌려줍니다. 그 차이는 보통 그에 관하여 신경쓰지 않아도 될 정도로 매우 작습니다.
고지 2: 정의된 메쏘드 모두 첫 매개변수로 self가 있습니다. 그 이름은 관례이지만 실체 자신을 나타냅니다. 곧 보시겠지만 이 매개변수는 프로그래머가 아니라 실행시간에 파이썬이 채워줍니다. 그리하여 클래스의 실체에 아무 인자 없이 printIt가 호출됩니다: m.printIt().
고지 3: 대문자 'M'으로 Message 클래스를 호출했습니다. 이는 순전히 관례이지만 상당히 널리 사용됩니다. 단지 파이썬뿐만 아니라 다른 OO 언어들도 마찬가지입니다. 관련 관례에 의하면 메쏘드 이름은 소문자로 시작되고 다음에 따르는 단어들은 대문자로 시작해야 합니다. 그리하여 "calculate current balance"라는 메쏘드는 다음과 같이 명명됩니다: calculateCurrentBalance.
잠깐만이라도 '미가공 재료' 섹션에 되돌아가 다시 '사용자 정의 유형'을 보고 싶으실 것입니다. 이제 파이썬 주소록 예제가 더 잘 이해되리라 믿습니다. 본질적으로 파이썬에 유일한 사용자-정의 유형은 클래스입니다. 속성은 있으나 메쏘드는 없는 클래스는 ( __init__ 을 제외하고) 그 효과상 다른 언어에서의 record 또는 struct라는 구조와 동등합니다.
클래스 사용하기
클래스를 정의하였으므로 Message 클래스의 실체를 만들고 조작할 수 있습니다:
m1 = Message("안녕 세계여")
m2 = Message("잘 가, 짧지만 즐거웠어")
note = [m1, m2] # 객체들을 리스트에 넣는다.
for msg in note:
msg.printIt() # 각 메시지를 순서대로 인쇄한다.
그래서 본질적으로 클래스를 마치 표준 파이썬 데이터 유형처럼 취급합니다. 무엇보다 이것이 연습의 목적이었습니다!
"self"란 무엇인가?
아닙니다. 철학적 논쟁이 아닙니다. 새내기 파이썬 OOP 프로그래머가 자주 제기하는 질문입니다. 파이썬에서 클래스의 메쏘드 정의는 self라고 부르는 매개변수로 시작합니다. 실제 이름 self는 그저 관례이지만, 많은 프로그래밍 관례처럼 일관성은 좋은 것입니다. 그러므로 지키는 것이 좋습니다! (나중에 보시겠지만 JavaScript에도 비슷한 개념이 있습니다. 대신에 this라는 이름을 사용합니다.)
그래서 도대체 self란 무엇인가? 왜 그것이 필요한가?
기본적으로 self는 그냥 현재 실체를 가리키는 참조점입니다. 클래스의 실체를 만들면 그 실체는 (구성자가 생성한대로) 자신만의 데이터를 보유하지만 메쏘드의 데이터는 보유하지 않습니다. 그리하여 실체에 메시지를 전송할 때 그리고 그에 상응하는 메쏘드를 호출할 때, 그 클래스의 내부 참조점을 통하여 호출합니다. 자신을 가리키는 참조점(self!)을 메쏘드에게 건넵니다. 그래서 클래스 코드는 어느 실체를 사용할지 압니다.
상대적으로 친숙한 에제를 살펴봅시다. 버튼 객체가 많이 있는 GUI 어플리케이션을 생각해 봅시다. 사용자가 버튼을 누를 때 버튼 눌림에 연관된 메쏘드가 활성화됩니다 - 그러나 어떻게 버튼 메쏘드가 어느 버튼이 눌렸는지 아는가? 그 대답은 self 값을 참조하는 것입니다. 이 값은 눌린 버튼 실체를 가리키는 참조점이 됩니다. 이것은 잠시후에 GUI 주제에 이르면 살펴보겠습니다.
그래서 메시지가 객체에 전송되면 어떤 일이 일어나는가? 다음과 같이 작동합니다:
· 클라이언트 코드는 실체를 호출한다 (메시지를 OOP 언어로 전송한다).
· 실체는 클래스 메쏘드를 호출한다. 자신을 가리키는 참조점(self)을 건넨다.
· 그러면 클래스 메쏘드는 건네어진 참조점을 사용하여 그 수신 객체의 실체 데이터를 손에 넣는다.
다음 코드에서 이것이 실제로 작동하는 것을 볼 수 있습니다. 클래스 메쏘드를 명시적으로 호출할 수 있는 것을 주목하세요. 바로 앞 줄에서 호출한 것처럼 말입니다:
>>> class C:
... def __init__(self, val)
self.val = val
... def f(self)
print "안녕, 내 값은", self.val, "이야"
...
>>> # 두 실체를 만든다.
>>> a = C(27)
>>> b = C(42)
>>> # 먼저 메시지를 인스턴스들에게 보내려고 시도한다.
>>> a.f()
안녕, 내 값은 27 이야
>>> b.f()
안녕, 내 값은 42 이야
>>> # 이제 클래스를 통하여 명시적으로 메쏘드를 호출한다.
>>> C.f(a)
안녕, 내 값은 27 이야
그래서 실체(인스턴스)를 통하여 또는 명시적으로 클래스를 통하여 메쏘드를 호출할 수 있습니다. 실체(인스턴스)를 통하는 경우 파이썬이 우리를 대신하여 self 매개변수를 채워줍니다.
이제 궁금할 것입니다. 파이썬이 실체와 그의 클래스 사이에 보이지 않는 참조점을 제공할 수 있다면 왜 파이썬은 마법을 동원하여 스스로 self를 채우지 못하는가? 그 대답은 귀도 반 로섬(Guido van Rossum)이 이렇게 설계했기 때문입니다! 많은 OOP 언어는 실제로 self 매개변수를 감추지만, 파이썬의 지침중 하나는 "명시적인 것이 묵시적인 것보다 낫다"는 것입니다. 곧 익숙해 질 것이고 잠시 지나면 그렇게 하지 않는 것이 오히려 이상하게 보입니다.
같은 것, 다른 것
지금까지 자신만의 유형(클래스)을 정의하는 법과 그 실체를 만드는 법 그리고 그 실체를 변수에 할당하는 법을 배웠습니다. 이제 메시지를 이런 객체들에 건넬 수 있습니다. 그러면 메시지는 정의해 둔 메쏘드를 촉발시킵니다. 그러나 마지막으로 이 OO라는 것에는 중요한 요소가 있습니다. 그리고 여러모로 가장 중요한 측면입니다.
클래스는 다르지만 메시지 집합이 똑 같은 객체가 두 개 있다고 생각해 봅시다. 그러나 각각 자신만의 상응하는 메쏘드를 가지고 있다면 이런 객체들을 함께 모아서 프로그램에서 동일하게 취급할 수 있습니다. 그 객체들은 다르게 행동합니다. 같은 입력 메시지에 대하여 다르게 행위하는 이 능력을 다형성(polymorphism)이라고 부릅니다.
전형적으로 다형성은 수 많은 다양한 그래픽 객체가 'paint' 메시지를 받으면 자신을 그리게 하는데 사용될 수 있습니다. 원은 삼각형과 아주 다른 도형을 그리지만 둘 다 paint 메쏘드가 있다면, 프로그래머의 관점에서 그 차이를 무시하고 그냥 그 둘을 '도형'으로 생각하면 됩니다.
예제 하나를 살펴봅시다. 도형을 그리는 대신에 그 넓이를 계산합니다:
먼저 Square 클래스와 Circle 클래스를 만듭니다:
class Square:
def __init__(self, side):
self.side = side
def calculateArea(self):
return self.side**2
class Circle:
def __init__(self, radius):
self.radius = radius
def calculateArea(self):
import math
return math.pi*(self.radius**2)
이제 (원이거나 사각형인) 도형 리스트를 만들 수 있고 그 넓이를 인쇄할 수 있습니다:
list = [Circle(5),Circle(7),Square(9),Circle(3),Square(12)]
for shape in list:
print "The area is: ", shape.calculateArea()
이 생각을 모듈과 조합하면 재사용 가능한 코드를 위한 아주 강력한 메커니즘을 얻습니다. 클래스 정의를 모듈에 두세요 - 예를 들어 'shapes.py'에 두었다면 도형을 조작하고 싶을 때 그냥 그 모듈을 반입하면 됩니다. 많은 표준 파이썬 모듈에서 정확하게 이렇게 시행해 오고 있습니다. 이 때문에 한 객체의 메쏘드에 접근하는 것은 모듈에서 함수를 사용하는 것과 아주 많이 닮았습니다.
상속
상속은 종종 다형성을 구현하는 메커니즘으로 사용됩니다. 실제로 많은 OO 언어에서 그것이 다형성을 구현하는 유일한 방법입니다. 다음과 같이 작동합니다:
클래스는 부모 (parent) 클래스 또는 수퍼(super) 클래스로부터 속성과 연산을 모두 상속(inherit)받을 수 있습니다. 이는 곧 새로운 클래스가 대부분의 관점에서 다른 클래스와 동일하다면 기존 클래스의 모든 메쏘드를 재구현할 필요가 없다는 뜻입니다. 오히려 그런 능력들을 상속 받아서 (위 사례의 calculateArea 메쏘드와 같이) 다르게 행동했으면 하고 바라는 것들을 오버라이드( override)할 수 있습니다.
역시 이를 잘 설명해 줄 예제를 하나 더 보여드리겠습니다. 은행 계정이라는 클래스 계통도를 사용하겠습니다. 은행 계정에 현금을 적립하고 잔고를 맞추며 예금을 인출할 수 있습니다. 어떤 계정은 이자를 제공하고 (이자는 예금마다 계산된다고 가정하겠습니다 - 은행 업계에 흥미로운 혁신입니다!) 어떤 계정은 인출하려면 수수료를 청구합니다.
BankAccount 클래스
어떻게 보이는지 살펴봅시다. 먼저 가장 일반적인 (즉, 추상적인(abstract)) 수준에서 은행 계정의 속성과 연산을 생각해 봅시다.
보통 연산을 먼저 고려하고 다음으로 필요한대로 속성을 제공하면서 이 연산들을 지원하는 것이 제일 좋습니다. 그래서 은행 계정에 대하여 할 수 있는 일은:
· 현금을 적립한다(Deposit)
· 현금을 인출한다(Withdraw)
· 현재 잔고를 점검한다(Check current balance)
· 기금을 다른 계정으로 이체한다(Transfer)
이런 연산을 지원하기 위하여 (이체 연산용) 은행 계정 ID 그리고 현재 잔고가 필요합니다.
그를 지원하는 클래스를 만들 수 있습니다:
class BalanceError(Exception):
value = "Sorry you only have $%6.2f in your account"
class BankAccount:
def __init__(self, initialAmount):
self.balance = initialAmount
print "Account created with balance %5.2f" % self.balance
def deposit(self, amount):
self.balance = self.balance + amount
def withdraw(self, amount):
if self.balance >= amount:
self.balance = self.balance - amount
else:
raise BalanceError, BalanceError.value % self.balance
def checkBalance(self):
return self.balance
def transfer(self, amount, account):
try:
self.withdraw(amount)
account.deposit(amount)
except BalanceError:
print BalanceError.value
고지 1: 인출하기 전에 잔고를 점검합니다. 그리고 예외를 사용하여 에러도 처리합니다. 물론 파이썬 에러 유형에 BalanceError는 없습니다. 그래서 따로 유형을 만들 필요가 있습니다 - 그냥 표준 Exception 클래스의 하위클래스에 불과하며 문자열 값을 가졌을 뿐입니다. 문자열 값(value)은 순전히 편의상 예외 클래스의 속성으로 정의됩니다. 에러를 일으킬 때마다 확실하게 표준 에러 메시지를 생성할 수 있습니다. 예외가 일어나면(raise BalanceError ) 내부의 형식화 문자열 value에 현재 그 객체의 잔고 값(balance)을 채워서 건넵니다. BalanceError에 값을 정의할 때 self를 사용하지 않은 것에 주목하세요. 그 이유는 value가 모든 실체의 공유 속성이기 때문입니다. 이 속성은 클래스 수준에서 정의되었기 때문에 클래스 변수(class variable)라고 부릅니다. 위에 보여준 BalanceError.value와 같이 클래스 이름 다음에 점을 찍어서 클래스 변수에 접근합니다. 이제, 에러가 그의 역추적을 생성하면 포맷된 에러 문자열을 인쇄하여 현재 잔고를 보여주고 끝납니다.
고지 2: transfer 메쏘드는 BankAccount의 withdraw/deposit 멤버 함수(member functions), 즉 메쏘드를 사용하여 이체합니다. 이는 OO에서 아주 흔한 일이며 self messaging이라고 부릅니다. 그 의미는 파생된 클래스(derived classes)는 자신만의 deposit/withdraw 버전을 구축할 수 있지만 transfer 메쏘드는 모든 계정 유형에 대하여 그대로 남아 있을 것이라는 뜻입니다.
InterestAccount 클래스
이제 상속을 사용하여 예금마다 이자가 (3%) 붙는 계정을 제공합니다. 표준 BankAccount 클래스와 동일하지만 deposit 메쏘드만 다릅니다. 그래서 그냥 오버라이드 하면 됩니다:
class InterestAccount(BankAccount):
def deposit(self, amount):
BankAccount.deposit(self,amount)
self.balance = self.balance * 1.03
바로 그것입니다. OOP의 힘을 보기 시작했습니다. 다른 모든 메쏘드는 (BankAccount를 새 클래스 이름 뒤에 있는 괄호 안에 넣어서) BankAccount로부터 상속받습니다. 또 주목하세요. deposit 메쏘드가 코드를 복사하는 것이 아니라, superclass의 deposit 메쏘드를 호출했습니다. 이제 BankAccount 클래스의 deposit 메쏘드를 수정하여 에러 점검을 포함시킨다면 하위클래스(sub-class)는 자동으로 그런 변화에 영향을 받습니다.
ChargingAccount 클래스
이 클래스도 역시 표준 BankAccount 클래스와 동일하며 다만 이번에는 인출할 때마다 $3를 요구하는 점만 다릅니다. InterestAccount에 대하여 그랬던 것처럼 BankAccount로부터 클래스를 상속받아 withdraw 메쏘드를 수정할 수 있습니다.
class ChargingAccount(BankAccount):
def __init__(self, initialAmount):
BankAccount.__init__(self, initialAmount)
self.fee = 3
def withdraw(self, amount):
BankAccount.withdraw(self, amount+self.fee)
고지 1: 수수료를 실체 변수(instance variable)로 저장했습니다. 그래서 나중에 필요하면 바꿀 수 있습니다. 다른 메쏘드와 똑 같이 상속받은 __init__ 메쏘드를 호출할 수 있음을 주목하세요.
고지 2: 그냥 수수료를 인출 요청에 추가하고 BankAccount 클래스의 withdraw 메쏘드를 호출하여 실제 일을 시킵니다.
고지 3: 여기에서 부작용이 초래되었습니다. 이체에도 청구가 자동으로 부과되었습니다. 그러나 그것은 원하는 바이므로 문제가 없습니다.
시스템을 테스트하기
모든 것이 잘 작동하는지 점검하려면 (파이썬 프롬프트나 따로 테스트 파일을 만들어서) 다음 코드 조작을 실행해 보세요.
from bankaccount import *
# 먼저 표준 은행계정
a = BankAccount(500)
b = BankAccount(200)
a.withdraw(100)
# a.withdraw(1000)
a.transfer(100,b)
print "A = ", a.checkBalance()
print "B = ", b.checkBalance()
# 이제 이자계정
c = InterestAccount(1000)
c.deposit(100)
print "C = ", c.checkBalance()
# 이제 지불계정
d = ChargingAccount(300)
d.deposit(200)
print "D = ", d.checkBalance()
d.withdraw(50)
print "D = ", d.checkBalance()
d.transfer(100,a)
print "A = ", a.checkBalance()
print "D = ", d.checkBalance()
# 마지막으로 지불 계정에서 이자 계정으로 이체한다.
# 지불 계정은 지불하고 이자 계정은 이자가 붙는다.
print "C = ", c.checkBalance()
print "D = ", d.checkBalance()
d.transfer(20,c)
print "C = ", c.checkBalance()
print "D = ", d.checkBalance()
이제 a.withdraw(1000) 줄에서 주석을 제거하고 예외가 작동하는지 알아보세요.
바로 그것입니다. 보이는 그대로 거의 이해되는 예이지만 어떻게 상속을 이용하면 신속하게 기본 작업틀에 강력한 특징을 새로 갖추어 확장할 수 있는지 보여줍니다.
지금까지 어떻게 단계별로 예제를 구축할 수 있는지 그리고 어떻게 테스트 프로그램을 조립하여 작동하는 것을 점검할 수 있는지 살펴보았습니다. 제시해 드린 테스트는 완전하지 않습니다. 모든 사례를 다루고 있지 않으며 포함시켜야 할 점검사항이 많이 남아 있습니다 - 예를 들어 계정이 마이너스로 생성되면 어떻게 할까...
객체 집단
한 가지 문제가 떠 오를 수 있습니다. 어떻게 수 많은 객체를 다룰 것인가. 즉, 어떻게 실행시간에 생성한 객체들을 관리할 것인가. 위와 같이 정적으로 은행 계정을 만들면 아무 문제가 없습니다:
acc1 = BankAccount(...)
acc2 = BankAccount(...)
acc3 = BankAccount(...)
etc...
그러나 실제 세계에서는 얼마나 많은 계정을 만들 필요가 있는지 미리 알지 못합니다. 이를 어떻게 처리할 것인가? 문제를 좀 더 자세하게 생각해 봅시다:
주어진 은행 계정을 소유주의 이름으로 찾을 수 있도록 해 줄 '데이터베이스'가 필요합니다. (또는 계좌 번호가 더 좋습니다 - 왜냐하면 한 사람이 여러 계정을 가질 수 있고 여러 사람이 같은 이름을 가질 수 있기 때문입니다...)
주어진 유일 키로 집단에서 무언가를 찾는 것은....음, 마치 사전처럼 보입니다! 어떻게 파이썬의 사전을 이용하여 동적으로 생성되는 객체들을 담을 수 있는지 실펴봅시다:
from bankaccount import *
import time
# 새로 함수를 만들어 유일 아이디 번호를 생산한다.
def getNextID():
ok = raw_input("Create account[y/n]? ")
if ok[0] in 'yY': # 입력이 유효한지 점검한다.
id = time.time() # 현재 시간을 ID의 기반으로 한다.
id = int(id) % 10000 # 정수를 4 자리로 줄인다.
else: id = -1 # 회돌이가 중단된다.
return id
# 계정을 몇 개 만들어서 사전에 저장한다.
accountData = {} # 새 사전이다.
while 1: # 영원히 회돌이한다.
id = getNextID()
if id == -1:
break # 회돌이를 강제로 빠져 나가게 한다.
bal = float(raw_input("Opening Balance? ")) # 문자열을 부동소수점수로 변환한다.
accountData[id] = BankAccount(bal) # id를 사용하여 사전 엔트리를 새로 만든다.
print "New account created, Number: %04d, Balance %0.2f" % (id,bal)
# 이제 계정에 접근해 보자.
for id in accountData.keys():
print "%04d\t%0.2f" % (id,accountData[id].checkBalance())
# 특정한 계정을 찾는다.
# 문자를 넣어서 강제로 예외를 일으키고 프로그램을 끝낸다.
while 1:
id = int(raw_input("Which account number? "))
if id in accountData.keys():
print "Balance = %0.2d" % accountData[id].checkBalance()
else: print "Invalid ID"
물론 사전에 사용하는 키는 그 객체를 유일하게 식별하기만 하면 무엇이든 될 수 있습니다. 키는 예를 들면 이름 같이 객체의 속성 중의 하나가 될 수 있습니다. 유일하기만 하면 무엇이든 다 됩니다. 미가공 재료 장으로 되돌아가 사전 섹션을 다시 읽어 보시면 도움이 되실 겁니다. 사전은 정말 아주 유용한 그릇입니다.
객체 저장하기
이 모든 것의 결점 하나는 프로그램이 끝나면 데이터를 잃어버린다는 것입니다. 객체를 저장할 방법이 필요합니다. 앞으로 더욱 숙련되면 데이터베이스를 사용하여 저장하는 법을 배울 것입니다. 그러나 간단한 텍스트 파일을 사용하여 객체를 저장하고 열람하는 법을 살펴보겠습니다. (파이썬을 사용하고 있다면 Pickle과 Shelve라는 두 가지 모듈이 있습니다.) 이 방법이 훨씬 더 효과적이지만 언제나 그렇듯이 어느 언어에서도 작동할 일반적인 방식을 보여드리겠습니다. 우연하게도 객체를 저장하고 복구하는 능력에 대한 기술적 용어는 영속(Persistence)이라고 부릅니다.
일반적인 방식은 save 메쏘드와 restore 메쏘드를 가장 높은 수준의 객체에 만들고 각 클래스마다 오버라이드하는 것입니다. 그런 식으로 해서 상속받은 버전을 호출하고 지역적으로 속성을 정의해 추가합니다:
class A:
def __init__(self,x,y):
self.x = x
self.y = y
def save(self,fn):
f = open(fn,"w")
f.write(str(self.x)+ '\n') # 문자열로 변환하고 새줄문자를 추가한다.
f.write(str(self.y)+'\n')
return f # 자손 객체가 사용하기 위해 돌려준다.
def restore(self, fn):
f = open(fn)
self.x = int(f.readline()) # 다시 원래 유형으로 변환한다
self.y = int(f.readline())
return f
class B(A):
def __init__(self,x,y,z):
A.__init__(self,x,y)
self.z = z
def save(self,fn):
f = A.save(self,fn) # 부모의 save 메쏘드를 호출한다.
f.write(str(self.z)+'\n')
return f # 자손이 더 있을 경우를 대비한다.
def restore(self, fn):
f = A.restore(self,fn)
self.z = int(f.readline())
return f
# 실체를 생성한다
a = A(1,2)
b = B(3,4,5)
# 실체를 저장한다
a.save('a.txt').close() # 반드시 파일을 닫는다
b.save('b.txt').close()
# 실체를 열람한다
newA = A(5,6)
newA.restore('a.txt').close() # 반드시 파일을 닫는다.
newB = B(7,8,9)
newB.restore('b.txt').close()
print "A: ",newA.x,newA.y
print "B: ",newB.x,newB.y,newB.z
고지: 인쇄되는 값은 복구된 값이지 실체를 만들 때 사용한 값이 아닙니다.
핵심은 각 클래스에서 save/restore 메쏘드를 오버라이드 하고 최초 단계로 부모 메쏘드를 호출하는 것입니다. 그 다음 자손 클래스는 오직 자손 클래스 속성만 취급합니다. 어떻게 속성을 문자열로 바꾸고 그것을 저장할 것인지는 프로그래머인 여러분에게 달려 있지만 반드시 한 줄로 출력되어야 합니다. 복구할 때는 그냥 저장 과정을 거꾸로 하면 됩니다.
클래스와 모듈 혼합해 사용하기
모듈과 클래스는 모두 프로그램의 복잡도를 제어하는 메커니즘을 제공합니다. 프로그램이 커질 수록 클래스를 모듈로 바꾸어서 이런 특징들을 결합하고 싶어지는 것이 당연해 보입니다. 어떤 권위자들은 클래스마다 따로 모듈에 두라고 권장합니다. 그러나 본인이 발견한 진실은 그렇게 하면 모듈이 폭발적으로 증가할 뿐이며 복잡도가 감소되는 것이 아니라 오히려 증가한다는 것이었습니다. 대신에 본인은 클래스를 무리를 지어서 그 무리를 모듈로 바꾸었습니다. 그리하여 위의 예제에서 모든 은행 계정 클래스 정의를 하나의 모듈, 즉 bankaccount에 두고 나서 그 모듈을 사용하는 어플리케이션 코드를 위하여 별도의 모듈을 만들 수 있었을 것입니다. 그를 간략하게 표현하면 다음과 같을 것입니다:
# 파일: bankaccount.py
#
# 은행 계정 클래스 집합을 구현한다
###################
class BankAccount: ....
class InterestAccount: ...
class ChargingAccount: ...
그 다음에 그를 사용하려면:
import bankaccount
newAccount = bankaccount.BankAccount(50)
newChrgAcct = bankaccount.ChargingAccount(200)
# 이제 작업 시작
그러나 서로 상세하게 접근할 필요가 있는 두 개의 모듈에 따로 두 클래스가 있다면 무슨 일이 일어나는가? 가장 간단한 방법은 두 모듈을 반입하고, 두 클래스의 지역 실체를 만들어서 한 클래스의 실체를 상대편 실체 메쏘드에 건네는 것입니다. 객체를 통째로 이리저리 건네기 때문에 객체 지향적 프로그래밍인 것입니다. 한 객체에서 속성을 뽑아내어 또다른 객체에 건넬 필요가 없습니다. 그냥 전체 객체를 건네주세요. 이제 수신 객체가 다형적인 메시지를 사용하여 필요한 정보에 접근합니다. 그러면 그 메쏘드는 그 메시지를 지원하기만 하면 어떤 종류의 객체와도 작동할 것입니다.
예제를 보면서 좀 더 구체적으로 이해해 보겠습니다. logger라는 짧은 모듈을 만들어 봅시다. 클래스가 두 개 담겨 있는데 하나는 파일에 활동상황을 기록합니다. 이 기록기는 log()라는 메쏘드를 달랑 한 개 가질 것입니다. 이 메쏘드는 "기록가능 객체"를 매개변수로 받습니다. 또하나는 Loggable 클래스로서 다른 클래스가 이를 상속받아서 기록기와 작업을 할 수 있습니다. 다음과 같이 보입니다:
# File: logger.py
#
# Loggable 클래스를 만들어서,
# 객체의 활동상황을 기록한다.
############
class Loggable:
def activity(self):
return "이 메쏘드는 지역적으로 오바라이드할 필요가 있습니다."
class Logger:
def __init__(self, logfilename = "logger.dat"):
self._log = open(logfilename,"a")
def log(self, loggedObj):
self._log.write(loggedObj.activity() + '\n')
def __del__(self):
self._log.close()
이제 로거 객체가 삭제되거나 쓰레기 수거될 때 파일을 닫기 위한 파괴자(destructor) 메쏘드(__del__)를 제공하였습니다. 이는 (두개의 '_' 문자에 보이듯이) 파이썬에서 또다른 "마법의 메쏘드"로서 여러모로 __init__()와 비슷합니다.
또 주목하세요. 로그 속성 _log를 이름 앞에 '_'를 붙여 호출하였습니다. 이는 파이썬에서 또다른 일반적인 이름짓기 관례입니다. 클래스 이름에서 단어의 첫 문자는 대문자를 사용하는 것 같이 말입니다. 한 개의 밑줄문자는 그 속성 직접 접근하면 안되며, 오직 클래스의 메쏘드를 통해야만 접근할 수 있다는 표시입니다.
이제 모듈을 사용하기 전에 은행 계정 클래스를 기록가능 버전으로 정의한 모듈을 새로 만들겠습니다:
# File: loggablebankaccount.py
#
# logger 모듈과 작동하도록 Bankaccount 클래스를 확장한다.
###############################
import bankaccount, logger
class LoggableBankAccount(bankaccount.BankAccount, logger.Loggable):
def activity(self):
return "Account balance = %d" % self.checkBalance()
class LoggableInterestAccount(bankaccount.InterestAccount,
logger.Loggable):
def activity(self):
return "Account balance = %d" % self.checkBalance()
class LoggableChargingAccount(bankaccount.ChargingAccount,
logger.Loggable):
def activity(self):
return "Account balance = %d" % self.checkBalance()
다중 상속(multiple inheritance)이라고 부르는 특징을 사용하고 있음을 주목하세요. 하나의 클래스가 아니라 두 개의 클래스에서 상속을 받습니다. 이것이 파이썬에서 필수는 아닙니다. 왜냐하면 그냥 원래 클래스에 activity() 메쏘드를 추가해도 같은 효과를 얻을 수 있기 때문입니다. 그러나 Java나 C++ 같이 정적으로 유형이 정의되는 OOP 언어에서는 이 테크닉이 필수적입니다. 그래서 앞으로 참고하시라고 여기에서 그 테크닉을 보여드리겠습니다.
날카로운 눈을 가지신 분이라면 activity() 메쏘드가 세 클래스 모두에서 동일하다는 것을 눈치채셨을 것입니다. 그것은 타자수를 좀 줄일 수 있다는 뜻입니다. 오직 activity 메쏘드만 있는 중간 유형의 기록가능 계정을 만들면 됩니다. 그러면 세 가지 다른 기록 가능 계정 유형을 그로부터 상속받아 만들 수 있습니다. 그 평범한 Loggable 클래스부터 상속받는 대신에 말입니다. 다음과 같이:
class LoggableAccount():
def activity(self):
return "Account balance = %d" % self.checkBalance()
class LoggableBankAccount(bankaccount.BankAccount,
logger.Loggable)
LoggableAccount):
pass
class LoggableInterestAccount(bankaccount.InterestAccount,
logger.Loggable)
LoggableAccount):
pass
class LoggableChargingAccount(bankaccount.ChargingAccount,
logger.Loggable)
LoggableAccount):
pass
별로 코드가 절약되지는 않지만 세 가지의 동일한 메쏘드 대신에 오직 하나의 메쏘드 정의만 테스트하고 유지하면 된다는 뜻입니다. 공유 기능을 가진 수퍼클래스를 도입하는 이런 유형의 프로그래밍은 종종 믹스인(mixin) 프로그래밍이라고 부르며 최소한의 클래스를 믹스인 클래스(mixin class)라고 부릅니다. 이런 스타일의 일반적인 결과로 최종 클래스 정의는 몸체는 없지만 상속받은 클래스를 기다란 목록에 가지고 있습니다. 바로 여기에서 보여주는 것처럼 말입니다. mixin 클래스는 다른 어떤 클래스로부터도 상속받지 않은 것도 상당히 흔합니다. 본질적으로 그냥 상속의 파워를 이용하여 클래스나 클래스 집합(또는 메쏘드 집합)에 공통 메쏘드를 추가하는 방법일 뿐입니다. (믹스인이라는 용어는 아이스크림 가계의 세계에서 기원합니다. 다양한 맛을 지닌 아이스크림에 바닐라 아이스크림이 추가되어 (즉, 섞여서(mixed in)) 새로운 풍미를 만들어 냅니다. 이런 스타일을 지원하는 첫 언어는 Flavors였습니다. 이 언어는 Lisp의 사촌입니다.)
이제 이런 연습문제를 풀 정도에 이르렀습니다. 연습문제는 로거 객체와 은행 계정을 만들고 그 계정을 로거에 건네는 어플리케이션 코드를 보여주는 것입니다. 그 모두가 서로다른 모듈에 정의되어 있음에도 불구하고 말입니다!
# Test logging and loggable bank accounts.
#############
import logger
import loggablebankaccount as lba
log = logger.Logger()
ba = lba.LoggableBankAccount(100)
ba.deposit(700)
log.log(ba)
intacc = lba.LoggableInterestAccount(200)
intacc.deposit(500)
log.log(intacc)
loggablebankaccount를 반입할 때 단축 이름을 만들기 위하여 as 키워드를 사용한 것에 주목하세요
또 주목하세요. 일단 지역 실체를 만들었으면 더 이상 그 모듈 접두사를 사용할 필요가 없습니다. 한 객체에서 다른 객체로 직접적으로 접근하지 않기 때문에, 즉 모두 메시지를 통하여 접근하기 때문에, 두 클래스 정의 모듈이 직접적으로 서로 참조할 필요도 없습니다. 마지막으로 또 주목할 것은 Logger가 LoggableBankAccount의 실체와 LoggableInterestAccount의 실체 모두와 작동한다는 것입니다. 왜냐하면 둘 모두 Loggable 인터페이스(interface)를 지원하기 때문입니다. 다형성을 통한 객체 인터페이스의 호환성은 모든 OOP 프로그램이 구축되는 토대입니다.
훨씬 더 섬세한 기록 시스템이 표준 라이브러리 logging 모듈에 포함되어 있다는 사실을 지적해야 하겠습니다. 이 모듈은 순전히 테크닉을 보여주기 위한 것입니다. 기록 편의기능이 프로그램에 필요한다면 무엇보다도 표준 logging 모듈을 연구해 보셔야 합니다.
모쪼록 이 이 글이 객체 지향 프로그래밍의 맛을 보여주었기를 바랍니다. 이런 저런 온라인 자습서를 찾아 볼 수 있으며, 또는 처음부터 보다 상세한 정보와 예제가 언급된 책을 읽어 보실 수 있습니다. 이제 VBScript와 JavaScript로 어떻게 OOP를 작업하는지 간략하게 살펴보겠습니다.
기억해야 할 것 |
· 클래스는 데이터와 함수를 하나의 개체에 넣는다(encapsulate). · 클래스는 쿠키 틀(cookie cutters)과 같아서, 실체(instances 또는 objects)를 생성하는데 사용된다. · 객체는 서로 메시지를(messages)를 건네서 교신한다. · 객체가 메시지를 받으면 그에 상응하는 메쏘드(method)를 실행한다. · 메쏘드는 클래스에 속성으로 저장된 함수이다. · 클래스는 메쏘드와 데이터를 다른 클래스로부터 상속(inherit)받을 수 있다. 이 덕분에 원래 클래스를 손대지 않고서도 클래스의 능력을 쉽게 확장할 수 있다. · 다형성(Polymorphism)은 여러 다양한 유형의 객체에게 같은 메시지를 전송하는 능력이다. 각 객체는 응답으로 자신만의 특별한 방식으로 행위한다. · 캡슐화와 다형성 그리고 상속이라는 특징만 갖추면 객체 지향적 프로그래밍 언어이다. · VBScript와 JavaScript는 객체 기반의 언어라고 부른다. 왜냐하면 캡슐화는 지원하지만, 상속과 다형성은 완벽하게 지원하지 않기 때문이다. |
'Python > 파이썬 프로그래밍 연습' 카테고리의 다른 글
Tkinter GUI 프로그래밍 (0) | 2012.04.24 |
---|---|
사건 주도적 프로그래밍 (0) | 2012.04.24 |
정규 표현식 (0) | 2012.04.24 |
이름공간 (0) | 2012.04.24 |
에러 처리하기 (0) | 2012.04.24 |
댓글