상속을 사용하는 경우

업데이트: 2007년 11월

상속은 유용한 프로그래밍 개념이지만 잘못 사용하기 쉽습니다. 인터페이스로 이러한 작업을 더 잘 수행하는 경우가 많습니다. 이 항목과 인터페이스 사용 시기는 각 방법을 사용해야 하는 경우를 이해하는 데 도움이 됩니다.

다음 경우에는 상속을 사용하는 것이 바람직합니다.

  • 상속 계층 구조가 "포함" 관계가 아니라 "동등" 관계를 나타내는 경우

  • 기본 클래스의 코드를 다시 사용할 수 있는 경우

  • 다른 데이터 형식에 같은 클래스 및 메서드를 적용해야 하는 경우

  • 클래스 계층 구조가 비교적 얕으며 다른 개발자들이 더 많은 수준을 추가할 가능성이 적은 경우

  • 기본 클래스를 변경하여 파생 클래스를 전체적으로 변경하려는 경우

이러한 고려 사항은 아래에서 순서대로 설명됩니다.

상속 및 "동등" 관계

개체 지향 프로그래밍에서 클래스 관계를 나타내는 두 가지 방법은 "동등" 및 "포함" 관계입니다. "동등" 관계에서 파생 클래스는 명확하게 기본 클래스와 같은 종류의 클래스에 해당합니다. 예를 들어, PremierCustomer라는 클래스가 있을 때 우수 고객 역시 고객에 해당하므로 이 클래스는 Customer라는 기본 클래스와 "동등" 관계를 형성합니다. 그러나 CustomerReferral 클래스의 경우에는 고객 조회에 고객이 포함되지만 고객 조회가 고객과 같은 종류에 해당하지는 않으므로 Customer 클래스와 "포함" 관계를 형성합니다.

상속 계층 구조의 개체는 기본 클래스에 정의된 필드, 속성, 메서드 및 이벤트를 상속하므로 기본 클래스와 "동등" 관계를 형성해야 합니다. 다른 클래스와 "포함" 관계를 형성하는 클래스는 부적절한 속성 및 메서드를 상속할 수 있으므로 상속 계층 구조에 적합하지 않습니다. 예를 들어, 앞에서 설명한 CustomerReferral 클래스는 Customer 클래스에서 파생된 경우 ShippingPrefs 및 LastOrderPlaced와 같은 무의미한 속성을 상속할 수 있습니다. 이와 같은 "포함" 관계는 관련 없는 클래스 또는 인터페이스를 사용하여 표시해야 합니다. 다음 그림은 "동등" 및 "포함" 관계의 예를 보여 줍니다.

"동등" 관계와 "포함" 관계 비교

기본 클래스 및 코드 다시 사용

상속을 사용하는 또 다른 이유는 코드를 다시 사용할 수 있기 때문입니다. 잘 디자인된 클래스는 한 번 디버깅한 다음 새 클래스의 기본으로 반복해서 사용할 수 있습니다.

효과적으로 코드를 다시 사용하는 예를 보면 데이터 구조를 관리하는 라이브러리를 함께 사용하는 경우가 많습니다. 예를 들어, 여러 종류의 메모리 내장 목록을 관리하는 대규모 업무용 응용 프로그램이 있다고 가정합니다. 이러한 목록 중에는 세션이 시작될 때 빠른 진행을 위해 데이터베이스에서 데이터를 읽어 오는 고객 데이터베이스의 메모리 내장 복사본이 있습니다. 해당 데이터 구조는 다음과 같을 수 있습니다.

Class CustomerInfo
    Protected PreviousCustomer As CustomerInfo
    Protected NextCustomer As CustomerInfo
    Public ID As Integer
    Public FullName As String

    Public Sub InsertCustomer(ByVal FullName As String)
        ' Insert code to add a CustomerInfo item to the list.
    End Sub

    Public Sub DeleteCustomer()
        ' Insert code to remove a CustomerInfo item from the list.
    End Sub

    Public Function GetNextCustomer() As CustomerInfo
        ' Insert code to get the next CustomerInfo item from the list.
        Return NextCustomer
    End Function

    Public Function GetPrevCustomer() As CustomerInfo
        'Insert code to get the previous CustomerInfo item from the list.
        Return PreviousCustomer
    End Function
End Class

사용자 응용 프로그램에 다음 코드 조각에서와 같이 장바구니 목록에 추가된 제품과 유사한 제품 목록을 사용할 수도 있습니다.

Class ShoppingCartItem
    Protected PreviousItem As ShoppingCartItem
    Protected NextItem As ShoppingCartItem
    Public ProductCode As Integer
    Public Function GetNextItem() As ShoppingCartItem
        ' Insert code to get the next ShoppingCartItem from the list.
        Return NextItem
    End Function
End Class

여기서 한 가지 패턴을 볼 수 있습니다. 즉, 두 개의 목록이 동일한 방식으로 작동하지만(삽입, 삭제 및 검색) 서로 다른 데이터 형식에서 작동한다는 것입니다. 기본적으로 동일한 기능을 수행하는 두 개의 코드베이스를 유지 관리하는 것은 효율적이지 않습니다. 가장 효율적인 해결 방법은 다음과 같이 목록 관리 작업을 자체 클래스에 할당한 다음 다른 데이터 형식에 대해서는 해당 클래스에서 상속하는 것입니다.

Class ListItem
    Protected PreviousItem As ListItem
    Protected NextItem As ListItem
    Public Function GetNextItem() As ListItem
        ' Insert code to get the next item in the list.
        Return NextItem
    End Function
    Public Sub InsertNextItem()
        ' Insert code to add a item to the list.
    End Sub

    Public Sub DeleteNextItem()
        ' Insert code to remove a item from the list.
    End Sub

    Public Function GetPrevItem() As ListItem
        'Insert code to get the previous item from the list.
        Return PreviousItem
    End Function
End Class

ListItem 클래스는 한 번만 디버깅하면 됩니다. 그런 다음에는 목록 관리를 다시 고려할 필요 없이 해당 클래스를 사용하는 클래스를 작성할 수 있습니다. 예를 들면 다음과 같습니다.

Class CustomerInfo
    Inherits ListItem
    Public ID As Integer
    Public FullName As String
End Class
Class ShoppingCartItem
    Inherits ListItem
    Public ProductCode As Integer
End Class

상속 기반 코드 다시 사용은 유용한 도구이지만 위험이 따릅니다. 가장 잘 디자인된 시스템의 경우에도 디자이너가 예측할 수 없는 방식으로 변경될 수 있습니다. 기존 클래스 계층 구조가 변경되어 의도하지 않은 결과가 발생하는 경우도 있습니다. 배포 후 기본 클래스 디자인 변경 사항의 "손상되기 쉬운 기본 클래스 문제"에 몇 가지 관련 예제가 제공되어 있습니다.

호환 가능 파생 클래스

클래스 계층 구조의 파생 클래스는 경우에 따라 해당 기본 클래스와 교대로 사용할 수 있습니다. 이 프로세스를 상속 기반 다형성이라고 합니다. 이 접근 방법은 인터페이스 기반 다형성의 최상 기능을 기본 클래스의 코드를 다시 사용하거나 재정의하는 옵션과 조합합니다.

이러한 방법이 유용하게 사용될 수 있는 예로 그림 패키지를 들 수 있습니다. 예를 들어, 다음 코드 조각을 고려할 수 있는데, 여기서는 상속을 사용하지 않습니다.

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
    ByVal Y As Integer, ByVal Size As Integer)

    Select Case Shape.type
        Case shpCircle
            ' Insert circle drawing code here.
        Case shpLine
            ' Insert line drawing code here.
    End Select
End Sub

이 접근 방법에는 몇 가지 문제가 있습니다. 누군가가 나중에 타원 옵션을 추가하기로 한 경우에는 소스 코드를 변경해야 합니다. 대상 사용자가 소스 코드에 액세스하지 못할 수 있기 때문입니다. 좀 더 까다로운 문제는 타원을 그릴 때 선에는 적합하지 않은 다른 매개 변수(타원에는 긴 지름과 짧은 지름이 있음)가 필요하다는 것입니다. 누군가가 다시 연결되어 있는 여러 개의 선을 추가하려고 하면 다른 매개 변수가 추가되며 이 매개 변수 역시 다른 경우에는 적합하지 않습니다.

이러한 문제점의 대부분은 상속 기능으로 해결할 수 있습니다. 잘 디자인된 기본 클래스에서는 특정 메서드의 구현이 파생 클래스에까지 유지되므로 어떠한 종류의 모양도 구현할 수 있습니다. 다른 개발자들은 기본 클래스에 대한 설명서를 사용하여 파생 클래스의 메서드를 구현할 수 있습니다. 다른 클래스 항목(예: x 축 및 y 축)은 모든 하위 항목에서 사용되므로 기본 클래스로 만들어질 수 있습니다. 예를 들어, Draw는 MustOverride 메서드가 될 수 있습니다.

MustInherit Class Shape
    Public X As Integer
    Public Y As Integer
    MustOverride Sub Draw()
End Class

그런 다음 해당 클래스에 적절하게 추가하여 다른 모양을 그릴 수 있습니다. 예를 들어, Line 클래스에는 Length 필드만 필요할 수 있습니다.

Class Line
    Inherits Shape
    Public Length As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for this shape.
    End Sub
End Class

이 방법은 소스 코드에 액세스할 수 없는 다른 개발자들이 필요에 맞게 새로운 파생 클래스로 기본 클래스를 확장할 수 있으므로 유용합니다. 예를 들어, Rectangle 클래스는 다음과 같이 Line 클래스에서 파생될 수 있습니다.

Class Rectangle
    Inherits Line
    Public Width As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for the Rectangle shape.
    End Sub
End Class

이 예제에서는 각 수준에서 구현 세부 항목을 추가하여 일반용 클래스에서 아주 특정한 클래스로 이동하는 방법을 보여 줍니다.

여기서 파생 클래스가 실제로 "동등" 관계를 나타내는지 또는 "포함" 관계를 나타내는지 여부를 다시 평가하는 것이 바람직합니다. 새로운 사각형 클래스가 선으로만 구성된 경우 상속 기능을 사용하는 것은 바람직하지 않습니다. 그러나 새 사각형이 너비 속성을 갖는 선으로 이루어진 경우 "동등" 관계가 유지됩니다.

얕은 클래스 계층 구조

상속은 비교적 얕은 클래스 계층 구조에 가장 적합합니다. 너무 깊고 복잡한 클래스 계층 구조는 개발하기 어려울 수 있습니다. 클래스 계층 구조 사용 여부를 결정할 때에는 클래스 계층 구조를 복잡하게 만들어 사용할 경우 어떠한 장점이 있는지를 고려해야 합니다. 일반적으로 계층 구조를 여섯 개 수준 이하로 제한하게 됩니다. 그러나 특정 클래스 계층 구조의 최대 깊이는 각 수준의 복잡도를 포함하여 많은 요인에 따라 달라집니다.

기본 클래스를 통한 파생 클래스 전역 변경

상속의 가장 강력한 기능 중 하나는 기본 클래스를 변경하고 해당 변경 내용을 파생 클래스로 전파하는 능력입니다. 상속을 신중하게 잘 사용하면 단일 메서드의 구현을 업데이트하고 수십 개 또는 수백 개의 파생 클래스에서 새 코드를 사용할 수 있습니다. 그러나 이러한 변경 내용이 다른 사용자가 디자인한 상속 클래스에 문제를 야기할 수 있으므로 이 방법은 위험할 수 있습니다. 새로운 기본 클래스가 원본을 사용하는 클래스와 호환될 수 있도록 하려면 많은 주의가 필요합니다. 특히 기본 클래스 멤버의 이름이나 형식을 바꾸는 일이 없어야 합니다.

예를 들어, 사용자가 Integer 형식의 필드를 갖는 기본 클래스를 디자인하여 우편 번호 정보를 저장하고 다른 개발자가 상속된 우편 번호 필드를 사용하는 파생 클래스를 작성했다고 가정합니다. 또한 해당 우편 번호 필드에서는 다섯 자리 숫자를 저장할 수 있으며 우체국에서는 하이픈과 네 자리 숫자를 추가로 갖도록 우편 번호를 확장했다고 가정합니다. 최악의 경우, 사용자는 10개의 문자열을 저장할 수 있도록 기본 클래스의 해당 필드를 수정할 수 있으나 다른 개발자들은 새로운 크기 및 데이터 형식을 사용하도록 파생 클래스를 변경하여 다시 컴파일해야 합니다.

기본 클래스를 변경하는 가장 안전한 방법은 단순히 새 멤버를 추가하는 것입니다. 예를 들어, 앞에서 설명한 우편 번호 예제에서는 네 자리 숫자를 추가로 저장하도록 새 필드를 추가할 수 있습니다. 이렇게 할 경우 기존 응용 프로그램을 중단하지 않고도 새 필드를 사용하도록 클라이언트 응용 프로그램을 업데이트할 수 있습니다. 상속 계층 구조에서 기본 클래스를 확장할 수 있는 기능은 인터페이스를 사용할 때에는 얻을 수 없는 중요한 이점입니다.

참고 항목

개념

인터페이스 사용 시기

배포 후 기본 클래스 디자인 변경 사항