검색을 통해서 들어오시는 분들이 계시길래, 새로 정리하여 포스트를 올렸습니다.  아래 포스트를 먼저 읽고 오시기 바랍니다.


------------------------------------------------------------------------------------------------------

다형성은 요구사항 변화에 대한 소스 코드의 충격을 완화하는데 상당히 중요한 도구 입니다.  그리고, 이것을 구현하기 위해서는 클래스 상속과 인터페이스 구현이라는 방식을 사용하게 됩니다.  (다른 방법이 전혀 없는 것은 아니지만)  그런데, 그 둘의 차이점은 무엇일까요?  구구 절절하게 쓰면, 이해하기 어려워 질 수 있으니 중요한 차이점만 살펴보도록 하겠습니다.

복수 개의 메소드가 서로 유사성을 지녔다면, 다형성으로 이를 통일하는 것이 효율적일 경우가 대부분 입니다.  그렇지 않다면, 객체의 타입에 맞춰서 조건문을 통해서 호출하는 수 밖에 없습니다.  이러한 조건문들은 변화에 민감합니다.  결국, 유사한 기능들을 동일한 호출 방법으로 통일하면, 객체의 타입에 따라 코드가 변경되는 것을 방지할 수가 있습니다.  그리고, 유사성을 간직한 객체들은 같은 클라이언트 객체를 통해서 접근 될 가능성이 아주 높습니다.

[표 1] 클래스 상속과 인터페이스 구현의 차이점

Class

Interface

코드의 공유 제공

코드의 공유를 제공하지 않음

다중상속을 지원하지 않음 (일부 언어 제외)

다중상속이 가능 함


음..  너무 간단하게 정리했나요?  구태여 정리하지 않아도 이정도는 아시는 분들이 대부분일 겁니다.  자, 그럼 이 차이점이 대체 어떤 의미를 가지게 될까요?  아래와 같이 두 가지의 논점을 통해서 설명하도록 하겠습니다.  
  • 변화의 파급 효과와 변화의 자동 적용
  • "Is-a" vs "Can-do"

인터페이스의 다중상속은 진정한 의미의 다중상속으로 보기는 어렵습니다.  저는 인터페이스 구현을 상속으로 보지 않고, 인터페이스 위임이라는 형태로 간주합니다.  이 포스트의 제목에서부터 상속과 인터페이스 구현을 의도적으로 구별해서 사용하였습니다.

나는 태생적으로 그런 인터페이스를 제공 할 수 없어!
하지만, 그런 기능을 제공 할 수 있는 능력은 있어!
그러니까, 네가 나 대신 인터페이스를 제공해 주는 리모콘이 되어줘!


변화의 파급 효과와 변화의 자동 적용
코드 공유를 통해서 얻어지는 이점은 중복 코드의 제거와 변화의 자동 적용 입니다.  그리고, 이것으로 인해서 얻어지는 부작용이 바로 변화의 파급효과 입니다.

변화에 대한 자동 적용이라는 것은 간단합니다.  [그림 1]에서 기반 클래스에 해당하는 관리자의 "로그인" 메소드가 원래는 오라클 데이터베이스를 이용해서 구현되었다가, MySQL로 변경되었다고 가정하겠습니다.  이때, 관리자의 클래스의 "로그인" 메소드를 수정하는 것만으로 모든 하위 클래스들은 그 변화를 그대로 적용 할 수가 있습니다.  또한, 만약에 관리자가 공지를 올릴 수 있도록 기능을 추가한다고 했을 때, 역시 관리자 클래스에 "공지작성"이라는 메소드만 추가하면 모든 종류의 관리자 클래스에 바로 적용이 됩니다.  

[그림 1] 쇼핑몰 관리자에 대한 Class Diagram

그런데, 이러한 이점이 항상 즐거운 것만은 아니라는 것이 문제입니다.  만약 업무 특성상 제품관리자는 공지를 작성 할 수 없다면 어떻게 될까요?  제품관리자는 해당 메소드를 호출하지 못하도록 다른 조치를 해야 할 필요가 생기게 됩니다.  

  "내부에 권한 플래그를 두면 되지."
  "상속 받은 쪽에서 override를 통해서 무시하거나 Exception을 발생 시키면 되지."
라고 생각하는 분이 혹시라도 계신다면, 이건 다소 위험한 발상입니다.  이에 대해서는 나중에 여유 있을 때 정리하도록 하겠습니다.  (제가 잊어 먹고 지나가지 않는다면 ㅡ.ㅡ)

기반 클래스의 변화가 모든 파생 클래스에게 영향을 주기 때문에, 설계 시 고려하지 못했던 요구사항의 변화가 왔을 때, 그로부터 파생 된 모든 클래스에게 그 영향이 파급된다는 점은 심각한 문제가 될 수 있습니다.  또한, 리스코프 대체의 원칙을 위반하게 됩니다.

가장 심각한 문제는 어떤 변화가 있을 지 모르기 때문에 이것을 효과적으로 대처 할 수가 없다는 것입니다.

정리하면, "코드 공유를 통해서 얻어지는 이점이 있는 가 하면, 그에 따른 부작용도 있다" 입니다.

일단 클래스의 상속은 생각보다 비용이 큽니다.  상속보다는 위임을 사용하라는 말을 수 없이 듣게 됩니다.  그리고, 다형성이 필요할 때도, 클래스 상속보다는 인터페이스 구현을 사용하라고 합니다.

OOP에서 상속은 일단 피할 수 있으면 피하는게 유리합니다.


"Is-a" vs "Can-do"
그럼 어떻게 할까요?  상속은 무시하고 인터페이스 구현만으로 코딩을 해야 할까요?  결론은 상황에 따라서 적절한 선택을 해야 한다는 것 입니다.  참으로 무책임한 말이긴 하지만, 어쩔 수 없는 현실입니다.

그 선택을 하는데 가장 중요한 기준이 바로, 두 객체 사이의 공통점이 같은 종(種)으로서의 유전적인 유사성이냐 또는 종(種)과 상관없이 후천적인 획득 형질의 중복이냐를 구별하는 것 입니다.

말이 어려워졌으니 다시 설명을 드립니다.  설계 이후부터 기반 클래스의 변화가 파생 클래스에게 그대로 이어져도 상관없을 듯 한 자연스러운 유사성이라면 이것을 유전적인 형질과 동일하게 취급할 수 있다는 뜻 입니다.  또, 다시 첨언 한다면, 설계가 튼튼해서 앞으로도 크게 문제가 없을 듯 한 것들은 상속을 이용한다고 해도 큰 문제가 없고, 오히려 상속을 사용해야 코드가 간결해지고 효율적이라는 이야기 입니다.

처음 설계 할 때에는 전혀 고려 할 수도 없었던 변화가 생기거나 예측이 된다면, 상속보다는 인터페이스를 사용해야 하고, 이 것들은 후천적인 형질, 즉, 후천적인 문제로 간주 할 수 있다는 뜻 입니다.  

상속을 받아서 따로 추가한 기능과 같은 후천적인 문제는 아무런 문제가 될 것이 없습니다.  그냥 그렇게 하면 됩니다.  문제는 이러한 것들이 중복되는 경우입니다.  즉, 복수의 객체가 자신의 종과 상관이 없는 기능이 추가되었는데, 이것이 서로 중복이 될 경우 입니다.

"그 기반 클래스와 부류(파생 클래스)들은 당연히 그래" 라고 할 수 있다면, 그 모든 클래스들은 같은 종이 됩니다.  그리고, 그 기반 클래스와 파생 클래스의 관계는 "Is-a"와 같은 관계에 있다고 할 수 있습니다.  동일한 종(種)으로 취급을 받는 것 입니다.  

"Is-a" 관계를 나타내는 것은 상속이 자연스럽습니다.

예를 들면, 나도 사람이고 아내도 사람입니다.  우리는 같은 종(種)입니다.  그래서, 배도 고프고, 짜증을 낼 때도 있고, 아프기도 합니다.  이런 것들은 너무나 당연한 유전적인 형질들입니다.  상속되는 것이죠.  그런데, 저는 프로그래밍을 할 수 있지만, 아내는 컴퓨터 다루는 것 자체를 어려워 합니다.  그리고, 제 동료들은 역시 프로그래밍을 잘 합니다.  이러한 형질들은 사람이니까 당연히 할 수 있는 것이 아닙니다.  획득된 형질들이며, "Can-do"로 분류 할 수 있는 것 들 입니다.

"Can-do" 의 특징을 같는 것들은 인터페이스 구현이 자연스럽습니다.

문제는 소프트웨어 개발의 관점에서는 경우에 따라서 유전적 형질에 대한 판단이 달라진다는 것 입니다.  만약 프로그래머가 핵심 관점이라면, 프로그래밍 능력은 유전적인 형질로 취급 될 수 있습니다.  더구나, 요구사항 변화로 인해 다양한 관점에서 접근하게 된다면, 이러한 기준이 지속적으로 흔들릴 수도 있습니다.  이처럼 현실과 코드 사이의 차이는 상당합니다.  이부분은 설계자의 재량이 필요한 부분이라고 할 수 있겠습니다.

물론 다중상속을 통해서 해결할 수도 있습니다.  하지만, 다중상속은 단일상속에 비해서 코드를 어렵게하는 단점이 너무 뼈 아픕니다.  더구나, 다중상속을 지원하지 않는 언어가 상당히 많기 때문에 완벽한 해결책이라고 할 수 없습니다.


이제 기본적인 원리를 설명했으니, 간단한 예제를 통해서 살펴보도록 하겠습니다.

[그림 2] 요구사항 분석 이후

우선 [그림 2]는 요구사항 분석 이후에 발견된 객체들입니다.  유사성이 보입니다.  그런데 이것을 상속으로 처리할 지, 위임으로 처리할 지 또는 인터페이스 구현을 할 지는 설계자 마음입니다.  어떤 것이 무조건 옳고 그르다고 이야기 할 만한 근거가 아직 없습니다.

설령 근거가 충분하다고 해도, 반드시 옳은 해법과 설계란 없습니다.

[그림 3] 중복 코드의 정규화 이후

[그림 3]에서는, 각 객체의 중복 코드를 제거하기 위하여 새로운 클래스를 생성하고 중복되는 "사료먹기"를 기반 클래스로 옮겼습니다.  사료먹기는 애완동물의 자연스러운 유전적 형질이라고 판단하여 상속을 이용하기로 했습니다.  부디 판단이 틀리지 않기를 기도합시다 ^^;

[그림 4] 새로운 요구사항의 발생

[그림 4]에서는 새로운 요구사항 "기어다니기"가 생겨났습니다.  그런데, 이것들은 유사성을 보이는 클래스가 존재하기는 하지만, 전체에 그 영향이 미치지 않고 국소적입니다.  즉, 후천적 형질입니다.  이것을 기반 클래스로 옮기게 되면, 리스코프 교체의 원칙에도 위반되고, 유전적 형질이라고 선언했지만, 이를 거부해야 하는 파생 클래스가 생기는 모순이 발생합니다.

[그림 5] 쉬운 해결책

[그림 5]에서는 간편한 해결책으로 한 단계 더 상속의 깊이를 더했습니다.  이 방법이 당장 문제라고 지적하기는 어려울 수도 있겠지만, 상속을 남발하면 유지 보수 비용도 커지고, 소스 코드가 복잡해질 수 있습니다.

[그림 6] 후천적 형질은 인터페이스로 구현하자

[그림 6]에서는 인터페이스 구현을 통해서 후천적 형질의 유사성을 관리하고 있습니다.  금붕어와 같은 수중동물의 경우에는 "헤엄치기" 기능에 대한 요구사항이 늘었음을 알 수 있습니다.  이제, "사료먹기"는 애완동물의 유전적인 특징이라고 할 수 있고, "기어다니기" 등은 후천적인 특징으로 강아지와 고양이는 육상동물행동이 제공하는 인터페이스의 기능을 수행 할 수 있어라고 표현 된 것 입니다.  (Is-a vs Can-do)

[그림 7] 더욱 추가된 요구사항

[그림 7]에서는 새로운 애완동물인 오리가 나타났습니다.  오리는 육지를 기어다니거나, 헤엄치기를 할 수가 있습니다.  그래서, 자신이 할 수 있는 "Can-do"의 인터페이스를 육상동물행동 인터페이스와 수상동물행동 인터페이스에게 위임하여 다른 객체에게 제공하게 됩니다.

이해를 돕기 위해서 간단히 지나간 것이 있습니다.  위의 클래스 다이어그램을 보면, 중복이 발견되고 있습니다.  무엇인가요?

그렇습니다!  "기어다니기"와 "헤엄치기" 메소드가 중복 구현되고 있습니다.  정말 그래야 할 수도 있습니다.  이것이 그래픽 디스 플래이를 지원해야 하고, 동일한 인터페이스의 기어다니기와 헤엄치기가 실제 구현이 상당한 차이가 있을 수도 있습니다.  그런 경우라면 크게 문제 삼지 않아도 될 듯 합니다.

하지만, 만약 동일한 구현이라면 중복은 심각한 문제입니다.  중복을 해결하는 방법은 상속과 조합에 의한 위임이 있습니다.  여기서 상속은 답이 아니므로, 위임을 통해서 구현하면 됩니다.  최종 코드는 아래와 같습니다.  문제를 쉽게 하기 위해서 실제 코드가 아닌 의사코드 수준으로 작성하였습니다.

유전적 형질을 복수 개로 선정하는 것은 다중상속이 필요합니다.

unit Base;

interface

uses
  Classes, SysUtils;

type
  육상동물행동 = interface
  ['{F99833CA-A0C3-49D4-A677-220803AAFEE1}']
    procedure 기어다니기;
  end;

  수상동물행동 = interface
  ['{A934FBD6-55D3-435B-A10B-2357AF98584B}']
    procedure 헤엄치기;
  end;

  육상동물행동클래스 = class (TInterfacedObject, 육상동물행동)
  private
  public
    procedure 기어다니기;
  end;

  수상동물행동클래스 = class (TInterfacedObject, 수상동물행동)
  private
  public
    procedure 헤엄치기;
  end;

  애완동물 = class
  private
  public
    procedure 사료먹기;
  end;

  강아지 = class(애완동물, 육상동물행동)
  private
     _육상동물행동 : 육상동물행동클래스;
     property AnimalBehavior : 육상동물행동클래스 read _육상동물행동
              implements 육상동물행동;
  public
  end;

  고양이 = class(애완동물, 육상동물행동)
  private
     _육상동물행동 : 육상동물행동클래스;
     property AnimalBehavior : 육상동물행동클래스 read _육상동물행동
              implements 육상동물행동;
  public
  end;

  금붕어 = class(애완동물, 수상동물행동)
  private
     _수상동물행동 : 수상동물행동클래스;
     property AnimalBehavior : 수상동물행동클래스 read _수상동물행동
              implements 수상동물행동;
  public
  end;

implementation

{ 육상동물행동클래스 }

procedure 육상동물행동클래스.기어다니기;
begin

end;

{ 수상동물행동클래스 }

procedure 수상동물행동클래스.헤엄치기;
begin

end;

{ 애완동물 }

procedure 애완동물.사료먹기;
begin

end;

end.
클래스 다이어그램을 쉽게 보이기 위해서 한글을 사용한 것이 실수인 듯 합니다.  델파이가 생성한 코드에 한글이 붙으니 다소 복잡해 보이네요.

여기서는 중복된 코드를 위임을 하는 방법을 보이기 위한 것이니, 해당 주제에 집중해서 간단하게 설명하도록 하겠습니다.

39-41: 라인에 보시면 강아지 클래스가 구현해야 하는 인터페이스인 육상동물행동의 구현을 9: 라인에서 육상동물행동클래스로 구현을 이미 한 상태입니다.  이것을 단지 강아지 클래스에서도 이 기능을 사용하겠다고 선언하는 방법을 보여주고 있습니다.  implements를 이용해서 해당 구현을 위임하는 과정입니다.




Posted by 류종택


티스토리 툴바