결론

클래스와 인터페이스를 다중상속 시 메모리 구조

+-------------------+
| class_parent 영역 |
+-------------------+
| class_childB 영역 |
+-------------------+
| VTable (interface_parent) |
+-------------------+

인터페이스를 포함한 다운캐스팅은 안전하다.

C#에서 클래스와 인터페이스의 상속은 메모리 구조상 안전한 업캐스팅과 다운캐스팅을 가능하게 한다.

이는 VTable(가상 함수 테이블)을 활용하여 객체의 타입 정보를 유지하면서 올바른 메서드를 호출할 수 있기 때문이다.

다운캐스팅 시에도 원본 객체의 메모리 주소는 변하지 않으며, VTable을 통해 적절한 메서드가 호출되므로 안전하다.


이 글을 작성한 이유

나는 아래에 예시 코드를 보며, class_childB가 인터페이스를 상속받았지만, 메모리 구조를 보면 부모 클래스만 앞에 배치된다는 점에서 의문이 생겼다.

즉, 다운캐스팅 시 메모리상 뒤에 있는 인터페이스 정보가 과연 올바르게 해석될 수 있을지 궁금했다.

C#에서 인터페이스를 통해 다중 상속을 지원하기 때문에 그 내부 동작 방식이 어떻게 구현되는지 정확히 알고 싶었다. 이를 위해 C#의 상속과 인터페이스의 메모리 구조를 살펴보고 VTable이 어떻게 작동하는지를 찾아보게 되었다.


상속의 메모리구조

C#에서는 두가지 상속 형태가 있다.

  1. 부모 class로부터의 상속
  2. interface로부터의 상속

예시 코드

아래의 예시를 보자

void Main(string[] args)
{
    class_parent parentC = new class_parent();
    class_childA childA = new class_childA();
    class_childB childB = new class_childB();

    List<class_parent> list_a = new List<class_parent> { parentC, childA, childB };
    interface_parent a = childB;
}

class class_parent{}

interface interface_parent{}

class class_childA : class_parent{}

class class_childB : class_parent, interface_parent{}
  • class_parent는 부모 클래스 역할

  • class_childAclass_parent만 상속

  • class_childBclass_parent를 상속하면서 interface_parent 인터페이스도 구현

  • List<class_parent>를 이용하여 부모 타입의 리스트에 class_parent, class_childA, class_childB를 넣을 수 있다.

  • interface_parent 타입의 변수에 class_childB 객체를 할당할 수 있다.

메모리 구조 분석 (업캐스팅)

List<class_parent> 리스트의 경우

List<class_parent> list_a = new List<class_parent> { parentC, childA, childB };
  • list_aclass_parent 타입의 리스트이므로, class_parent를 상속하는 모든 객체를 포함할 수 있음.
  • childAchildBclass_parent의 파생 클래스이므로 업캐스팅되어 리스트에 저장 가능.

interface_parent 변수에 childB 할당

interface_parent a = childB;
  • childBinterface_parent를 구현했기 때문에 인터페이스 타입으로 업캐스팅 가능.
  • 이때, childB 내부에 인터페이스 VTable이 포함되며, 인터페이스 메서드 호출을 위해 가상 함수 테이블이 사용됨.

메모리 구조 (예상)

childB 객체가 메모리에 배치될 때 내부적으로 다음과 같이 저장될 가능성이 크다.

+-------------------+
| class_parent 영역 |
+-------------------+
| class_childB 영역 |
+-------------------+
| VTable (interface_parent) |
+-------------------+
  • class_parent 영역: class_childBclass_parent를 상속하므로, 부모 클래스의 메모리 레이아웃을 포함.
  • class_childB 영역: class_childB만의 고유한 데이터 및 메서드가 저장됨.
  • VTable (interface_parent): class_childBinterface_parent를 구현했기 때문에 인터페이스 메서드 호출을 위한 가상 테이블 포인터가 추가됨.

다운캐스팅

캐스팅은 메모리 주소 자체를 변경하지 않고, 타입을 해석 하는 방식을 변경하는 것이다.

  • 업캐스팅은 부모 클래스 타입이 자식 클래스 데이터에 존재하기 때문에 쉽다.
  • 다운캐스팅은 여러 확인 과정을 거친다.

다운캐스팅이 가능한 이유

객체의 타입 정보(Runtime Type Information, RTTI)를 확인

  • C#에서는 객체마다 실제 타입 정보가 저장되어 있다.
  • 다운캐스팅 시 현재 객체의 타입이 변환 대상과 호환되는지 체크한다.

메모리 레이아웃을 확인

  • 부모 클래스 타입(class_parent)으로 저장된 객체가 실제로 자식 클래스(class_childA)인지 확인한다.
  • 만약 class_childB와 같이 호환되지 않는 클래스라면 InvalidCastException이 발생한다.

메모리 주소 유지 및 해석 방식 변경

  • 메모리 주소 자체는 변하지 않는다.
  • 다운캐스팅이 성공하면 기존 메모리 블록을 자식 클래스의 시각에서 다시 해석한다

VTable (Virtual Table)

이 방법을 이해하기 위해서 메모리 구조에 붙어있는 VTable의 작동 방식을 이해해야 했다.

참고자료

  • https://en.wikipedia.org/wiki/Virtual_method_table
  • https://guru.tistory.com/125

VTable이 필요한 이유

보통 정적 바인딩 (Static Binding)에서는 함수 주소가 컴파일 타임에 결정된다. 그러나 동적 바인딩 (Dynamic Binding)에서는 런타임에 실제 호출할 함수가 결정되어야 한다.

interface IAnimal
{
    void Speak();
}

class Dog : IAnimal
{
    public void Speak() { Console.WriteLine("Woof!"); }
}

class Cat : IAnimal
{
    public void Speak() { Console.WriteLine("Meow!"); }
}

이제 IAnimal 타입의 참조 변수를 통해 Dog 또는 CatSpeak()를 호출할 경우,

  • 정적 바인딩이라면 IAnimal.Speak()만 호출될 것.
  • 동적 바인딩 (VTable 사용)이라면 Dog.Speak() 또는 Cat.Speak()를 정확하게 호출할 수 있음.

이것이 가능한 이유가 VTable 때문이다.

VTable의 구조

C#에서 인터페이스를 사용하면 객체 내부에 인터페이스 메서드 포인터를 저장하는 VTable이 추가된다.

interface IAnimal
{
    void Speak();
}

class Dog : IAnimal
{
    public void Speak() { Console.WriteLine("Woof!"); }
}

메모리 구조를 간단히 표현하면:

+------------------+
| Dog Object       |
+------------------+
| VTable Pointer  |  ---> [ Speak() → Dog.Speak() ]
+------------------+
  • VTable Pointer는 VTable을 가리킴.
  • VTable 내부에는 Speak()를 호출하면 Dog.Speak()가 실행되도록 연결됨.

즉, IAnimal a = new Dog(); 상태에서 a.Speak();를 호출하면:

  1. aDog 객체를 가리킴.
  2. a의 내부 VTable을 통해 Dog.Speak()를 호출.

이렇게 동적 바인딩이 구현된다.

결론

캐스팅은 안전하다

  • C#의 객체는 항상 같은 메모리를 유지
    • childBinterface_parent로 캐스팅되어도 원본 객체의 메모리 주소는 변하지 않음.
  • 인터페이스는 별도의 VTable을 사용
    • interface_parent로 업캐스팅할 때, 실제로 객체의 데이터가 변경되는 것이 아니라 VTable을 통해 인터페이스 메서드가 참조됨.
  • 다운캐스팅 시 원본 객체의 포인터를 유지

    • interface_parent 타입의 참조 변수 a는 실제로 childB 객체를 가리키고 있음.

    • (class_childB)a로 변환하면 원래 객체의 주소를 되찾기 때문에 원본 데이터를 안전하게 읽을 수 있음.

도움주신분들

혼자만의 궁금증을 해결하기위해 도움 주신 내배캠 튜터 김영호튜터님, 소재철튜터님, 오정호튜터님 감사합니다.

태그: ,

카테고리:

업데이트:

댓글남기기