객체지향 이야기 #1

2006. 10. 6. 01:44
객체는 특성과 행위를 하나의 단위로서 포함하고 있다. 다시 말하면, 특성과 행위가 객체라고 하는 캡슐 안에 포함되어 있는 것이다. 결국 객체란 캡슐화라고 하는 객체지향의 원리를 구체화한 것이 되는 것이다. 자동차 객체의 운전이란 행위는 운전자에게 노출된 인터페이스가 되며, 엔진 등의 특성은 자동차를 구현하는 세부적인 사항이 되는 것이다.

클래스명

특성

행위

클래스의 특성과 행위의 UML 표현

Car

body
engine
steering
transmission
wheel

start()
stop()
accelerate()
slowDown()turnLeft()turnRight()

Car 클래스의 특성과 행위

대구32다1234 : Car

객체의 UML 표현의 예


붕어빵은 붕어빵 만드는 기계를 통해서 만들어진다. 그리고 붕어빵 만드는 기계는 쇠로 만들어진 틀이다. 이것을 ‘몰드’라고 한다. 붕어빵은 붕어빵 몰드의 인스턴스가 된다. 붕어빵이 붕어빵 몰드에 정의된 형태를 만족하는 하나의 경우니까. 그러니까 붕어빵은 객체가 되는것이다. 또한 붕어빵 몰드는 붕어빵이라고 하는 객체를 생성하는 수단이 되므로 클래스가 되는 것이다.

C++언어에서 클래스의 인스턴스 즉, 객체는 두 가지 방법으로 생성할 수 있다. 그 중 하나는 다음과 같이 지역 변수와 같은 형식으로 생성하는 것이다. 이 방법은 해당 객체가 정의된 코드 블록을 벗어날 때 자동적으로 소멸된다. 다시 말해 객체의 생명은 해당 코드 블록 안에서만 지속된다.


Car myCar;    // Car 인스턴스 생성


다른 하나는 다음과 같이 new 키워드를 사용하는 것이다. 이때 new 키워드는 생성하여 저장한 인스턴스의 메모리 상에서의 시작 위치 즉, 포인터를 반환하게 된다. 우리는 이 포인터를 통해서 해당 인스턴스 즉, 객체에 접근할 수 있게 된다.


Car* pMyCar = new Car;      // Car 인스턴스 생성


이렇게 생성된 인스턴스가 더 이상 필요 없게 되면 반드시 delete 키워드를 사용해서 소멸시켜 주어야 한다. 그렇지 않으면 메모리의 우주 공간을 하염없이 떠도는 우주 미아가 되어 버린다. 이런 현상이 자꾸만 반복되면 쓸데없는 메모리가 쓰레기로 가득 차게 되어 메모리 누수 현상(memory leakage)이란 커다란 문제를 일으키게 된다. 다시 말해 new 키워드로 생성한 객체는 delete 키워드를 사용하여 소멸시킬 때까지 생명이 지속된다.

이와 같이 클래스의 인스턴스가 생성되면 해당 객체의 특성 정보는 유효한 값으로 초기화되어야 할 필요가 있다. 예를 들어서 날짜 정보를 관리하는 Date라고 하는 클래스가 있다고 하자. 이 클래스에는 다음과 같이 각각 년, 월, 일 정보를 관리하는 특성을 정의할 수 있다.


Date 클래스

Date

year : int
month : int
day : int



그리고 우리가 다음과 같이 Date 클래스의 인스턴스를 생성했다고 하자


Date myDate;


그렇다면 myDate 인스턴스의 year, month, day 데이터 멤버에는 어떤 값이 저장되어 있을까? 결론을 말하면 이들 데이터 멤버에는 쓰레기 값이 저장된다. 해당 메모리 위치에 우연히 남아있던 값이 저장되는 것이다.

이렇게 우리가 원하는 값으로 멤버를 초기화할 수 있도록 각 언어에서는 생성자(constructor)라고 하는 것을 정의할 수 있게 한다. 다시 말해 생성자란 클래스의 인스턴스가 생성될 때 자동적으로 호출되어 특성을 초기화하는 기능을 수행하는 특수한 멤버 함수 또는 메서드인 것이다. 디폴트 생성자는 우리가 클래스에 생성자를 정의하지 않았을 때 각 언어에서 기본적으로 제공해주는 생성자를 말한다. C++, 자바, C# 언어의 경우네는 다음과 같이 Date 클래스의 생성자를 정의할 수 있다.


Date (int yy, int mm, int dd) {
       year = yy;
       month = mm;
       day = dd;
}


이 경우 C++ 언어에서는 다음과 같이 인스턴스의 초기값을 지정할 수 있다.


Date myDate(2001, 12, 8);


또는


Date* myDate = new Date(2001, 12, 8);


생성자가 클래스의 인스턴스가 생성될 때 초기화하는 역할을 담당한다면 인스턴스가 소멸될 때 뒷정리를 하는 역할을 하는 것이 필요할 수도 있다. 이러한 역할을 하는 특별한 멤버 함수 또는 메서드를 소멸자(destructor)라고 한다. C++와 C# 언어에서 소멸자는 클래스명 앞에 지렁이(~, tilde)를 붙인 메서드로 구현된다. 그러나 생성자와 달리 소멸자는 인수를 지정하지 않는다.

클래스의 각 인스턴스는 서로 다른 데이터 멤버 또는 필드 값을 저장하게 된다. 다음 그림과 같이 myDate와  yourDate등 두 개의 Date 클래스 인스턴스가 있다고 가정하자.


myDate : Date

yourDate : Date

두 개의 Date 클래스 인스턴스


C++언어의 경우에 이들 Date 클래스의 인스턴스는 다음과 같은 코드로 생성된다.


Date myDate(2001, 9, 9);
Date yourDate(2001, 12, 8);


또는


Date* myDate = new Date(2001, 9, 9);
Date* yourDate = new Date(2001, 12, 8);


위의 코드에서 myDate와 yourDate 인스턴스의 데이터 멤버 또는 필드에는 각각 서로 다른 데이터가 저장된다. 따라서 이러한 데이터를 인스턴스 데이터(instance data)라고 한다. 그러나 이들 인스턴스 사이에도 멤버 함수나 메서드의 코드는 서로 공유하게 된다. 하지만 이들 메서드에서 어떤 year, month, day 등의 데이터 멤버 또는 필드를 사용하는가 하는 것은 해당 메서드를 호출한 인스턴스가 무엇인가에 따라 결정된다. 따라서 각 메서드에서는 어떤 인스턴스가 자신을 호출했는지를 알아야 할 필요가 있는 경우가 있다. 이것을 위해 C++와 자바, C# 언어에서는 this라는 키워드를 사용한다. 메서드 안에서 this 키워드를 명확하게 사용하지 않더라도 데이터 멤버나 필드 즉, 인스턴스 데이터에 접근할 때는 이미 이들을 암시적으로 사용하고 있다. 따라서 C++ 언어에서의 위의 Date 클래스의 생성자는 다음과 같이 고쳐 쓸 수 있다.


Date (int yy, int mm, int dd) {
       this->year = yy;
       this->month = mm;
       this->day = dd;
}


그러나 때로는 모든 인스턴스가 공유할 수 있는 데이터 멤버나 필드가 필요한 경우도 있다. 예를 들어 여러분이 은행의 예금 계좌를 나타내느 s 클래스를 구현해야 한다고 가정하자. 우선 이 클래스에는 고객의 이름과 예금 잔액을 저장할 필드 멤버가 필요할 것이다. 또한 현재의 이자율에 따라 이자액을 계산하여 예금 잔액에 추가하는 메서드도 필요할 것이다. 이때 우리는 다음 그림과 같은 SavingAccount 라는 이름을 갖는 클래스를 생각해볼 수 있다.


SavingAccount

name : string
amoung : double
interestRate : double

computeInterest()

SavingAccount 클래스

유진 : SavingAccount

name=유진
amount=100,000
interestRate : 0.01

진하 : SavingAccount

name=진하
amount=11,000,000
interestRate : 0.01

승욱 : SavingAccount

name=승욱
amount=1,000
interestRate : 0.01

SavingAccount 클래스의 인스턴스


여기서 interstRate 필드 멤버의 값을 주목하기 바란다. interestRate 필드 멤버에는 현재의 이자율이 저장된다. 그런데 이자율을 이와 같이 각 인스턴스에 모두 저장하는 것은 몇가지 문제를 일으키게 된다. 먼저 이자율이 변경되는 경우에 모든 인스턴스의 interestRate 필드 멤버의 값을 변경시켜야 한다. 또한 이렇게 각 인스턴스에 이자율을 저장하는 것은 아무래도 메모리 낭비라는 생각이 든다. 사람에 따라 이자율이 차등적으로 적용된다면 몰라도, 모든 사람들에 대하여 동일한 이자율이 적용된다면 모든 인스턴스에 이자율을 저장할 필요가 없는 것이다.

이자율은 그 클래스에 속해 있는 모든 인스턴스들이 공유할 수 있는 곳에 저장되는 것이 바람직하다. 그래야 이자율이 변경될 때도 모든 인스턴스들이 변경된 이자율을 반영할 수 있게 될 테니까. 우리는 이자율을 공유 멤버(shared member) 또는 정적 멤버(static member)로 정의할 수 있다. 공유 멤버는 클래스의 어떤 하나의 인스턴스에만 속해있는 것이 아니라, 전체로서 클래스에 속하며 해당 클래스의 모든 인스턴스에 공유되는 멤버를 가리킨다. UML에서 공유 멤버에는 밑줄이 그어진다.


공유 멤버

SavingAccount

name : string
amoung : double
interestRate : double

computeInterest()


이때 다음 그림과 같이 각 인스턴스에는 interestRate 필드 멤버가 빠지고 별도의 공유 메모리에 저장되어 SavingAccount 클래스의 모든 인스턴스 사이에 공유된다.


공유 멤버의 메모리 구조

유진 : SavingAccount

name=유진
amount=100,000

진하 : SavingAccount

name=진하
amount=11,000,000

승욱 : SavingAccount

name=승욱
amount=1,000

: SavingAccount

interestRate : 0.01



C++ 언어에서는 정적 멤버라는 용어를 사용하며  static이란 키워드를 지정한다


class SavingAccount
{
      char name [20];
      double amout;
      static double interestRate;
}


C++언어에서 정적 멤버는 다른 데이터 멤버와는 달리 클래스의 생성자에서 초기화할 수는 없다. 조금만 더 생각해보면 아주 당연하다고 느끼게 될것이다. 만약 인스턴스가 생성될 때마다 초기화된다면 해당 클래스에 속해있는 모든 인스턴스에 적용되는 값이 그때마다 달라질 것이기 때문이다. 따라서 C++언어에서는 다음과 같은 구문으로 정적 멤버를 초기화하게 된다.


double SavingAccount::interestRate = 0.01;

밥짓는아이 테크노트/기타