C#에서 상속이 예상과 다르게 작동할 수 있는 이유

상속은 객체 지향 프로그래밍(OOP)의 기본 개념 중 하나로, 개발자가 기존 클래스를 기반으로 새 클래스를 생성할 수 있도록 합니다. 이는 코드 재사용성을 촉진하고 자연스러운 계층 구조를 생성합니다. 그러나 C#에서 상속이 예상대로 작동하지 않는 시나리오가 있으며, 특히 추상 클래스 및 오버라이드된 메서드를 다룰 때 더욱 그렇습니다. 여기에서는 경험이 많은 개발자조차 종종 어려움을 겪는 흥미로운 사례를 살펴보겠습니다.

문제: 추상 클래스와의 상속 문제

한 개발자가 Animal 클래스를 만들고 그 파생 클래스인 Dog를 생성하려고 하는 시나리오를 고려해 보겠습니다. 각 동물이 그에 연결된 특정 타입의 다리를 반환하도록 하려는 의도입니다. 다음은 개발자가 구현하려고 하는 코드의 단순화된 버전입니다:

abstract class Animal
{
  public Leg GetLeg() {...}
}

abstract class Leg { }

class Dog : Animal
{
  public override DogLeg Leg() {...}
}

class DogLeg : Leg { }

예상되는 동작은 무엇인가요?

개발자는 GetLeg() 메서드를 호출할 때 Dog 클래스가 DogLeg를 반환하기를 원합니다. 이상적으로는 Animal 클래스를 사용하는 사람은 일반적인 Leg를 얻고, Dog와 같은 특정 서브클래스는 자신만의 특정 Leg 타입을 제공하게 됩니다. 그러나 개발자는 Dog의 메서드가 Animal과 호환되는 타입을 반환해야 한다는 컴파일 오류가 발생하는 것이 직관적이지 않다고 느낍니다.

근본 원인: C#의 불변성 및 타입 안전성

문제의 핵심은 C#의 불변성 개념에 있습니다. 컴파일이 이루어지지 않는 이유를 간단히 설명하면: GetLeg의 반환 타입은 불변입니다.

불변성이란 무엇을 의미하나요?

  • 불변성은 기본 클래스 메서드가 특정 타입을 반환할 경우, 파생 클래스의 모든 오버라이드된 메서드도 정확히 동일한 타입을 반환해야 한다는 사실을 의미합니다. 이는 파생 타입이 기본 타입으로 형변환이 가능하더라도 마찬가지입니다. 이러한 조건은 타입 안전성을 유지하고 기본 클래스를 사용하는 코드가 일관된 인터페이스에 의존할 수 있도록 보장하는 데 필수적입니다.

중요한 통찰

  • DogLegLeg로 형변환할 수 있지만, C#에서 메서드를 오버라이드하기에는 충분하지 않습니다. 반환 타입은 기본 클래스 메서드(GetLeg())와 정확히 일치해야 하며, 이로 인해 컴파일 문제가 발생하게 됩니다.

대안 탐색: 상속보다 구성

상속은 OOP에서 일반적으로 사용되는 기능이지만, 많은 경우 복잡성을 초래할 수 있습니다.

왜 구성을 고려해야 하나요?

  1. 유연성: 구성은 특정 클래스 계층에 얽매이지 않고 객체의 동작을 런타임에 변경할 수 있는 더 많은 유연성을 제공합니다.
  2. 단순화된 API: 구성을 사용함으로써 클래스 계층의 복잡성이 줄어들어 소비자에게 더 깔끔한 API를 제공합니다.
  3. 불변성 문제 회피: 구성을 사용할 때는 메서드 오버라이드에 관여하지 않기 때문에 공변성이나 반공변성 규칙에 제한받지 않습니다.

이 시나리오에서의 구성 예시

동물의 다리를 표현하는 방법을 정의하기 위해 상속에 의존하기보다, 다음과 같은 방법을 고려해 볼 수 있습니다:

class Animal
{
  private Leg leg; // Leg의 구성

  public Leg GetLeg() 
  {
    return leg; // 구성된 다리 반환
  }
}

class Dog : Animal
{
  public Dog()
  {
    this.leg = new DogLeg(); // 특정 DogLeg로 구성
  }
}

이 접근법은 상속에서 타입 일치를 부담을 덜어주어, 더욱 강력하고 유연한 코드 구조를 가능하게 합니다.

결론

C#에서 상속이 어떻게 작동하는지, 특히 불변성과 관련하여 이해하는 것은 강력한 애플리케이션을 만들려는 개발자에게 중요합니다. 제한적으로 보일 수 있지만, 이러한 제약을 인식하고 구성을 같은 대안으로 고려하는 것이 기능성과 유지 관리성을 향상시키는 더 효과적인 솔루션으로 이어질 수 있습니다.

이 주제에 대해 더 읽고 싶으시다면 C#의 공변성과 반공변성을 탐험해 보시길 권장합니다. 이곳에서 OOP에서의 타입 변환에 대한 미묘한 논의가 깊이 다뤄지고 있습니다.

상속과 구성 모두를 염두에 두고 디자인에 접근함으로써, 개발자들은 이러한 복잡성을 항해하고 강력하면서도 관리하기 쉬운 시스템을 구축할 수 있습니다.