개발자가 놓치기 쉬운 닷넷의 기본 원리 #3: 상속과 interface의 문제점

지난번 글에서 이어집니다. 🙂


3. 상속과 interface의 문제점


3.1. 상속


3.1.1. 상속에 있어서의 생성자 (constructor)

“child의 default 생성자가 있으면 그 생성자에는 parent의 생성자 (base()) 호출이 compile시 자동으로 삽입된다.” 인라인 초기화 구문 상에서 지정해 놓은 내용이 있다면 이 내용들은 기본 생성자보다 앞서서 처리된다. 처리 순서를 요약하면 다음과 같다.



  1. 자식 클래스의 멤버 필드 내 인라인 표현식

  2. 상위 클래스의 멤버 필드 내 인라인 표현식

  3. 자식 클래스 생성자에서 지정한 상위 클래스 생성자의 호출

  4. 자식 클래스 생성자의 호출

3.1.2. down cast는 본질적으로 매우 위험하다.

down cast – child의 type으로 parent를 casting – 는 parent 형태의 type이 정말 child type인지 compile시에는 알 수 없다. 실행 시에 type check가 이루어지므로 runtime시에 InvalidCastException이 발생할 가능성이 커진다.


“프로그래밍 시 오류는 가능한 compile 시에 처리하는 것이 좋다.”


덧. down cast에 대한 방비책이 몇 가지가 있다. 다음은 그 예시이다.



  • 실제 형식 변환 이전에 해당 개체가 정말 지정한 형식과 호환성이 있는지 확인하는 방법: is 연산자를 통하여 주어진 개체를 해당 형식으로 down cast하는 것이 안전한지의 여부를 boolean 값으로 평가할 수 있다.

  • 형식 변환 실패 시 null 참조로 fallback하도록 만드는 방법: as 연산자를 통하여 주어진 개체를 해당 형식으로 down cast 시도하되, 호환되는 형식이 아니거나 처음부터 null 참조였을 경우에는 null을 반환하도록 할 수 있다. 이 때, C# 2.0 컴파일러를 사용 중이라면, 연이어서 변환에 실패하였을 경우 추가 fallback 처리를 다음과 같이 할 수 있다.

    string s = (sth as string) ?? String.Empty;

  • 위의 is나 as 연산자의 평가 결과와 무관하게 동작하는 경우도 있다. 클래스 내에 implicit, explicit operator method를 추가하였을 경우 상속 관계와 무관한 casting이 발행되기도 한다. implicit operator method가 주어진 형식에 대하여 존재하는 경우 해당 형식으로의 대입 구문에서 자동으로 호출되며, explicit operator method가 주어진 형식에 대하여 존재하는 경우 강제 형변환 구문을 통하여 이를 명시적으로 호출할 수 있다. 하지만 양쪽 모두 꼭 필요한 경우가 아니면 explicit operator method만을 제한적으로 사용하는 것이 혼란을 줄일 수 있다.

  • primitive type의 경우 강제 형변환을 통하여 손실 형변환을 유도할 수 있으며 이는 상속 관계로 설명될 수 있는 것이 아니다.

3.1.3. 봉인 클래스의 protected 액세스 한정자를 사용하면 경고가 발생한다.

sealed 키워드로 선언된 클래스나 구조체에서 protected 멤버를 선언하면 컴파일러 경고가 발행된다. 왜냐하면, 상속 대상이 더 이상 존재하지 않기 때문에 protected 멤버는 원래의 의미가 아닌 private 멤버로 의미가 변질되기 때문이다.


3.2. interface


3.2.1. interface는 interface일 뿐 다중 상속의 대용품이 아니다.

interface를 method signature – 추상 클래스와 같이 구현부는 없고 선언부만 있는 method -의 용도, 즉 클래스나 구조체의 프로토타입 정도로만 생각하는 것이 옳은 판단이다. 즉, interface는 abstract method가 있는 클래스와 유사하지만 상속의 의미와는 그 용도가 다르다. 공통된 type을 정의하는 것으로 보아야 하고, 어떤 공통된 기능을 “지원한다”는 의미로 해석하는것이 옳다.


또한 interface는 클래스를 재이용하기 위해 상속을 사용하여 캡슐화의 파괴를 수반하는 것을 방지하는 기능이 있다. 상속을 사용하면 모두 구현 후 마치 소스 코드가 여기저기 천 조각을 주워 모아 만든 ‘누더기’ 같이 보이는 것에 한숨을 쉰 경험이 있을 것이다. 이 부분을 interface로 구현하면 보다 깔끔한 코드가 나오게 된다. 물론 public과 protected를 적절히 잘 사용해도 되긴 하지만 말이다.


하지만 상속은 메서드 오버라이드한 경우 클래스를 마음대로 개조해 버린 셈이 되므로 어디선가 묘한 모순이 발생하게 될 가능성도 높아질 뿐 아니라 추상 클래스의 경우 실제 구현부가 어디에 위치하는지도 애매하게 느껴질 수 있어 불안정한 코드가 되고 만다.


덧. 인터페이스의 설계는 최대한 단순하고 가볍게 하되, 한 번 외부에 공표한 인터페이스는 바꾸지 않는 것을 전제로 해야 한다. 인터페이스는 이 인터페이스를 구현하는 클래스들간의 약속이며 이를 깨뜨리게 될 경우 프로그램 코드의 일부 혹은 전체를 매번 새로 컴파일해서 전체 소프트웨어를 재 배포해야 하는 초과 비용을 발생하게 만든다. 즉, 작은 단위의 기능으로만 인터페이스를 신중하게 추가하고 관리해야 한다.


3.3. 상속 제대로 사용하기


“그렇다면 제대로 된 상속은 어떻게 판단할 수 있을까?”


상속은 ‘is a’ 관계가 성립해야 올바르다. 즉 ‘서브클래스 (자식) is a 슈퍼클래스 (부모)’가 성립해야 한다. 예를 들면 ‘Red is a Color’는 올바른 명제이지만, ‘Engine is a Car’는 ‘has a” 관계이므로 상속이 아닌 Composition 관계 – 또는 – Delegation 관계로 ‘Engine has a Car’라는 명제가 올바르게 된다.


Composition은 ‘객체를 field가 갖게 하는 방법’을 뜻하므로 ‘has a’ 관계가 정확히 성립한다. 상속 대신 composition과 delegation (조작이나 처리를 다른 객체에 위임)을 사용하면 다음과 같은 장점이 있다.



  1. 상속에서는 슈퍼 클래스가 허용하고 있는 조작을 서브 클래스에서 모두 허용하게 되지만, composition과 delegation에서는 조작을 제한할 수 있다.

  2. 클래스는 결코 변경할 수 없지만, composition하고 있는 객체는 자유롭게 변경할 수 있다. 예를 들면 학생 클래스가 항상, 영원히 학생이 아니라 나중에 취직을 하여 직장인 클래스가 되도록 할 수 있다는 의미이다.

상속을 composition과 delegation으로 변경하는 요령은? 여기서 Shape를 상속한 Polyline과 Circle을 변경한다면 다음과 같다.



  1. Shape (부모)의 공통된 내용을 구현한 구현 클래스 (ShapeImpl)를 만든다.

  2. Polyline과 Circle 클래스에서 ShapeImpl을 composition하고 부모와 공통되지 않는 method를 각각 위임받는다.

  3. ShapeImpl 클래스의 method를 추출한 IShape 인터페이스를 작성하고 Polyline과 Circle에서 이 인터페이스를 구현한다.

댓글 남기기