3. 자료 추상화 


 자료 추상화는 객체 지향 기법의 기본이 되는 원리로 객체의 정의 시에 데이터와 이 데이터에 적용 가능한 연산을 함께 정의하는 방식을 말한다. 자료 추상화는 구현 시에 자료 캡슐화(data encapsulation)를 통하여 이루어진다. 자료 추상화를 이용하는 이유는 이를 통하여 외부와 독립적으로 다루어 질 수 있는 소프트웨어 부품을 구현하기 위함이다. 자료 추상화의 중요 목적은 소프트웨어를 “명세부”와 “구현부”로 분리하는 정보 은닉이다. 자료 추상화를 통한 객체들의 구현은 외부의 요구 변화에 영향을 덜 받는 효과를 얻는다. 그리고 객체의 구현과 관계되는 상세한 정보들이 객체 외부에 알려질 필요가 없기 때문에 소프트웨어의 생산자와 소비자의 관계가 분명해진다.


 자료 추상화는 초기에 프로그래밍을 위한 원리라기보다는 자료형을 기술하기 위한 도구로 사용되었다. 자료 추상화를 통하여 정의되는 자료형을 추상 자료형(abstract data type)이라 한다. 자료 추상화의 관점에서 자료형을 정의하면 그 의미가 명확하고 간결해지기 때문에 자료 추상화는 자료형을 기술하기 위하여 사용될 수 있고 따라서 객체 지향 기법이 확산되기 이전에도 널리 사용되었던 개념이다. 자료 추상화를 이용하여 소프트웨어 객체를 정의하기 위해서는 네 가지 요소가 언급되어야 하는데 이들은, 자료의 정의(data definition), 연산자의 정의(operator definition), 공리(axiom), 예외 처리(exception handling)이다. 우선 “자료의 정의”는 대상이 되는 소프트웨어 객체를 컴퓨터 내부에서 표현하고자 하는 자료 구조로 정의하는 것을 말하며 이는 그 객체의 “구현” 대상이므로 공급자만이 관심을 가지면 된다. “연산자의 정의”는 소프트웨어 객체가 제공하는 “함수”의 명칭과 그들의 시그니처(signature)를 밝히는 부분으로 이 부분이 객체 소비자에게 공개되는 “명세부”이다. “공리”는 “연산자의 정의”에서 나열된 함수들이 만족해야 할 조건들을 수학적으로 엄밀하게 기술한 것으로 이들은 함수 본체의 “구현”시에 알고리즘으로 반영되는 부분이다. 즉 “공리”는 그 함수를 만들어야 되는 필요성 혹은 요구사항을 명시하는 것이다. 마지막으로 “예외 처리”는 함수의 실행 시에 발생할 수 있는 에러의 종류를 나열한 부분이다. 다음은 이의 사용 예로서 스택(stack)에 대한 정의를 자료 추상화를 통하여 기술한 것이다.


1: structure STACK(object)

2: declare CREATE() -> stack

3: ADD(object,stack) -> stack /* push */

4: DELETE(stack) -> stack /* pop */

5: TOP(stack) -> object

6: ISEMTS(stack) -> boolean;

7: for all S in stack, i in object let

8: ISEMTS(CREATE) ::= true

9: ISEMTS(ADD(i,S)) ::= false

10: DELETE(CREATE) ::= error

11: DELETE(ADD(i,S)) ::= S

12: TOP(CREATE) ::= error

13: TOP(ADD(i,S)) ::= i

14: end

15: end STACK

16: CREATE(stack) ::= var stack: array[1..N] of objects; 

17: top: 0..N;

18: ISEMTS(stack) ::= if top = 0 then true

19: else false;

20: TOP(stack) ::= if top = 0 then error

21: else stack[top];

코드 2.1 자료 추상화를 이용한 스택의 명세


 이 정의에서 2,3,4,5,6 라인에 기술된 내용이 스택의 “명세” 부분으로 스택이 제공하고 있는 연산자들을 나열한 것이다. 즉 이 내용들로부터 스택의 소비자는 다섯 가지의 함수가 외부에서 사용 가능함을 알 수 있다. 이 함수들은 마치 수학에서의 함수 정의와 비슷한 방법으로 기술되는데 예를 들어 ADD 함수 경우에 정의 구역(domain)은 object와 stack이고 치역(range)은 stack임을 나타낸다. 이때 정의 구역은 프로그래밍 언어 입장에서 함수의 입력 값들, 그리고 치역은 함수의 출력 값으로 이해하면 된다. 다음으로 8,9,10,11,12,13 라인에 기술되어 있는 내용은 스택이 만족해야 할 조건들을 명시한 “공리” 부분이다. 이 공리 중에서 “ISEMTS(CREATE) ::= true” 문장이 의미하는 것은 “스택을 처음 생성한 이후에 그 스택이 비어 있는가를 물으면 그 답이 참 값이어야 함”을 말한다. 추상 자료형에서 정확한 공리들이 많이 나열될 수록 그 자료형의 구현이 더욱 용이하며 의미도 분명해진다. 그리고 16,17 라인에 기술되어 있는 내용은 스택의 내부 자료 구조를 의미한다. 즉 16 번 라인으로부터 이 스택은 “배열”(array)을 이용하여 구현됨을 알 수 있다. 독자들이 알고있는 바와 같이 스택을 구현하기 위해서는 “연결 리스트”(linked list)를 이용할 수도 있다. 따라서 이 스택을 연결 리스트로 구현하고 싶은 경우에는 16,17 라인에 명시된 사항만 바꾸면 된다. 마지막으로 18,19,20,21 라인에 기술된 내용은 연산들의 구현 사항을 다소 상세히 기술한 부분으로 특히 error를 유발시키는 20 라인의 비교 문은 예외 조건을 명시한 것이다.


 자료 추상화를 이론적인 관점에서 이용할 경우에는 항상 “자료 정의”, “연산자 정의”, “공리”, “예외 처리”를 모두 기술하여야 함을 알 수 있다. 그러나 자료 추상화를 실제로 구현하는 프로그래밍 관점에서는 이들이 모두 수용 가능하지는 않다.


 객체 지향 언어에서 자료 추상화를 지원하는 구조가 클래스이다. 클래스는 개념적으로는 응용 목적과 관계되는 중요한 특성들을 기술하기 위하여 객체들을 추상화시키고 중요하지 않은 내용을 무시하도록 하는 도구로서 이해될 수 있으며, 실질적으로는 관련 자료 구조와 함수를 편리하게 정의할 수 있는 문법적인 틀로서 이해될 수 있다. 클래스 구조는 요구 분석 시에 전체 모델에 대한 소규모 단위의 분할을 가능하게 한다. 아울러 기능상으로 분명하며 독립적으로 분리된 클래스는 쉽게 재사용될 수 있기 때문에 클래스는 재사용 단위로서의 의미를 갖는다. 대부분의 객체 지향 언어에서는 클래스의 정의 시에 서비스 공급자와 소비자 입장을 구분할 수 있도록 “명세”와 “구현”을 분리하여 다룰 수 있게 하는 기능을 제공하고 있다.



4. 상속


 상속은 클래스들의 관계에 있어서 공통적인 속성과 연산을 공유할 수 있게 하는 기능이다. 한 클래스는 분석 과정을 거쳐 좀더 상세하고 정제된 하위 클래스로 파생되는데 이때 하위 클래스는 상위 클래스의 속성 및 연산을 물려받는다. 상속을 사용하는 근본적인 이유는 기존에 만들어진 클래스를 재사용 하기 위한 것이다.


 상속을 이용한 시스템의 구현 시에 파생되는 하위 클래스에는 새로운 자료 구조나 새로운 연산이 추가되거나 기존 연산이 수정된다. 따라서 공통적인 자료 구조와 연산을 갖는 클래스는 상위 클래스로 만들어지며 추가되는 정보가 하위 클래스에서 반영된다.


 상속을 이용한 시스템의 구현 방식을 간단히 알아보자. 예를 들어 “서울 건설”이란 회사에서 모든 직원에 대한 전산 처리를 위하여 “Employee”라는 클래스를 만들었다고 가정하자. 그리고 이 클래스에 속하는 자료 구조를 위하여 피고용자의 “이름”, “주민등록번호”, “근무 부서”, “월급”을 만들고 연산에는 “승진하다”, “부서를 옮기다”, “퇴직하다”를 만들었다고 가정하자. 그런데 아마도 이러한 전산 시스템에서 관리직 사원을 위한 “Manager” 클래스도 필요할 것이라는 점을 이해할 수 있을 것이다. 이러한 경우에 관리자 클래스를 새로 만들기 위하여 상속을 이용하면 클래스 재사용의 이득을 얻을 수 있다. 왜냐하면 관리자도 직원의 한 가지 경우이기 때문이다. 즉 “관리자는 직원이다”라는 IS_A 관계가 성립하기 때문에 이를 프로그램의 상속 기능으로 표현하면 시스템의 이해도 쉬울 뿐더러 코딩 시에 재사용의 이득도 얻을 수 있는 것이다. “관리자는 직원이다”라는 문장의 실질적 의미는 Manager도 “이름”, “주민등록번호”, “근무 부서”, “월급”이라는 자료 구조를 Employee 클래스에서와 똑같이 가지고 있으며, 마찬가지로 Employee 와 같은 연산자 “승진하다”, “부서를 옮기다”, “퇴직하다”를 가지고 있음을 의미한다. 그렇다면 Manager와 Employee는 어떻게 다른가? 당연히 Manager에는 추가되는 자료 구조와 연산이 있기 때문에 Employee와 구별될 수 있다. 예를 들어 Manager 클래스에는 “판공비”라는 자료 구조가 추가되며 “부하 직원들에 대한 평가 보고서를 작성하다”라는 연산이 추가로 존재할 수 있다. 상속을 이용하여 Manager 클래스를 만들 경우 Employee 클래스는 “상위 클래스(super class)”, “부모 클래스(parent class)” 혹은 “기반 클래스(base class)”라 명하며 Manager 클래스는 “하위 클래스(subclass)”, “자식 클래스(child class)”, 혹은 “파생 클래스(derived class)”라 명한다. 그리고 Manager 클래스는 Employee 클래스로부터 “이름”, “주민등록번호” 등의 자료 구조와 “승진하다”, “부서를 옮기다” 등의 연산을 상속받는다고 말한다. 


 상위 클래스와 하위 클래스간에 관계는 벤 다이어그램(Venn diagram)을 이용하여 이해할 수 있다. (그림 2.5)는 Employee 클래스와 Manager 클래스의 자료 구조와 연산들의 집합이 어떠한 포함 관계를 갖는지를 보여준다.

그림 2.5 상위 클래스와 하위 클래스간의 멤버 포함 관계와 해당 UML 다이어그램


 객체 지향 과제에서 상속 관계가 차지하는 비중은 매우 막중하다. 그 이유는 상속 관계를 나타내는 트리 구조가 시스템의 골격을 형성하기 때문이다. 객체 지향 시스템이 구조적 시스템에 비하여 외부 변화에 대처하기 쉬운 유연성을 많이 제공하고 있기는 하지만 상속 구조의 변경 시에는 그렇지 않다. 프로젝트 수행 초기에 잘못 분석된 상속 트리를 프로젝트 중간에 변경하는 것은 엄청난 비용과 노력을 부담케 한다. 객체 지향 언어를 사용하는 초보자들이 범하는 실수의 대부분이 클래스의 상속 관계를 잘못 설정하기 때문이며 이 때문에 초보자들의 객체 지향 기법의 유연성에 대하여 오해하는 경우가 다반사이다. 즉 초보자들 스스로가 모델링을 잘못하여 생긴 시스템의 구조적 결함을 객체 지향 기법 자체의 문제점인 것으로 해석하는 오류를 범하기 쉽다.


 상속 구조를 잘못 설정하는 대부분 경우는 클래스들 간의 본질적 상속 관계를 이해하지 못하고 눈앞의 이익에 급급하여 파생 클래스를 만드는 경우이다. 즉 기존에 만들어진 클래스에 대하여 한, 두 가지의 자료 구조와 연산이 추가되었다고 하여 새로운 파생 클래스를 시스템에 추가하는 경우가 많은데 이는 지극히 위험하다. 상속 구조를 바람직하게 만들기 위해서는 실 객체가 갖는 본질적 특성을 이해하고 클래스간의 IS_A 관계를 인식하며 차후 변경 및 추가의 가능성을 고려해야만 한다.


+ Recent posts