← 문제로

36. 인스턴스 생성 순서와 생성자 내 가상 메서드 호출

난이도 중
내 리뷰 · C#
해설 · C#

해설 — 인스턴스 생성 순서와 생성자 내 가상 메서드 호출

난이도: 중

요약

C# 의 파생 객체 생성 순서는 ① 파생 필드 초기화자 → ② base 생성자 본문 → ③ 파생 생성자 본문 이다(C++ 과 정반대 지점이 여기 있다). base 생성자가 가상 메서드를 호출하면 파생 오버라이드가 디스패치되는데, 그 시점엔 파생 필드 초기화자는 이미 실행됐지만 파생 생성자 본문은 아직이다. 그래서 Init() 안에서 fromInitializer 는 111, fromCtorBody 는 아직 기본값 0 으로 보인다.

예측 출력:

Base ctor start
Derived.Init: initializer=111, ctorBody=0
Base ctor end
Derived ctor: initializer=111, ctorBody=222

핵심 개념

  • 가상 디스패치는 생성 중에도 동적 타입 기준: 객체의 실제 타입은 생성 시작부터 Derived 다. 따라서 base 생성자에서의 Init()Derived.Init 으로 간다.
  • 필드 초기화자 vs 생성자 본문 시점 차이: 파생 필드 초기화자는 base 생성자 이전에 실행되지만, 파생 생성자 본문은 base 이후다.

단계별 내부 동작

  1. new Derived() → 먼저 파생 필드 초기화자 실행: fromInitializer=111, fromCtorBody 는 대입 없으니 기본값 0.
  2. base 생성자 본문 진입 → "Base ctor start" 출력 → Init() 가상 호출 → Derived.Init 실행. 이 시점 fromInitializer=111(초기화자 완료), fromCtorBody=0(본문 미실행) → "Derived.Init: initializer=111, ctorBody=0".
  3. "Base ctor end" 출력, base 생성자 종료.
  4. 파생 생성자 본문 실행 → fromCtorBody=222 → "Derived ctor: initializer=111, ctorBody=222".

흔한 오해·함정

  • 생성자에서 가상 메서드 호출은 위험 패턴: 오버라이드가 "완전히 초기화되지 않은" 파생 객체 위에서 실행된다. 위 예처럼 생성자 본문에서만 설정되는 필드는 기본값(0/null)으로 보여, Init() 이 그 필드를 신뢰하면 NullReferenceException 이나 잘못된 초기화가 난다. 게임 서버에서 base 가 생성자에서 RegisterHandlers() 같은 가상 훅을 부르면, 파생이 아직 준비 안 된 상태에서 등록돼 미묘한 버그가 생긴다.
  • C++ 과의 차이(질문 3): C++ 에서는 base 생성자 실행 중 객체의 동적 타입이 아직 Base 다. 그래서 base 생성자에서의 가상 호출은 Base::Init 으로 정적 디스패치되고, 파생 멤버는 아예 초기화되지도 않았다. C# 은 반대로 파생 오버라이드가 호출되지만(동적 타입이 처음부터 Derived), 파생 필드 초기화자는 이미 돌았고 생성자 본문은 아직 안 돈 어정쩡한 상태를 본다. 두 언어 모두 "생성자에서 가상 호출 금지"가 권고지만 이유·증상이 다르다.
  • 회피책: 가상 훅을 생성자에서 부르지 말고, 팩토리 메서드가 생성 후 별도 Initialize() 를 호출하거나, 초기화를 필드 초기화자/생성자 매개변수로 끌어와 가상 디스패치에 의존하지 않게 한다.

면접 포인트

  • C# 인스턴스 생성 순서(파생 필드 초기화자 → base 생성자 → 파생 생성자)를 정확히 말할 수 있어야 한다.
  • "생성자에서 가상 메서드를 호출하면 왜 위험한가"를 C#(부분 초기화된 파생 오버라이드 실행)과 C++(base 로 정적 디스패치)의 차이로 설명.
  • 실무 회피 패턴(2단계 초기화, 팩토리, 봉인 클래스)을 제시.