5. 다형 개념
다형 개념이라는 용어는 “같으면서도 다른 것” 혹은 “다르면서도 같은 것”을 다루기 위해 사용되는 개념이다. 2.4 절에서 논의한 바와 같이 “같은 것”과 “다른 것”에 대한 구별은 객체 지향 원리에서 매우 중요한 요소이다. 2.4 절에서 알아본바 와 같이 두 개의 사물이 같은가 혹은 다른가에 대한 결정은 관점에 따라 다르게 이루어진다는 것을 알 수 있었다. 그런데 우리는 실제 생활에서 구체적으로는 다른 객체들을 포괄적으로 같다고 해석해야 할 경우가 있다. 예를 들어 “살인범”이나 “절도범”이나 “뇌물 수수범”이나 모두 “범죄자”로 해석할 수 있다. 또한 “범죄자”나 “안 범죄자”나 모두 같은 “사람”으로 해석할 수 있다. 이 경우 “범죄자”와 “안 범죄자”가 같은가 혹은 다른가? 이 질문에 대하여 이들이 다르다고 답할 수 있기도 하고, 이들이 모두 “사람”이라는 점에서 같다고 답할 수도 있다는 것이다. 이와 같은 상황을 프로그램에서 반영하기 위하여 필요한 개념이 다형 개념이다. 다형 개념은 상속과 밀접한 연관을 갖고 있다.
다형 개념이 객체 지향 프로그래밍 시에 반영되는 경우는 이질적인(heterogeneous) 객체들의 리스트를 만드는 경우이다. 다음과 같은 논리적인 리스트 someList를 생각해 보자.
someList = { “사람 객체 김태균”, “자동차 객체 부산 30 더 6875”, “사람 객체 김은영”, “비행기 객체 KAL 700 편”, “기차 객체 새마을 123 편”, “비행기 객체 ASIANA 300 편” }
우선 주의해야 할 사항은 이 리스트의 요소들이 문자열을 의미하는 것이 아니라 논리적인 실 객체를 의미한다는 것이다. 즉 첫 번째 요소 “사람 객체 김태균”은 실제 사람를 말한다. 객체 지향 프로그래밍을 해보지 않은 독자들은 배열을 사용하여 동질적인(homogeneous) 객체들을 위한 리스트만을 작성해 보았을 것이다. 예를 들어 “학생”을 위한 리스트 studentList를 {(“성규환”,100,80,90), (“이충실”,30,50,60), (“홍필희”,70,60,80) ...} 와 같이 표현하여 프로그래밍 한 경험이 있을 것이지만 이질적인 객체들의 리스트에 대한 프로그래밍은 경험이 없을 뿐더러 아마 그 필요성도 느끼지 못할 것이다. 그러나 객체 지향 프로그램에서는 someList와 같이 이질적인 객체를 포함하는 리스트를 작성함으로써 프로그램의 유연성을 달성하는 것이 일반적이다. someList에 속하는 객체들이 같은 리스트에 속할 수 있는 이유는 이들이 공통점을 같고 있기 때문인데 그 공통점은 이들이 모두 “움직일 수 있다”는 것이다. 다형 개념이 적용되는 리스트는 항상 상속 구조의 영향을 받는다. 즉 someList와 같은 리스트가 존재할 수 있기 위해서는 (그림 2.6)과 같은 클래스들의 상속 관계가 설정되어 있어야 한다.
(그림 2.6)의 상속 관계에서 독자가 주지해야 할 사항은 이 계층의 말단에 해당하는 Person 클래스, Car 클래스 등이 모두 “move()”라는 연산을 가지고 있다는 점이다. 즉 Person 클래스의 move() 연산은 사람을 움직이며, Car 클래스의 move() 연산은 자동차를 움직인다. 이러한 점에서 다형 개념이 제공하는 또 다른 의미는 연산자 중복(operator overloading)이다. 즉 다형 개념은 똑같은 이름의 연산이 다른 클래스에서 다른 작업을 할 수 있도록 하는 것을 의미하기도 한다. Person 클래스의 move() 연산과 Car 클래스의 move() 연산은 같은 이름이지만 구현 내용은 전혀 다르다. 이 경우 독자들은 Person 클래스의 move() 연산과 Car 클래스의 move() 연산이 같다고 말하겠는가? 혹은 다르다고 말하겠는가? 각 연산의 구현은 객체 지향 기법에서 메소드(method)라 불리는데 우리는 앞의 질문에 대하여 “각 클래스의 연산자 이름은 같으며 메소드는 다르다.”라고 답할 수 있다. 이와 같이 “같으면서도 다른 것” 혹은 “다르면서도 같은 것”을 다루는 개념이 다형 개념이다. 결론적으로 다형 개념이 객체 지향 프로그래밍 시에 구체적으로 반영되는 두 가지 상황은 함수 중복 경우와 폴리몰픽 리스트 경우이다.
다형 개념은 프로그래밍 시에 시스템이 제공하는 동적 바인딩 기능에 의하여 그 효용성이 입증된다. 동적 바인딩은 연산자의 호출에 따른 메소드의 선택이 실행 시에 이루어지는 기법을 의미하는데 이를 통하여 소비자는 원하는 연산과 메소드의 바인딩에 대한 의무감을 전혀 가질 필요 없이 유연한 마음가짐으로 서비스를 이용할 수 있다. 또한 다형 개념의 사용으로 인해 기존 프로그래밍 언어의 사용 시 중복되지 않는 함수의 이름을 고안하기 위해 신경 써야하는 어려운 점을 피할 수 있다.
그림 2.6 someList가 사용되기 위한 클래스의 상속 계층
6. 동적 바인딩
객체 지향 기법을 사용하는 중요한 이유 중의 하나가 사용자의 요구 변화에 적절히 대응할 수 있는 시스템을 만들고자 하는 것이다. 이를 위하여 공급자가 제공하는 서비스의 추가나 변경이 발생했을 경우 사용자가 영향을 받지 않도록 하는 방법이 필요하다. 이를 위해 도입된 객체 지향 기법의 중요한 기능이 동적 바인딩이다. 동적 바인딩과 비교될 수 있는 상대적인 개념은 정적 바인딩(static binding)이다. 구조적 언어에서 사용되는 바인딩 방식은 대부분 정적 바인딩이다. 동적 바인딩을 이해하기 위하여 우선 “바인딩”의 의미를 먼저 알아보자. 바인딩이란 함수 호출(function call)과 호출된 함수를 연결하는 메커니즘을 말한다. 예를 들어 다음과 같이 Fortran 프로그램의 호출문이 있고 서브루틴 SORT()의 본체가 어디엔가 있다고 가정하자.
...
CALL SORT(DATA)
...
이 호출문은 컴파일 작업과 링크 작업이 끝난 후에 다음과 같은 가상적인 어셈블리 코드로 바뀔 것이다.
...
현재 상태를 스택에 저장하고 actual parameter를 함수에 넘겨주기 위한 코드 부분
branch 12345
...
함수 호출을 위한 컴파일 및 링크 작업의 가장 중요한 결과는 호출될 함수의 논리적 주소 할당과 이의 연결이다. “branch 12345”에서 12345 는 SORT() 함수가 존재하는 논리적인 번지수이다. 주 프로그램의 호출문은 어셈블리어에서는 분기문으로 변환된다. 이때 분기해야 될 주소를 결정하는 것이 바인딩 작업의 임무이다. 그런데 구조적 언어에서는 호출될 함수의 주소가 컴파일 시에 혹은 링크 시에 완전히 결정된다. 이와 같이 호출될 함수의 주소가 컴파일 시나 링크 시에 결정되는 바인딩 기법을 정적 바인딩이라 한다. 그런데 동적 바인딩 기법에서는 호출되어야 할 함수의 주소가 프로그램이 로드 될 때까지 결정되지 않고 프로그램의 실행 시에 결정된다. 이를 위해서는 프로그램의 수행 시에 바인딩 임무를 수행하기 위한 메시지 디스패치 메커니즘(message dispatching mechanism)이 필요한데 프로그래밍 언어에 따라 메시지를 디스패치 할 수 있는 특별한 서버 함수를 이용하거나 혹은 특별한 자료구조를 이용하거나 한다.
동적 바인딩의 도입은 프로그램 작성 방식에서 커다란 변화를 야기했다. 즉 사용자 관점에서의 메소드 선택을 위한 코딩 부담을 시스템에게 전가할 수 있게 함으로써 프로그래밍이 좀 더 유연하게 된 것이다. 사용자 입장에서의 직접적인 메소드 선택은 시스템의 유연성을 저하시키는데 반하여 동적 바인딩에 의한 자동적인 메소드 선택은 시스템을 유연성을 향상시킨다. 동적 바인딩이 적용될 경우의 코딩 스타일을 앞 절의 다형 개념에서 언급했던 예를 대상으로 살펴보도록 하겠다. 앞 절에서 언급된 이질적인 객체를 수용하는 someList를 이용한 객체 지향 프로그래밍 스타일은 (코드 2.3)과 같다.
1: MovableObjectList someList;
2: ...
3: // someList에 객체들을 삽입하는 부분
4: someList.addTail(new Person(“김태균”, MALE, 100, 200));
5: someList.addTail(new Car(“부산 30 더 6875”, “비스토”, 110, 210));
6: someList.addTail(new Person(“김 은영”, FEMALE, 90, 190));
7: someList.addTail(new Aircraft(“KAL”, 500, 115, 200, 0));
8: someList.addTail(new Train(“새마을 123”, 10, 100, 100));
9: someList.addTail(new Aircraft(“ASIANA 300”, 300, 110, 200, 0));
10: ...
11: POSITION pos = someList.getHeadPosition(pos);
12: while (pos != NULL) {
13: MovableObject *pMovingObject = someList.getNext(pos);
14: pMovingObject->move(Δx,Δy);
15: }
16: ...
코드 2.3 동적 바인딩을 이용한 객체 지향 프로그래밍 스타일
(코드 2.3)은 완벽한 C++ 프로그램이 아니다. 하지만 이 코드는 완벽한 C++ 코드와 거의 비슷하다. 이 코드에서 1 라인의 문장은 클래스 MovableObjectList의 한 객체 someList를 생성하는 것을 의미한다. 물론 이를 위해서는 MovableObjectList라는 클래스가 미리 만들어져 있어야 한다. 다음으로 4,5,6,7,8,9 라인의 문장들은 앞 절에서 언급한 다형 개념에 의하여 이질적인 객체들을 someList에 삽입하는 것이다. 이들 문장에서 알 수 있는 바와 같이 someList에는 Person, Car, Aircraft, Train 객체들이 혼합적으로 삽입되었다. 각 객체를 생성하는 명령어로부터 각 클래스의 자료 구조를 짐작할 수 있을 것이다. 예를 들어 Person 클래스의 데이터에는 “이름”, “성별”, “x 좌표”, “y 좌표”등이 있으며, Car 클래스의 데이터에는 “차번호”, “차종”, “x 좌표”, “y 좌표”등이 있다. 그리고 Aircraft 클래스의 데이터에는 “회사이름”, “편번호”, “x 좌표”, “y 좌표”, “z 좌표”등이 있고 마지막으로 Train 클래스의 데이터에는 “차종”, “차량 수”, “x 좌표”, “y 좌표”등이 있다.
“어떠한 경우에 이러한 응용 프로그램이 필요할까 ?”라는 의문을 가질 수 있을 것이다. 이 의문에 대한 답을 한 가지 예로 들어보겠다. “부산 해운”이라는 해운회사에 엄청나게 큰 화물선이 있어서 사람, 자동차, 기차, 비행기들을 한꺼번에 실어 나를 수 있다고 가정하자. 또한 이 회사의 본사에서는 GIS(Geographical Information System)를 갖추고 있어서 자기 회사에 속하는 화물선들의 위치와 화물선에 선적되거나 혹은 선적되지 않은 화물들의 위치를 그래픽 화면을 통해 확인할 수 있는 시스템을 갖추고 있다고 하자. 이런 경우라면 화물선에 선적된 화물들의 위치는 화물선이 이동할 때마나 같은 변위만큼 이동해야할 것이다. 만약에 어떠한 화물선이 x 축으로 Δx, y 축으로 Δy 만큼 이동했다고 가정해 보자. 이 경우에 GIS 시스템은 그 화물선에 선적된 객체들의 좌표 값을 (Δx,Δy) 만큼 이동 시켜야 할 것이다. 이러한 경우를 반영하는 코드 부분이 11,12,13,14 라인의 문장들이다. 즉 이 문장들은 someList에 있는 모든 객체를 (Δx,Δy) 만큼 움직이게 하는 부분이다. 이때 14 라인의 문장이 동적 바인딩에 의하여 처리되는 것이다. 즉 이 문장에 의하여 someList에서 추출된 객체가 무엇인가에 관계없이 자동적으로 해당 메소드가 선택되어 수행된다. 만약 pMovingObject가 Person인 경우에는 Person 클래스의 move() 메소드가 수행되며, Car인 경우에는 Car 클래스의 move() 메소드가 수행된다. 그리고 이 예제에서의 가정은 각 클래스의 move() 메소드가 수행하는 작업이 서로 다르다는 것이다.
객체 지향 언어에서는 새로운 클래스 추가되더라도 사용자 입장의 프로그램 코드를 수정할 필요가 없다. 따라서 공급자에 의하여 제공되는 자원의 변화에 민감하게 반응하지 않아도 되는 객체 지향 프로그래밍은 응용 프로그램의 독립성을 보장하며 소프트웨어의 유지보수를 쉽게 한다.
C++ 언어를 통하여 동적 바인딩을 이용하고자 하는 경우에 필수적으로 사용해야 하는 문법 구조가 가상 함수(virtual function)이다.
동적 바인딩과 관련하여 마지막으로 집고 넘어가야 할 사항은 함수 호출을 표현하는 문법 구조이다. 객체 지향 프로그램을 처음 접하는 사람의 대부분은 “someList.insert(x)” 혹은 “pMovingObject->move(Δx,Δy)” 과 같은 함수 호출 방식에 대하여 약간의 거부감을 느낄 것이다. 실제로 객체 지향 사회에서는 이러한 문장을 “함수 호출”(function call)이라고 지칭하지 않고 “메시지 보내기”(message sending)라고 부른다. 그 이유는 “pMovingObject->move(Δx,Δy)” 라는 문장이 마치 “객체 pMovingObject에게 Δx, Δy 만큼 이동하라는 요구 사항을 보내니까 네가 알아서 처리해라”라는 의미로 해석될 수 있기 때문이다. 따라서 “메시지 보내기”는 동적 바인딩을 염두에 둔 용어이다. “메시지 보내기”는 “함수 호출”이라는 용어와 물리적인 관점에서는 동일하지만 논리적으로는 공급자 객체에 대하여 서비스를 요구한다는 의미를 갖기 때문에 전혀 다르다. 물론 여기에서 “메시지”라는 용어는 통신에서 사용되는 용어인 메시지 스위칭(message switching) 혹은 패킷 스위칭(packet switching) 등과 같은 “메시지”와 전혀 관계가 없다.
8. 마치며...
3절부터 6절까지 다룬 내용인 “자료 추상화”, “상속”, “다형 개념”, “동적 바인딩”의 네 가지 속성이 객체 지향성을 나타내는 핵심적인 속성들이다. 객체 지향 패러다임 하에서 작업하는 소프트웨어 엔지니어는 객체 지향 프로그래밍을 하든, 혹은 객체 지향 분석 및 설계를 하든 상관없이 항상 이 네 가지 개념을 염두에 두고 작업해야 한다. 아울러 주의해야할 사항은 이들 개념이 상호 독립적으로 존재하는 것이 아니라 서로 맞물려있다는 점이다. 이들 네 가지 개념의 상호 관계를 도식화한 것이 (그림 2.7)이다.
그림 2.7 객체 지향 개념의 상호 관계
(그림 2.7)의 특징은 크게 두 가지인데 그 하나는 자료 추상화가 이들 개념의 기초라는 점이고 다른 하나는 나머지 세 개념이 서로 맞물려 있다는 점이다. 즉 객체 지향 개념이 생기게 된 시발점은 사용자와 공급자의 구별, 정보 은닉의 구현, 명세부와 구현부의 구분을 통한 복잡성 제어 등을 이루기 위한 자료 추상화의 도입이며 이를 기초로 나머지 개념들이 시스템의 복잡성을 제어하기 위해 복합적으로 사용되는 것이다. 특히 독자들이 주지해야할 사항은 상속, 다형 개념, 동적 바인딩의 관계가 서로 분리할 수 없는 삼위일체 요소들이라는 점이다. 예를 들어 동적 바인딩을 이용하지 않고 상속과 다형 개념만을 이용하여 시스템을 구현한다거나 상속을 이용하지 않고 다형 개념과 동적 바인딩 두 가지 만을 이용하여 시스템을 구현한다는 것은 객체 지향 시스템으로서의 의미가 없다. 결론적으로 소프트웨어 엔지니어들은 시스템의 설계 및 구현 시에 (그림 2.7)에 나타난 개념들을 의도적으로 사용할 수 있는 능력을 갖추어야만 객체 지향의 장점을 맛볼 수 있다.
'프로그래밍 개론' 카테고리의 다른 글
| 객체지향 프로그래밍 자료 추상화/상속 (0) | 2016.01.28 |
|---|---|
| 프로그래밍 객체지향 개념/추상화/클래스 (0) | 2016.01.28 |