C#) 상속의 메모리구조와 캐스팅
결론
클래스와 인터페이스를 다중상속 시 메모리 구조
+-------------------+
| class_parent 영역 |
+-------------------+
| class_childB 영역 |
+-------------------+
| VTable (interface_parent) |
+-------------------+
인터페이스를 포함한 다운캐스팅은 안전하다.
C#에서 클래스와 인터페이스의 상속은 메모리 구조상 안전한 업캐스팅과 다운캐스팅을 가능하게 한다.
이는 VTable(가상 함수 테이블)을 활용하여 객체의 타입 정보를 유지하면서 올바른 메서드를 호출할 수 있기 때문이다.
다운캐스팅 시에도 원본 객체의 메모리 주소는 변하지 않으며, VTable을 통해 적절한 메서드가 호출되므로 안전하다.
이 글을 작성한 이유
나는 아래에 예시 코드를 보며, class_childB가 인터페이스를 상속받았지만, 메모리 구조를 보면 부모 클래스만 앞에 배치된다는 점에서 의문이 생겼다.
즉, 다운캐스팅 시 메모리상 뒤에 있는 인터페이스 정보가 과연 올바르게 해석될 수 있을지 궁금했다.
C#에서 인터페이스를 통해 다중 상속을 지원하기 때문에 그 내부 동작 방식이 어떻게 구현되는지 정확히 알고 싶었다. 이를 위해 C#의 상속과 인터페이스의 메모리 구조를 살펴보고 VTable이 어떻게 작동하는지를 찾아보게 되었다.
상속의 메모리구조
C#에서는 두가지 상속 형태가 있다.
- 부모
class로부터의 상속 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_childA는class_parent만 상속 -
class_childB는class_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_a는class_parent타입의 리스트이므로,class_parent를 상속하는 모든 객체를 포함할 수 있음.childA와childB는class_parent의 파생 클래스이므로 업캐스팅되어 리스트에 저장 가능.
interface_parent 변수에 childB 할당
interface_parent a = childB;
childB는interface_parent를 구현했기 때문에 인터페이스 타입으로 업캐스팅 가능.- 이때,
childB내부에 인터페이스 VTable이 포함되며, 인터페이스 메서드 호출을 위해 가상 함수 테이블이 사용됨.
메모리 구조 (예상)
childB 객체가 메모리에 배치될 때 내부적으로 다음과 같이 저장될 가능성이 크다.
+-------------------+
| class_parent 영역 |
+-------------------+
| class_childB 영역 |
+-------------------+
| VTable (interface_parent) |
+-------------------+
- class_parent 영역:
class_childB는class_parent를 상속하므로, 부모 클래스의 메모리 레이아웃을 포함. - class_childB 영역:
class_childB만의 고유한 데이터 및 메서드가 저장됨. - VTable (interface_parent):
class_childB가interface_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 또는 Cat의 Speak()를 호출할 경우,
- 정적 바인딩이라면
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();를 호출하면:
a가Dog객체를 가리킴.a의 내부 VTable을 통해Dog.Speak()를 호출.
이렇게 동적 바인딩이 구현된다.
결론
캐스팅은 안전하다
- C#의 객체는 항상 같은 메모리를 유지
childB가interface_parent로 캐스팅되어도 원본 객체의 메모리 주소는 변하지 않음.
- 인터페이스는 별도의 VTable을 사용
interface_parent로 업캐스팅할 때, 실제로 객체의 데이터가 변경되는 것이 아니라 VTable을 통해 인터페이스 메서드가 참조됨.
-
다운캐스팅 시 원본 객체의 포인터를 유지
-
interface_parent타입의 참조 변수a는 실제로childB객체를 가리키고 있음. -
(class_childB)a로 변환하면 원래 객체의 주소를 되찾기 때문에 원본 데이터를 안전하게 읽을 수 있음.
-
도움주신분들
혼자만의 궁금증을 해결하기위해 도움 주신 내배캠 튜터 김영호튜터님, 소재철튜터님, 오정호튜터님 감사합니다.
댓글남기기