무엇이든 다룰 수 있어야 하는 한국인 프로그래머와 객체지향
Korean Programmer
C++인터페이스(Interface)
미켈란젤로가 183 평방 미터나 되는 시스티나 성당의 천장 벽화를 그릴 때에도 천장의 한 구석에 조그마한 인물을 꼼꼼히 그려 넣었다. 누가 알아보겠냐는 질문에 미켈란젤로는 “내가 알지”라고 답했다. 우리는 개발을 하면서 적게는 바로 옆 동료가, 많게는 세상에 수많은 개발자들이 개발하는 라이브러리나 프로그램을 제작 할 수 있다. 물론 개발을 하면서 다른 어떠한 요소보다 주어진 상황을 좁히고 요구 명세를 수렴 시키는 것이 중요하다. 하지만 미켈란 젤로가 그러 했듯이 현재 요구사항에 딱 맞는 프로그램을 완성하는 것 만이 끝은 아니다. 제작될 프로그램이나 라이브러리에서 사용자를 고려하여 확장성에 물꼬를 틔워 주는 일은 개발자에게 있어 시스티나 성당 구석의 조그마한 인물을 그려 주는 일만큼이나 중요하다. 객체 지향 언어에서 확장성의 핵심에 있는 인터페이스의 정의를 알아보고 이의 실례를 들어보자.
필자는 현재 삼성전자 메모리사업부의 플래시 소프트웨어 개발 관련 연구원으로 플래시 메모리와 관련된 파일 시스템과 커널 드라이버들을 제작하고 있다. 컴퓨터 공학을 전공하고 Haskell과 Scheme등의 Functional language를 좋아하며, 학부 시절, C++과 관련하여 3개의 논문과 대회 수상을 한 경험이 있다. 올해 들어와서 Class와는 전혀 다른 작업을 통해 조금은 OOP와 거리가 멀어진 길을 걷고 있지만, 항상 언어에 대한 갈등은 지우지 못하는 이제 막 걸음마를 시작한 부족한 그릇의 개발자이다.
필자가 대한민국 개발자와 객체지향 이야기라는 주제로 칼럼을 쓴지도 벌써 10개월이 되었다. 2007년 마지막 기고 글에 들어가기 전에 필자의 바쁜 생활을 잘 이해 해주시고, 옆에서 너그러이 지켜 봐주신 마소
객체 지향 언어에서 고급 기술들이 하나하나 꽃이었다면 이제 그 꽃을 좀 더 편리하도록 만들어주는 기술을 마지막으로 대한만국 개발자와 객체 지향 이야기를 마무리 하고자 한다. 설계자, 사용자 모두에게 있어서 편리함을 제공하는 인터페이스 기술을 알아보고 C++에서는 어떻게 적용 할 수 있는지 그 실례를 살펴 보자.
추상 클래스(Abstraction Class)
이제껏 우리가 상속을 설계하고 또 디자인 이슈들을 살펴 보면서 만들어 내었던 클래스들을 다시 한번 정리 해보자. <그림 1>에서처럼 객체 지향 패러다임에서 클래스는 구체 클래스(Concrete Class)와 추상 클래스(Abstract Class)로 나눌 수 있다. 구체 클래스는 구현된 시스템에서의 클래스 계층도에 따라서 그것이 단말 클래스(Leaf class)인지 아니면 또 다른 상속 관계에 있는 비단말 클래스(Non-leaf class)인지로 구분 된다. 여기서 우리가 집어 보고 갈 것이 추상 클래스에 대한 고찰이다.
<그림 1> 객체 지향 패러다임에서 클래스 계층.
추상 클래스라는 용어는 때로 약간의 혼돈을 주기도 한다. 왜냐면 추상 클래스라는 용어가 두 가지 정도의 의미를 내포하고 있기 때문에 단지 추상클래스로 구현하라고 하면 설계도나 또는 코드의 문맥에서 이 추상 클래스가 갖는 의미를 찾아야 할 것이다. 추상 클래스의 가장 보편적인 교과서적인 의미는 오브젝트를 인스턴시에이션 시키지 않는 클래스를 말한다. 이러한 의미의 추상 클래스는 <그림 1>에서 쉽게 파악 할 수 있다. 추상 클래스는 Object Instance라는 객체를 통해 인스턴시에이션을 가지지 않기 때문에 구체 클래스와 달리 그 실체가 없다. 다시 말해 프로그램에서 직접적으로 객체를 만들지 않는 클래스로 단순히 클래스 계층도에서 중복되는 코드들을 떠맡기 위해 사용되거나 하위 클래스의 인터페이스를 명세 하기 위해 사용되는 클래스이다. 이렇게 사용되는 예를 살펴 보자. 우리가 흔하게 사용되는 MFC에서 추상 클래스는 CObject라는 이름으로 존재 한다.
class AFX_NOVTABLE CObject
{
public:
// 런타임 클래스를 인출, 즉 실행시에 클래스에 대한 정보를 쿼리 할 수 있다.
virtual CRuntimeClass* GetRuntimeClass() const;
virtual ~CObject() = 0; // 가상 소멸자.(가상 소멸자에 대한 이야기는 step 9를 참조)
// 메모리 관련 operator들을 오버로딩.
void* PASCAL operator new(size_t nSize);
// Attributes
public:
BOOL IsSerializable() const;
// 런타임시 클래스 식별
BOOL IsKindOf(const CRuntimeClass* pClass) const;
// Overridables
// 데이터 저장 및 로딩을 수행
virtual void Serialize(CArchive& ar);
};
<코드 1> MFC의 추상 클래스.
MFC의 설계를 살펴 보자는 것이 목적이 아니므로 조금 간추려서 그 내용을 보도록 하자. CObject는 순수 가상함수(현 코드에서는 소멸자)를 최소 한 개를 가지는 ABC이다.(개인적으로 MFC의 설계는 시대에 비하여 아주 잘 정의 된 계층도라고 생각한다. 이에 대해서는 개발자들마다 여러 의견이 존재 할 수 있지만, 필자에게 RTTI가 지원 되지 않던 시절에 그를 위한 런타임 클래스의 스킴을 넣은 것으로 보나 개발자들을 편리를 위해 시리얼라이즈와 같은 것들을 디파인하고 각기 다른 가상 함수를 클래스 위저드를 통해 폼과 연동 시킨 것은 객체지향에 대한 충분한 이해와 경험을 바탕으로 한 대가들의 훌륭한 아키텍트 결과물이라고 생각한다.) 또 시리얼라이즈와 같은 개발자를 위한 극도의 인터페이스의 의미가 훨씬 강하다는 것은 이 순수 가상함수를 통해 알 수 있다. 이러한 CObject는 추상 클래스로서 하위 클래스의 인터페이스를 명세 하는 목적을 가지고 있다. 하위 클래스의 인터페이스를 명세한다는 이야기는 CObject를 통해 파생되는 모든 클래스에서 사용 될 수 있는 메서드의 각 시그니쳐를 정의 한다라고 할 수 있다. 즉 CObject로부터 상속을 받게 되는 개발자의 모든 코드는 데이터 저장 및 로딩을 수행하기 위해서 시리얼라이즈를 사용 할 수 있고 이러한 시리얼라이즈는 아카이브를 통해서 가능 하다는 것을 미리 정의 하는 것이다.
인스턴시에이션(Instantiation)이 존재하는 추상 클래스(Abstraction class)
이상하게 들릴지도 모르겠지만, 추상클래스의 첫 번째 의미가 인스턴시에이션을 시키지 않는 일관된 메서드들의 행동에 대한 정의라고 한다면 이와 반대로 클래스를 인스턴시에이션을 시켜서 행동들을 정의 하는 추상 클래스가 그 두 번째 의미가 된다. 인스턴시에이션을 통해 추상 클래스를 지원하는 최초 언어는 Ada에서 였다. Ada에서는 상속을 통한 코드의 재사용이 불가능 하였기 때문에 추상 클래스를 통해서 코드를 재사용하는 변칙(?)기법을 가지고 있었다. 필자나 독자들이 좀 더 쉽게 이해 할 수 있는 예제는 C++의 템플릿(Template)클래스이다. 템플릿 클래스들은 그 이름 그대로 여러 종류의 클래스를 대표하는 범용적인 클래스로서 개발자의 코딩 부담을 줄여 주기 위해 존재 한다. 템플릿이 가장 잘 정의되어 사용되는 예는 STL과 같은 표준 템플릿 라이브러리 등이다. 템플릿은 Ada에서 그러하듯이 기본적으로 상속을 통한 코드 재사용으로 볼 수 없다. 예를 들어 C++경우를 생각 해보자. 범용적인 Queue를 사용하기 위해 우선적으로 Queue에 대한 클래스를 정의 한다(물론 우리가 부르는 표준 템플릿 라이브러리는 이러한 클래스 정의를 다 하고 나서 우리에게 보여주는 덩어리이기 때문에 인스턴스 클래스로서 재정의 하기만 하면 된다.) 정의된 클래스를 가지고 우리는 Queue<int>, Queue<float>, Queue<UserClass>식으로 Queue의 클래스에 인스턴스 클래스를 작성하여 사용한다. 다른 매개인자(Parameter)를 갖는 클래스(Parameterized class),범용 클래스(Generic class)와 같은 이름으로 불리기도 하는 템플릿은 이렇게 인스턴스 클래스를 작성하여 쓰는 것이 일반적인데, 인스턴스 클래스가 인스턴스 객체가 아님을 주의 해야 한다.인스턴스 클래스는 bound element라고 부르기도 하는데 추상 클래스가 타입(Type)을 인자(Argument)처럼 처리하기 때문에 인스턴스 클래스를 선언하는 행위는 인스턴스 객체가 될 수 없다. 템플릿은 우리가 처음 이야기 했던 인스턴시에이션을 하지 않는 추상 클래스와 인스턴시에이션 문제 외에 현저하게 다른 특징을 가지는데 그것은 추상 클래스와 관련되는 시스템의 작업이 모두 정적으로(Static하게) 컴파일 타임에 이루어진다는 것이다. 템플릿과 같은 추상 클래스가 컴파일 타임에 정적으로 모두 할당 되어 버린다는 것은 이를 추상 클래스로 볼 수 있는 가에 대한 질문을 남긴다. 우리가 이제까지 계속 언급해오던 폴리모피즘을 위한 동적 바인딩 시스템을 사용 할 수 없게 되고 상속과 다른 계통의 정제화과정(refinement)을 통해 코드를 재사용하게 되는 템플릿 클래스가 추상 클래스로 정의 될 수 있는 것인가 하는 의문이 생기긴 하지만 현재는 추상 클래스로 인정하고 있는 추세이다.
템플릿(Template) 형태의 추상 클래스에 대한 고찰.
C++에서 추상 클래스가 도입 된 것은 C++3.0 버전 이후 시점이다. 앞서 언급 되었던 것과 같이 C++에 템플릿 형태의 추상 클래스가 도입된 것은 템플릿의 기능이 개발자의 코딩 분량을 현저하게 감소 시키기 때문이다. 코딩 분량의 감소라는 매력과 자료구조와 특정 알고리즘을 몰라도 개발을 할 수 있다는 것은 초보 개발자에게 엄청난 메리트로 작용 할 수 있다. 하지만 우리는 여기서 간과 하지 않아야 하는 점이 있는데 그것은 코딩 분량은 줄지만 실제 코드 사이즈 즉, 오브젝트 코드의 분량이 줄어 드는 것은 아니라는 것이다. 컴파일러는 템플릿의 실체를 생성하기 위해 매크로 확장(Macro extension)방식을 이용 한다. 이러한 매크로 확장 방식은 알고 있다시피 단지 매크로를 전처리시에 교체하는 역할 밖에 하지 않기 때문에 실행 시의 이미지 사이즈는 템플릿을 사용하지 않고 잘 설계된 프로그램 보다 템플릿을 사용한 시스템이 크다. 이러한 이미지 사이즈 문제뿐만 아니라 프로젝트 운용 입장에서도 어려움을 야기 할 수 있다. UserClass나 int에 대한 큐 클래스를 제작할 때 Queue<int>, Queue<UserClass>와 같이 템플릿을 사용하여 구현 하는 것 보다 상속의 기능을 이용하여 시스템을 구성하는 것이 훨씬 유연하다. 그 이유는 int, UserClass와 같은 타입에 연관되는 연산들, 예를 들어 Insert()와 같은 연산이 수행 하는 실질적인 행위가 각 타입에 따라 이질적인 형태를 취할 수 있기 때문이다. 만약 각 타입의 클래스에서 insert와 같은 연산이 차이가 없는 비슷한 형태의 골격을 유지한다면 템플릿을 사용하여도 큰 문제가 없지만 서로 다른 양식에 따라 구현 되는 경우에는 템플릿의 사용이 도움이 되지 않는다는 것이다. 또한 템플릿에 들어가는 타입들은 서로 특별한 관련이 없어도 같은 형태의 템플릿 클래스를 통하여 구현이 되기 때문에 LSP문제를 근본적으로 떠 안을 수 밖에 없다.
더욱이 동적 바인딩을 통한 유연성을 확보하기 위해서는 템플릿 사용은 자제 되는 것이 올바르다. 템플릿은 초보 개발자에게는 마법처럼 보일 수 있으나 컴파일러가 매크로 확장을 통해서 단지 코드를 복사하는 사실이라는 것을 안다면 객체지향을 하면서 Copy & paste를 하는 C코드와 특별히 다를 바가 없다는 것을 알게 될 것이다. 실지로 템플릿을 대신에 각 클래스의 골격을 생성하거나 복사해주는 역할을 하는 CASE(Computer Aided Software Engineering) 도구 등을 통해 같은 효과를 취할 수 있다
템플릿(Template) 은 객체지향인가?
필자가 학창 시절에 한참 목에 핏대를 올려가며 이야기 했던 부분이 바로 템플릿이다. 필자는 범용컨테이너를 모르던 시절 대부분의 자료구조를 다 만들어서 사용 했다. 물론 잘 만들어서가 아니라 단지 STL이나 ATL과 같은 템플릿을 몰랐기 때문이다. (물론 현재에는 템플릿을 사용하기 싶어도 사용 할 수 없는 임베디드 영역으로 들어와 버렸기 때문에 그 마법 같은 개발 시간 단축을 경험할 수는 없다.) STL 내부를 조금이라도 공부해본 독자들은 알겠지만, STL이 얼마나 잘 만들어 졌는지를 조금이라도 경험했을 때, 필자는 소프트웨어에서 침범할 수 없는 네버랜드를 본 기분이었다.
지금도 STL과 같은 잘 만들어진 템플릿을 사용하는 것이 검증되지 않은 컨테이너를 만들어서 사용하는 것보다 훨씬 유용하고 어설픈 자료구조 및 알고리즘 보다 정확하고 명확한 퍼포먼스를 보장한다고 생각한다. 하지만 STL과 같은 모태가 되는 템플릿은 객체 지향으로 분류하기는 좀 애매하지 않는가 하는 것이 필자의 견해이다. 탬플릿 형태는 추상화 클래스에 대한 고찰에서도 이야기 되었듯, 재사용 성을 상속이 아닌 정제화를 통해 한다는 것과 정적 바인딩을 사용하지 못한다는 치명적인 약점 이외에도 객체 지향의 원류인 SmallTalk에서 템플릿을 지원 하지 않는 다는 점을 우리는 주목 해볼 필요가 있다. Smalltalk에서 템플릿을 지원하지 않는 이유는 템플릿이 없이 템플릿 보다 더 훌륭한 범용 컨테이너를 제작 할 수 있기 때문이다.
그렇다고 템플릿이 객체지향 언어에서 다중 상속의 사용 만큼 배제되어야 한다는 이야기는 아니다. 템플릿의 유용성은 ATL/COM환경에서 많은 템플릿을 통해 프로그래머의 부담을 경감 시켜 주고 있으며(물론 유사한 EJB(Enterprise Java Beans)환경에서 컴포넌트 제작이 템플릿을 사용하고 있지 않다는 점도 고려 해봐야 할 사항이다) STL과 같은 막강한 라이브러리들은 개발자가 소프트웨어에서 빈번하게 사용되는 자료구조와 알고리즘을 벗어나 좀 더 클래스간의 관계를 통한 시스템 구성을 구현하는데 도움을 준다는 점과 쉽게 개발을 할 수 있도록 도와 준다는 점에서 객체지향에서 없어서는 안될 존재 이다.
하지만 정적 바인딩과 같은 치명적 템플릿 클래스 구현 약점은 C++이 극복해야 할 문제이다. 템플릿을 지원 하지 않는다는 Java의 경우도 Generic programming이라는 타이틀 아래 Java Generics를 Java 5.0부터 이와 비슷한 기능을 지원 하고 있다. Java Generics가 타입캐스팅에 대한 문제와 컴파일 타임 시 타입 안정성을 제공하는 것과 함께 템플릿 클래스의 정보 은닉 문제(Encapsulation)에 C++과는 다른 정책을 가지고 있는 것도 C++이 앞으로 발전 할 수 있는 방향을 제시 하고 있지만, Java Generics를 사용하는 클래스들의 모든 인스턴스들은 컴파일과 런타임시 바인딩을 고려 하고 있다는 것은 매우 고무적인 사실이다.(물론 역으로 이야기 해서 Java와 같은 언어가 Java Generics라는 이름을 붙여 C++의 템플릿 기능을 흉내 냈다는 데서 템플릿의 유용성을 다시 한번 확인 해볼 수 있는 기회이기도 하다.)
객체지향에 있어서 인터페이스(Interface for OOP)
인터페이스라는 용어는 객체지향과 관련 없이 사용되기 때문에 더더욱 혼돈스러울 가능성이 있다. 일반적인 의미로 사용되는 인터페이스는 “컴퓨터 시스템을 구성하는 요소들을 연결하는 점”의 뜻이고 객체 지향과 관련하여 인터페이스는 소프트웨어를 작성하는 도구로 사용 되기 때문에 객체 지향 패러다임에서 사용되는 전자의 인터페이스와는 그 정의가 사뭇 다르다. 이전 스텝에서 언급된 바와 같이 객체 지향 문화에서는 하나의 용어가 서로 다른 의미로 사용되는 경우가 자주 있으며 객체 지향에 있어서 인터페이스도 문맥에 따라 조금씩 다른 의미를 한다.
스펙으로서의 의미
프로그래밍 시에 사용되는 인터페이스라는 용어가 가지는 하나의 의미는 API에서 사용되는 인터페이스의 의미이다. 이것은 사용자 관점에서 공급된 소프트웨어의 사용법을 나타내는 스펙과 같은 것으로 일종의 Export된 SDK 정의들과 같다. SDK(Software Development Kit)는 특정 플랫폼에서 응용 프로그램을 개발 하기 위한 함수 집합을 의미하는데 라이브러리 형태로, 제공되는 함수의 용도 목적, 사용법을 말하며 윈도우에서는 Win32 API들이 그러한 예이다. 이러한 스펙은 소프트웨어의 공급자와 소비자를 분리 함에 있어서 구현을 소비자의 사용 측면으로부터 분리하여 감춤에 그 목적이 있고 이러한 정보 은닉을 전제로 한 스펙 생성은 대규모의 프로젝트를 이행하거나 관리 할 때 매우 유용하게 쓰인다.
즉 객체 지향에 있어서 인터페이스라고 함은 어떠한 클래스의 스펙(Specification)라는 용어 대용으로 볼 수 있다.
View의 관점
시스템을 개발 하거나 설계를 하다 보면 특정 객체가 가지는 속성 이외에 몇몇 사용자가 그 객체를 바라보는 뷰(View에 의해) 특별한 명세를 제공 해야 하는 경우가 있다. 그 이유는 특정한 객체가 자료 추상화의 원칙에 의해 원래 실 세계에서 가지는 모든 속성과 행동을 정의 하지 못하기 때문이다. 이렇게 어떤 객체가 속하는 클래스에 정의된 기능 이외에 특정한 기능이 필요 할 때 사용되는 추상화 도구가 바로 인터페이스이다.
예를 들어보자. 필자는 개발자로서 직장인이라는 계층 군에 들어가고, 직장인은 봉급을 받거나 일을 하고 문서를 작성하는 행위등이 정의 되어 있다. <그림 >에서 처럼 사람으로 상속을 받아 일을 하는 개발자라는 클래스가 비로서 필자의 클래스가 될 것이다. 하지만 만약 독자들이 필자를 본다면 C++에 대한 글을 쓰는 개발자 정도와 글을 쓰기 위해 swblog.net이라는 소프트웨어 블로그를 운영하는 개발자정도의 특정한 요구명세가 생길 수 있다. 그렇다고 해서 글을 쓰거나 블로그를 운영하는 메서드를 개발자 클래스에 삽입 할 수는 없다. 왜냐하면 모든 개발자가 글을 쓰거
나 블로그를 운영하지는 않기 때문이다.
<그림 2> 개발자로서 Interface가 고려된 모델.
<그림 2>을 보고 필시 어떤 독자들은 어? 다중 상속인데, 라고 이야기 할 지도 모르겠다. 바로 이전 “스텝 9”에서 다중 상속의 사용시 그 모델을 사용하지 않는 쪽으로 권고 했기 때문에 더욱 의아해 할 수 있다. 하지만 <그림 2>는 스텝9에서 논의 되었던 다중 상속과 본질자체가 다르다. <그림 2>는 자바의 인터페이스를 통한 다중 상속처럼 이해 될 수 있는데 이에 대해서는 상속에 대하여 구현 상속과 인터페이스 상속, 이 두 가지에 대한 본질적 의미를 이해해야 한다.
구현 상속은 일반적 의미의 상속과 일맥상통하는데 상속을 통하여 자식 클래스가 부모 클래스의 모든 속성과 메서드 내용을 물려 받는 것을 이야기 한다. 이러한 경우는 사실 구현 모두를 물려 받는 것으로 볼 수 있다. 자식 클래스가 인스턴시에이션이 이루어짐과 동시에 부모 클래스의 데이터 멤버들이 모두 생성되고 생성자에 의해 초기화 되며 메스들의 구현 부분도 그대로 상속되기 때문에 자식 클래스에서 특별히 상위 클래스의 메서드를 오버라이드 하지 않더라도 상속받은 메서드를 그대로 사용 할 수 있다. 이에 반해 인터페이스 상속은 상위 클래스의 데이터 멤버와 메서드를 모두 물려 받는 것이 아니고 부모 클래스의 명세만을 물려 받는 것이다. 명세만을 물려 받는 다는 것은 앞서 말한 스펙의로서의 인터페이스 의미에서 언급 하였듯이 부모 클래스에서 정의된 멤버함수들의 시그니쳐만 물려 받는 것으로 자식 클래스에서는 반드시 부모 클래스에서 정의된 메서드들을 구현 해주어야 한다.
인터페이스 상속과 구현 상속 일부를 결합한 <그림 2>의 계층도는 개발자와 필자라는 두 가지 클래스를 구현 상속으로 이용하는 것이 아니기 때문에 다중 상속이라고 볼 수 없다. 저번 스텝에서도 잠시 언급 하였듯이 JAVA나 C#이 다중 상속을 사용하기 위해 인터페이스를 도입했다라고 소개 하거나 인터페이스를 통해 다중 상속을 구현 할 수 있다는 말은 이러한 이유에서 잘못된 설명임을 알 수 있다.
범용 자료구조 구현을 위한 인터페이스의 사용
자료구조를 사용 함에 있어서 우리는 무수한 고민들을 해왔다. 왜냐하면 자료 구조가 행동해야 하는 정의는 특별하게 변함이 없이 일정한데 이 자료 구조의 속성이 계속 변화하기 때문에 우리는 같은 요구사항을 가지는 자료 구조에 대해서도 여러 가지 구현을 생성 해야 했다. 이 문제를 다시 생각 해보자. 본질적으로 어떤 자료구조의 행동을 정의 하는데 있어서 자료구조가 다루어야 하는 속성이 미리 정해 져야 하는 것이 올바른가?
C++은 자료 구조의 행동에 대한 정의와 속성을 분리를 템플릿이라는 기술을 통하여 아주 깨끗하게 처리 하였다. 이러한 템플릿의 사용은 범용적인 자료 구조를 구현 하는데 있어서 매우 편리하게 동작 한다. 하지만 앞서 언급 했듯이 만약 Queue와 같은 범용 자료구조에 정수(int), 실수(float), 스트링(string), 사용자 클래스등이 계속 번갈아 가면서 입력 되어야 한다면 어떻게 하겠는가? 템플릿과 같은 방법으로는 이를 해결 할 수 없다. 인터페이스를 사용한다면 템플릿과 달리 동적으로 정수, 실수, 스트링, 사용자 클래스등을 다룰 수 있는 Queue를 정의 할 수 있다.
우선 Queue의 기본적인 행동을 정의할 구체 클래스(Concrete Class)를 생성하고 이 구체 클래스가 처리할 내부 원소인 Entry를 인터페이스로 구현하게 되면 Entry을 상속하여 각 실수, 스트링, 사용자 클래스 등을 구현하면 완전한 범용 자료 구조가 만들어 질 수 있다. 우선 인터페이스를 사용 할 수 있는 자바를 통해서 이러한 Queue를 생성해보자.
//
// 범용자료구조로클래스의속성을행동으로부터분리
//
interface QueueEntry {
public QueueEntry GetNextEnt();
public void SetNextItem(QueueEntry qEnt)
};
//
// 미리정의된인터페이를통하여행동만분리정의
//
class Queue {
QueueEntry tailEnt;
QueueEntry headEnt;
public Queue()
{
tailEnt = null;
headEnt = null;
};
public Push(QueueEntry qEnt)
{
if(tailEnt == null)
{
headEnt = qEnt;
tailEnt = qEnt;
}
else
{
tailEnt.SetNextItem(qEnt);
}
}
public QueueEntry Pop()
{
QueueEntry qEnt = headEnt;
if(headEnt == null)
{
java.lang.System.exit(-1);
}
headEnt = headEnt->GetNextEnt();
return qEnt;
}
};
<코드 2> 인터페이스를 이용한 범용 Queue의 Java 코드
Java의 코드라고 하더라도 인터페이스를 통하여 제작한 QueueEntry라는 자료 구조와 Queue라는 알고리즘이 분리 된 것을 알아 보는데 아무런 문제가 없을 것이다. 우리는 Queue라는 클래스를 사용 하는데 있어서 특별한 자료 구조 타입을 고려 하지 않고 일관적인 방법을 통해 Push, Pop과 같은 행동을 취할 수 있다.
CBSE(Component Based Software Engineering)에서의 Interface사용
C++의 인터페이스 응용으로 넘어가기 전에 인터페이스가 빛을 바라고 있는 부분을 마지막으로 살펴 보자. CBSE는 사실 그 범위가 객체 지향 패러다임에서 언급 할 수 있는 범위를 넘어선다. 하지만 COM, CORBA, EJB와 같은 컴포넌트 기술에서 인터페이스가 어떤 의미를 가지는 지 한번 상기 시킬 필요는 있다. CBSE에서 인터페이스는 클라이언트가 콤포넌트 객체의 서비스를 엑세스 할 수 있는 수단을 제공하고 한다. 서버로 동작하는 컴포넌트는 그 컴포넌트가 제공해야 하는 인터페이스들을 실제 구현 해야 한다. 객체 지향 언어에서 말하는 구현과 유사하다. 이와 반대로 클라이언트로 동작 하는 컴포넌트들은 서버 컴포넌트의 인터페이스를 기초로 응용, 작성되어야 한다. 인터페이스를 이용하는 기법들은 COM, CORBA, EJB에서 서로 유사한 부분을 가지고 있다. 우리가 추상 클래스로 인터페이스를 명세하는 것과 유사하게 CBSE의 컴포넌트 기술들은 IDL(Interface Definition Language)를 통하여 명세 한다.
[Object, uuid(40F4F436-8A41-4FF2-95C4-A1FDF60A152D)]
Interface IMicroSorftware : IUnkown {
Import “unknown.idl”;
HRESULT GetPaper([out] short * result);
HRESULT SendPaper([in] short input);
…
}
<코드 3> COM, IDL의 일부
<코드 3>에서 명세 된 IMicroSoftware 인터페이스를 구현하는 서버의 경우 <코드 3>에서 정의된 GetPaper와 SetPater의 함수를 통해서 그 행동을 정의 할 수 있다. 이 컴포넌트를 개발하는 개발자는 구현 내역을 클라이언트에게 전혀 공개 할 필요가 없다. 물론 이를 반대로 이야기 하면 이 컴포넌트를 사용하여 개발하게 되는 클라언트 개발자는 IDL외에 관련된 구현 내역을 전혀 알 필요가 없고, IDL만을 가지고 개발을 하게 되며 서버 컴포넌트 구현 시에 사용되었던 헤더 파일이나 구현된 바이너리 코드와 링크할 필요가 전혀 없다. 이러한 점에서 볼 때 CBSE에서 인터페이스 사용은 정보 은닉의 수단으로서의 의미가 강하다. (물론 이 부분도 많은 논란의 여지가 있으나 개발자 입장에서 봤을 때 가장 현실적인 의미가 정보 은닉 수단 인 것 같다.) 실질 적인 사용자 입장에서는 CoCreateInstance, QueryInterface와 같은 메서들을 제외 하면 객체 지향 프로그램을 작성 하듯이 코딩 할 수 있다. 예를 들면 IMaso->GetPaper와 같은 식으로 말이다.
여기서 이야기 하고자 하는 것은 COM, EJB와 같은 CBSE를 논하자는 것이 아니라 컴포넌트 기술이 인터페이스를 이용하여 컴포넌트가 제공하는 서비스를 외부에 공개하므로 인터페이스 사용법에 친숙해질 필요가 있다는 것이다. 대부분의 전문가들은 앞으로 컴포넌트를 기술을 이용한 개발이 보편화 될 것으로 예상하고 있고, 이 기술에 필수 요소가 인터페이스라는 점은 여러 C++개발자들에게 좀 더 고무적인 일이 될 수 있다.
C++에 있어서 인터페이스(Interface)
잠시 쉬어가는 의미로 이 질문에 대하여 대답 해보자. “여러분은 C++로 시스템을 작성하면서 몇 개의 인터페이스를 정의하였는가?”. 최소한 이 글을 읽고 있는 독자라면 1개, 2개, 또는 “음, 나는 사용자를 위해 C++에서 많은 인터페이스를 열어 줬는데”라는 답변은 나오지 않아야 한다. 왜냐하면 정확히 이야기 하여 C++은 인터페이스를 가지고 있지 않기 때문이다.
C로부터 개발을 진행하여 C++로 전향하게 된 개발자뿐만 아니라 대부분의 C++ 개발자들은 인터페이스 사용에 대한 포괄적인 지식이나 개념 이해를 가지고 있지 않은 경우가 많다. C++ 프로그래밍 시 Java에서와 같이 인터페이스에 대한 지식 및 개념 이해가 없이도 충분히 프로그램이 가능하기 때문이다. 우리의 C++은 왜 인터페이스를 사용하지 않은 것일까?
“객체 지향에서의 인터페이스” 절에서도 이야기 하였듯이 사물에 대한 특정 View를 제공하는 인터페이스 사용은 객체 지향에서 매우 멋진 도구임이 틀림 없음에도 불구하고 C++에는 인터페이스를 사용하지 않고도 코딩이 가능하다.
C++에는 인터페이스라는 도구가 채택되지 못했는데 굳이 변명을 늘어 놓으라면 C++이라는 언어의 태생 자체의 결함이라고 할 수 있다. ‘스텝 1’에서 이야기 했던 적이 있는데, C++의 초기 설계 목적은 C 언어를 기반으로 하는 객체 지향 언어를 만드는 것이 아니었다. 스트로우스트럽(Stroustrup)이 C++을 처음 릴리즈할때는 1980년도 벨 연구소에서 단지 C언어에 자료 추상화 기능을 추가 하고자 한 것이다. 이때 C++의 이름은 “C with Data Abstraction”이다. (1980년에서 27년이 지났는데도 아직까지 몇몇 개발자는 C++을 C With Data Abstraction처럼 사용 하고 있다는 사실이 C++이 객체 지향에 대한 개념이 얼마나 성숙해지기 전에 C개발자에게 개발 되어 보급되었는지를 알 수 있다.) C++은 Java처럼 언어의 구현 목적이 객체 지향 패러다임을 구현하기 위한 목적으로 만들어 진 것이 아니라 구조적 언어에 추가로 객체 지향 패러다임을 적용한 언어이기 때문에 인터페이스의 도입이 이루어지지 않았고 그 시기가(C++ Release 3.0이 배포 되는 1990년)너무 일렀기 때문에 인터페이스의 도입이 이루어지지 않았다. C++을 사용하면서 인터페이스를 사용하지 않고도 충분한 시스템을 제작 할 수 있는 것은 코딩을 통해 문제를 해결 할 때 반드시 인터페이스를 사용 할 필요는 없기 때문이다. 그도 그럴 것이 C와 같은 절차적 언어를 사용 하더라도 단지 그 표현 방법이 다를 뿐이지 구현에 문제가 되는 것은 아닌데다가 C++은 절차적 언어를 그 태생으로 하고 있기 때문에 절차적 방법을 충분히 이용 할 수 있다. 한술 더 떠서 C++에서는 다중 상속이라는 것을 지원 하기 때문에 이를 이용하여 여러 가지 뷰를 생성 할 수 있다.
템플릿을 사용 하지 않고 앞서 구현한 Java의 Queue를 C++로 구현 한다면 어떠한 식으로 표현 해야 할까?
전자에서 언급 되었듯이 C++에서는 Java에서와 같이 Interface와 같은 도구가 존재 하지 않기 때문에 Java에서처럼 명시적으로 Interface를 만들 수 없다. 하지만 언제나 그랬듯이 C++은 지원하지 않는 기능에 대하여 모방할 수 있는 기초 모델들을 지원 하는데 Interface를 구현 하기 위하여 C++에서는 가상 함수와 다중 상속을 이용 할 수 있다.
class QueueEntry {
public :
//
// 순수가상함수로서인터페이스기능을수행하도록함.
//
virtual QueueEntry* GetNextEnt() = 0;
virtual void SetNextEnt(QueueEntry *qEnt) = 0;
};
class Queue {
private :
//
// 인터페이스사용을위한포인터.
//
QueueEntry *headEnt;
QueueEntry *tailEnt;
private :
Queue();
~Queue();
public :
virtual void Push(QueueEntry* qEnt);
virtual QueueEntry* Pop();
};
<코드 4> Java의 범용 Queue를 C++로 옮긴 예
먼저 Interface로 정의될 클래스의 메서드들을 전부 순수 가상 함수로 선언한다. 우리가 알아야 할 것은 순수 가상 함수를 사용하여 만든 Queue 클래스는 명시적으로 인터페이스를 사용 한 것이 아니라 단지 인터페이스를 모방한 모델링의 한 방법이라는 것이다. 몇몇 C++ 개발자들은 순수 가상 함수를 선언 할 때 메서드 뒤에 붙게 되는 “=0” 을 오인하여 순수 가상 함수가 몸체가 없어야 하는 것으로 알고 있는 경우가 있다. C++에서 순수 가상 함수는 대부분이 구현을 하지 않지만 구현을 못하는 것은 아니다. 순수 가상 함수를 꼭 구현 하는 경우가 있는데 그것은 순수 가상 함수로 구현된 소멸자 이다. “추상 클래스 “절에 소개 되어 있는 CObject의 ~CObject가 좋은 예이다.
C++의 순수 가상 함수의 정확한 뜻은 순수 가상 함수를 선언이 있는 클래스는 추상 클래스이고 이 추상 클래스로부터 상속을 받아 구체화를 하게 되는 구체 클래스는 선언된 순수 가상 함수를 일반적인 가상 함수로 반드시 구현 해주어야 한다는 것이다. 그런 의미에서 순수 가상 함수를 사용한 인터페이스 구현하는데 있어서 좋은 도구로 사용 할 수 있다.
범용 자료 구조를 가지는 Queue의 바디는 아래와 같은 코드 구조를 가질 것이다.
void Queue::Push(QueueEntry* qEnt) {
if(headEnt == NULL)
{
headEnt = qEnt;
tailEnt = qEnt;
}
else
{
tailEnt->SetNextEnt(qEnt);
}
}
<코드 5> 범용 Queue의 Push
보시다시피 코드 자체는 Java의 Interface구현과 특별한 차이 없이 이를 구현 할 수 있다.
그렇다면 구현상속만으로도 충분히 시스템을 갖출 수 있는 C++에서 이러한 인터페이스를 사용 할 것인가? 하는 문제가 있다. 가상 함수와 다중 상속을 이용하여 인터페이스를 모델링하고 이를 사용 하는 것이 얼마나 이득일 것인지는 이를 사용할 개발자와 독자의 몫이다. 하지만 분명한 것은 여러분이 다른 개발자에게 제공해야 하는 코어나 엔진등을 제작 한다면 그것은 템플릿과 같은 형태를 통해서 확장을 해주는 것보다 인터페이스를 통해서 확장의 물꼬를 터놓는 것이 훨씬 바람직하다.
맺음말
그간 대한민국 개발자와 객체 지향 이야기를 지켜 봐 주신 독자 여러분, 감사 드립니다. 지금 이 시간에도 똑딱거리는 초침과 수많은 고민을 함께 해결 하고 계실 많은 개발자분 들에게 10개월 동안 부족하디 부족한 글을 쓰게 된 것을 송구스럽게 생각합니다. 칼럼에서 언급한 내용이 C++의 모든 기술들을 서술 하지는 못했지만, C++을 좀 더 객체 지향스럽게 사용하는 것에 대하여 조금이나마 서로 고민해보는 좋은 기회가 되었을 것으로 생각합니다. 사실 C++의 달콤한 기교들은 실제로 바쁜 일정 속의 업무에서 구현, 그 뒤편으로 사라지기 쉽습니다. 더욱이 C++ 다운 C++은 C언어가 주류를 이루는 임베디드 개발자에게는 더더욱 먼 이야기일 것입니다. 하지만 그러한 개발 환경도 서서히 변화를 거쳐 갈 것이라는 사실은 틀림이 없습니다.
불과 얼마 전, 코볼이나 포트란이 주류를 이루던 시절에는 C언어가 주류가 될지, 그 누구도 예상하지 못했습니다. 기존 언어들을 통해서 만들어진 많은 시스템들이 새로운 언어를 통해서 다시 태어나는 것은 그 구현의 어려움 이외에도 이제까지 구축 되어온 시스템의 보장된 안정성의 확보, 그리고 관련자들의 밥그릇 싸움 등 여러 가지 이유에서 어려웠을 것입니다. 이러한 이유에서 C언어는 오브젝트 링크 수준에서 타 언어와의 호환을 무기로 예전 시스템을 변경하지 않고 C를 쓸 수 있게 하여 입지를 굳혀 왔으며 결국 현재에는 코볼과 포트란과 같은 언어들을 우리에게 있어 추억 속의 한 장면으로 만들고, 대부분의 많은 시스템들을 장악 했습니다. C++ 또한 마찬가지입니다. C언어가 시스템 구현에 있어 조금씩 입지를 높여갔듯이 C++도 객체 지향이라는 여러 가지 설계의 기교들을 감추고 C언어와 완전한 호환을 통해서 커뮤니티의 분쟁 없이 현재 많은 시스템을 장악해 가고 있습니다. C++이 C의 자리를 밀어 내고 어느 정도 입지를 굳힌 다음 조금씩 C++만의 특징들을 추가 해오면서 완성 된 것이 오늘날의 C++입니다. 오히려 C가 C++의 문법 일부를 가져오는 등 많은 변화를 가졌지요. (예를 들면 // 과 같은 주석의 변화를 말합니다.)
C++이 성장 해왔듯 C++을 사용하지 못하는 시스템 영역에서의 개발자의 역할도 이와 마찬가지라고 생각합니다. C언어 만을 통하여 각 모듈간의 정의를 Object단위로 수행 할 수 있으며 (물론 객체지향의 고급기술들을 완전히 쓸 수는 없지만) C++과 같이 프로젝트 안에서 서로간의 역할을 나누고 정확히 추상화 할 수 있습니다. 주변에서 이런 아키텍트들을 쉽게 만나 볼 수 있는데 가장 큰 예가 GTK나 WDM과 같은 레이어드 아키텍트(Layered Architect)이겠지요. 더욱이 지금처럼 임베디드 영역에 ALU나 기타 Peripheral들의 속도가 무섭게 발전하고 임베디드 영역의 프로젝트가 거대해지면서 유지 보수의 중요성이 두각 되는 시점에서 각 모듈간의 행동이 정확히 정의 된 여러분의 코드는 반드시 언젠가 빛을 바랄 것을 믿어 의심치 않습니다.
물론 개발자에게 있어서 중요한 자질은 객체 지향적 패러다임을 익숙하게 다루기 보다는 문제를 정확히 정의 하고 처리 할 수 있는 능력과, 구현을 통하여 하나의 프로그램을 제작하기에 앞서 자신의 코드가 얼마나 빠를 지 퍼포먼스를 예측하고 관리하는 능력 등 훨씬 중요한 부분들도 있습니다. MS사에서는 이러한 SDE계층(Software Develop Engineering)의 역량에 대해서 정확히 정의 해두고 있습니다. 하지만 그러한 논리적 사고 이외에도 실제 프로젝트를 운용 하는데 있어서는 유지 보수, 요구 사항 관리, 프로세스간의 정확한 문서, 확장성을 고려한 설계 등, 개발자들이 할애 해야 하는 많은 노력과 비용들이 산재해 있습니다. 개발 시간을 단축 하고 좀 더 개발자가 개발에 몰두 할 수 있도록 많은 소소한 작업들을 대신 해주는 객체 지향 언어를 사용 하면서 이를 단지 추상화 도구로만 사용 할 것이 아니라 그 숨은 기능들의 의미를 제대로 파악하고 사용하는 것이 우리 개발자의 몫이 아닐까 합니다.
마지막으로 항상 저에게 있어서 촉매제 역할을 해주시는
참조 :
Addison Welsey, Scott Meyers. More Effective C++
배움터, 김태균저, K교수의 객체지향 이야기
'Drafts > C++' 카테고리의 다른 글
| [STEP 10] C++인터페이스(Interface) (0) | 2008/06/05 |
|---|---|
| [STEP 9] 상속구현 #2, More inheritance (0) | 2008/03/01 |
| [STEP 8] Interitant impelemetation (0) | 2008/02/06 |
| [STEP 7] Inheritance design 2 #2 (0) | 2008/02/06 |
| [STEP 7] Inheriance design 2 #1 (0) | 2008/02/06 |
| [STEP 6] Inheritance design 1 #4 (0) | 2008/02/06 |