객체 지향 프로그래밍

그것은 무었인가?

이제 우리는 약 5년 전에는 진보된 주제로 취급되는 용어였을 어떤 것에로 이동한다. 오늘날 객체 지향 프로그래밍'Object Oriented Programming'은 표준이 되었다. 자바와 파이썬 같은 언어들은 그 개념을 잘 구현하여 여러분은 객체와 어디에선가 마주치지 않고서는 아무것도 할 수가 없다. 그래서 그것은 도데체 무엇인가?

가장 훌륭한 입문서로는, 나의 견해로 다음과 같다:

이것들은 여러분이 목록 아래로 내려 갈수록 크기와, 깊이 그리고 학문적 깊이가 증가한다. 전문가가 아닌 프로그래머들의 목적을 위해서라면 첫 번째가 적당하다. 소개에 초점을 맞춘 프로그래밍 입문서가 더 필요하다면 'Object Oriented Programming by Timothy Budd(2nd edition)'을 시도해 보라. 나는 개인적으로 이것을 읽어보지는 않았으나 그것은 내가 존경하는 사람들의 의견에 의하면 칭송을 받는다. 마지막으로 OO 의 모든 주제에 관하여 방대한 양의 정보를 원한다면 다음의 링크 사이트를 시도해 보라: http://www.cetus.org

여러분이 시간도 부족하고 이러한 모든 책들과 링크들을 지금 당장 연구하려고 하지는 않을 것이라고 가정하고, 나는 여러분에게 간단하게 그 개념에 관하여 설명을 해 주려고 한다. (주 의: 어떤 사람들은 OO 가 이해하기가 어렵다고 생각하고 또 어떤 사람들은 즉각 '이해한다' 여러분이 이전의 범주들을 잘 숙지했다면 걱정하지 마라, 여러분은 실제로 '그 빛을 보지 않고서도' 여전히 객체를 사용할 수 있다.)

마지막 요점으로, 우리는 이장에서 베이직도 티클도 객체를 지원하지 않기때문에 오로지 파이써만을 사용할 것이다. 코딩 관례를 사용하여 OO가 아닌 언어에서도 객체지향적 디자인을 구현할수는 있지만, 그것은 보통 권장할 만한 전략이라기 보다는 가장 마지막으로 의지해야할 선택사항이다. 여러분의 문제가 OO테크닉에 잘 맞다면 그러면 OO언어를 사용하는 것이 가장 좋다.

데이타와 함수 - 모두

객체는 데이타를 처리하는 데이타와 함수의 모둠이다. 이것들은 함께 묶여져서 여러분은 하나의 객체를 여러분의 프로그램의 한 부분으로부터 넘겨줄 수 있다. 그리고 객체는 자동적으로 데이타의 속성attributes뿐만 아니라 사용가능한 처리방법operations에도 역시 접근한다.

예를 들어 문자열 객체는 문자열을 저장할 뿐만 아니라 그 문자열에 대한 처리를 하여 주는 메쏘드methods 또한 제공한다. - 검색하고, 대소문자를 변경하고, 길이를 계산하고 등등.

객체는 메시지 넘겨주기 message passing 메타포를 사용한다 거기에서 한 객체는 메시지를 다른 객체에게 넘겨주고 접수객체는 자신이 가지고 있는 처리를, 즉 메쏘드를 실행함으로써 반응한다. 그렇게 메쏘드는 상응하는 메시지를 접수할 때 주인 객체에 의해서 요청된다. 이것을 표현하는 데는 많은 표기법들이 있지만 가장 일반적인 것은 레코드의 필드에 접근하는 것을 흉내내는 것이다 - 점으로. 그렇게 가상적인 위젯 클래스에도 적용된다:

w = Widget() # create new instance, w, of widget
w.paint()  # send the message 'paint' to it

이것은 그 위젯 객체의 paint 메쏘드가 요청되도록 한다.

클래스를 정의하기

데이타가 많은 형태를 가지는 것과 마찬가지로 객체들은 다른 형태들을 가질 수있다. 이러한 동일한 성질을 가지는 개체의 모둠은 클래스 class라고 알려져 있다. 우리는 객체를 정의할 수 있으며 그것들의 실체 instances를 만들 수 있다. 실체는 실제로 움직이는 객체이다. 우리는 이러한 객체에 대한 참조점을 우리의 프로그램에서 변수에 저장할 수 있다.

구체적인 예제를 살펴보고 우리가 그것을 더 잘 설명할 수 있는지 알아보자. 우리는 문자열과 - 메시지 텍스트 - 그리고 그 메시지를 출력할 메쏘드를 담고 있는 메시지 클래스를 작성할 것이다.

class Message:
    def __init__(self, aString):
        self.text = aString
    def printIt(self):
        print self.text

주 의 1:이 클래스의 메쏘드중의 하나는 __init__ 이라고 불려진다. 그리고 그것은 구성자 constructor라고 불리는 특별한 메쏘드이다. 그 이름이 붙은 이유는 새로운 객체 실체가 생성되거나 혹은 구성될 때 그것이 호출되기 때문이다. 이 메쏘드안에서 할당된 (그러므로 파이썬에 생성된) 어떤 변수도 새로운 실체에 대하여 유일할 것이다. 파이썬에는 이와 같은 특별한 메쏘드들이 많이 있는데, 거의 모두는 __xxx__ 와 같은 이름형식으로 구별된다.

주 의 2:정의된 두 개의 메쏘드 모두 첫 번째 매개변수 self를 가진다. 그 이름은 관례이지만 그러나 객체 실체를 지시한다. 우리가 보게 되듯이 이 매개변수는 실행시에, 프로그램에 의해서가 아니라 인터프리터에 의해 채워진다. 그런식으로 print는 아무런 인수 없이 다음과 같이 호출된다: m.printIt().

주 의 3:우리는 Message 클래스를 대문자 'M'으로 호출하였다. 이것은 순수히 관례적이지만, 그것은 아주 광범위하게 사용되는 것으로, 파이썬 뿐만 아니라 다른 OO언어에도 역시 사용된다. 관련된 관례에 의하면 메쏘드 이름은 반드시 소문자로 시작하고 그 메쏘드 이름 속에 있는 하부 단어들은 대문자로 시작해야만 한다. 그런식으로 "calculate current balance" 라고 불리는 메쏘드는 다음과 같이 쓰여질 것이다: calculateCurrentBalance.

여러분은 잠깐 'Data' 섹션을 재방문하여 다시 '사용자 정의 형'을 살펴보고 싶을지도 모른다. 파이썬 주소록 예제는 이제 약간 더 명료해진 것 같다. 본질적으로 파이썬에서 사용자 정의 형의 유일한 형태는 클래스이다. 속성을 가지지만 ( __init__ 는 제외하고) 메쏘드는 가지지 않는 클래스는 결과적으로 베이직의 레코드와 동등하다.

클래스를 사용하기

클래스를 정의하고 나면 우리는 이제 우리의 메시지 클래스의 실체를 만들 수 있고 그것들을 조작할 수 있다:

m1 = Message("Hello world")
m2 = Message("So long, it was short but sweet")

note = [m1, m2] # put the objects in a list
for msg in note:
    msg.printIt() # print each message in turn

그래서 본질적으로 여러분은 그 클래스를 마치 파이썬의 표준 데이타형인 것처럼 다루기만 하면된다. 그것이 바로 결국 이 연습문제의 목적이다!

같은 것, 다른 것

우리가 지금까지 배운것은 우리 자신만의 클래스형(classes)을 정의하고 이 클래스들의 실체를 생성하며 그리고 그 실체들을 변수에 할당하는 능력이다. 그러면 우리는 메시지를 이 객체들에 넘겨줄 수 있고 이 객체는 우리가 정의한 메쏘드를 활성화시킨다. 그러나 이 OO 에 관한것에는 마지막 한가지 요소가 남아 있는데, 여러면으로 그것은 모든 것중에서도 가장 중요한 면이다.

만약 각자가 상응하는 메쏘드를 가지지만 같은 메시지의 집합을 지원하는 서로 다른 클래스의 두 객체를 우리가 가진다면 그러면 우리는 이러한 객체들을 모두 모아서 그것들을 우리의 프로그램에서 동일하게 취급할 수 있다. 그러나 그 객체들은 서로 다르게 행동할 것이다. 같은 입력 메시지에 대하여 서로 다르게 행동할 수 있는 이러한 능력은 다형성(polymorphism) 이라고 불리운다.

전형적으로 이것은 그리기'paint'메시지를 접수했을 때 수많은 서로 다른 그림 객체들이 자신들을 그리도록 하는데 사용될 수 있다. 동그라미는 삼각형과는 아주 다른 모양이지만, 그 둘 모두가 그리기(paint) 메쏘드를 가지고 있다면, 우리는, 프로그래머로써 그 차이를 무시하고 단지 그것들을 모양'shapes'으로만 생각하면 된다.

예제하나를 살펴보자, 거기에서 우리는 모양을 그리는 대신에 그들의 면적을 계산한다:

먼저 우리는 사각형과 원의 클래스를 만든다:

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받을 수 있다. 이것은 또 다른 클래스와 대부분의 점에서 동일한 새로운 클래스는, 존재하고 있는 클래스의 모든 메쏘드를 다시 구현할 필요가 없다는 뜻이며, 오히려 그러한 능력을 상속받을 수 있고 그리고나서 다르게 행동하기를 원하는 것들을 (위의 경우에서의 그리기 메쏘드와 같이) 덮어 쓸 override 수가 있다는 뜻이다.

다시 한 예제가 이것을 잘 설명해줄 것이다. 우리는 은행 계정의 클래스 계층도 class heirarchy 를 사용할 것이다. 거기에서 우리는 현금을 예치하고, 잔고를 찾고 그리고 철회할 수도 있다. 계정중 어떤 것은 이자를 제공한다 (이것은 모든 잔고에 대하여 계산된다고, 우리의 목적을 위하여 우리는 가정할 것이다 - 은행의 세계에 흥미로운 혁신이다!)그리고 다른 것은 철회에 대하여 벌금을 매긴다.

BankAccount 클래스

그것이 어떠한지 살펴보자. 먼저 은행 계정의 속성들과 처리방법들을 가장 일반적인 (혹은 추상적인(abstract)) 수준에서 고려해 보자.

가장 좋은 방법은 먼저 그 처리방법들을 고려해보고 이러한 처리방법들을 지원하기 위해 필요한 속성들을 제공하는 것이다. 그래서 은행 계정을 위하여 우리는 다음과 같이 할 수 있다:

  • Deposit cash, - 예금
  • Withdraw cash, - 철회
  • Check current balance - 잔고 확인
  • Transfer - 다른 계정으로 송금하기.

    이러한 처리방법들을 지원하기 위하여 우리는 (전송 처리를 위해서) 은행 계정 ID 와 현재의 잔고가 필요할 것이다.

    우리는 그것을 지원하기 위하여 클래스를 만들 수 있다:

    BalanceError = "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 % self.balance
    
        def checkBalance(self):
           return self.balance
    
        def transfer(self, amount, account):
           try:
              self.withdraw(amount)
              account.deposit(amount)
           except BalanceError:
              print BalanceError
    

    주의 1: 우리는 철회하기 전에 잔고를 점검하고 또한 에러를 처리하기 위해 예외상황을 사용하는 것을 점검해야 한다. 물론 BalanceError 라는 에러 형은 없으므로 우리는 그런 것을 만들 필요가 있다 - 그것은 단지 문자열 변수일 뿐이다!

    주 의 2: transfer (전송) 메쏘드는 BankAccount withdraw/deposit 멤버함수 혹은 메쏘드를 사용하여 전송을 한다. 이것은 OO 에서 매우 흔한 일이며 재귀메시징 self messaging이라고 알려져 있다. 그것이 뜻하는 바는 파생된 클래스는 자신만의 deposit/withdraw 버젼을 구현할 수 있지만 transfer 메쏘드는 모든 계정 형에 그대로 남아 있을수 있다는 것을 뜻한다

    InterestAccount 클래스

    이제 우리는 상속을 사용하여 모든 잔고에 대하여 이자를 추가하는 (우리는 3%로 가정할 것이다) 계정을 제공한다. 그것은 deposit 메쏘드만 빼고는 표준적인 BankAccount 클래스와 동일하다. 그래서 우리는 쉽게 그것을 덮어 쓴다:

    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 클래스

    이 계정은 다시 이번에는 5$를 모든 철회에 대하여 청구한다는 점만 빼고는 표준적인 BankAccount class 와 동일하다. 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 *
    
    # First a standard BankAccount
    a = BankAccount(500)
    b = BankAccount(200)
    a.withdraw(100)
    # a.withdraw(1000)
    a.transfer(100,b)
    print "A = ", a.checkBalance()
    print "B = ", b.checkBalance()
    
    
    # Now an InterestAccount
    c = InterestAccount(1000)
    c.deposit(100)
    print "C = ", c.checkBalance()
    
    
    # Then a ChargingAccount
    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()
    
    
    # Finally transer from charging account to the interest one
    # The charging one should charge and the interest one add
    # interest
    print "C = ", c.checkBalance()
    print "D = ", d.checkBalance()
    d.transfer(20,c)
    print "C = ", c.checkBalance()
    print "D = ", d.checkBalance()
    

    우리가 a.withdraw(1000)의 라인에 있는 주석을 제거하면 예외상황이 작동하는가를 살펴보라.

    바로 그것이다. 대단히 간단한 예제에 불과하지만 그것은 어떻게 상속이 기본적인 프레임워크를 강력한 새로운 사양으로 확장하는데 사용될 수 있는지를 즉시 보여준다.

    우리는 어떻게 단계별로 예제를 작성할 수 있는지 그리고 어떻게 테스트 프로그램을 그것이 잘 작동하는지 점검하여 합칠 수 있는지를 살펴보았다. 우리의 테스트는 완전하지는 않다. 우리가 모든 경우를 다루지 않았고 우리가 포함시켜야할 더 많은 점검사항이 있기 때문이다. - 만약 한 계정이 음수의 값으로 만들어질 때는 어떻게 해야하는지와 같은....

    그러나 다행스럽게도 이것은 여러분에게 객체 지향형 프로그래밍의 맛을 보여 주었다. 그리고 여러분은 또 다른 온라인 지침서들 중 몇몇으로 진일보 할 수 있다. 또는 서반에 언급한 책들중 하나를 더욱 많은 정보와 예제들을 찾기 위하여 읽을 수 있다.


    Previous  Next  Contents


    여러분이 이 페이지에 대하여 질문 혹은 제안사항이 있으면 다음주소로 나에게 전자메일을 보내라: agauld@crosswinds.net