← 문제로

38. 명시적 인터페이스 구현의 디스패치와 접근 규칙

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

해설 — 명시적 인터페이스 구현의 디스패치와 접근 규칙

난이도: 중상

요약

한 클래스가 같은 시그니처를 요구하는 두 인터페이스를 구현할 때, 명시적 인터페이스 구현으로 각 인터페이스에 서로 다른 동작을 줄 수 있다. 명시적 멤버는 접근 제한자를 갖지 않으며 인터페이스 타입을 통해서만 호출된다. 출력:

walk(public)
walk(IWalker)
swim(ISwimmer)
walk(IWalker)
swim(ISwimmer)
walk(IWalker)

핵심 개념

  • 명시적 구현 문법 string IWalker.Move(): 이 멤버는 IWalker 인터페이스 슬롯에만 연결된다. 클래스의 public 표면에는 나타나지 않는다(인스턴스 a. 로 직접 호출 불가, 접근 한정자 붙일 수 없음).
  • 암시적(public) 구현 public string Move(): 클래스의 public 멤버이자, 명시적 구현이 없는 인터페이스 슬롯을 채우는 후보. 여기서는 두 인터페이스 모두 명시적 구현이 있으므로 public Move() 는 인터페이스 슬롯이 아니라 그냥 클래스 자체 메서드로 존재한다.
  • 인터페이스 디스패치: 인터페이스 타입 참조로 호출하면 그 인터페이스의 슬롯(=명시적 구현)으로 간다.

단계별 동작

  1. a.Move()walk(public): 정적 타입이 Amphibian 이라 클래스의 public Move() 를 호출. 명시적 멤버는 이 경로로 보이지 않는다.
  2. ((IWalker)a).Move()walk(IWalker): IWalker 로 캐스트 후 호출 → IWalker.Move() 슬롯 → 명시적 구현.
  3. ((ISwimmer)a).Move()swim(ISwimmer): 마찬가지로 ISwimmer.Move() 슬롯.
  4. IWalker w = a; w.Move()walk(IWalker): 정적 타입이 인터페이스이므로 해당 슬롯으로 디스패치.
  5. ISwimmer s = a; s.Move()swim(ISwimmer): 동일 원리.
  6. CallMove<T>(a) where T:IWalkerwalk(IWalker): 제네릭 제약이 IWalkert.Move()IWalker.Move() 로 디스패치. 제약 인터페이스가 곧 슬롯을 결정한다.

흔한 오해·함정

  • "public Move()가 인터페이스도 만족하니 그게 불리겠지": 아니다. 명시적 구현이 있으면 그것이 해당 인터페이스 슬롯을 덮어쓴다. 인터페이스 경로는 항상 명시적 구현으로 간다.
  • 명시적 멤버에 public을 못 붙임: 명시적 구현은 접근 한정자를 가질 수 없고, 인스턴스에서 직접 호출 불가. 이는 "인터페이스로만 노출하고 클래스 public API 는 깔끔히 유지"하는 데 쓰인다(예: IDisposable.Dispose() 를 숨기고 Close() 만 public 으로).
  • 박싱된 구조체: struct 가 인터페이스를 명시적 구현하면 인터페이스로 호출 시 박싱이 일어난다(값 복사 + 힙). 핫패스에서 주의.
  • 이름 충돌 해결: 두 인터페이스가 같은 시그니처를 요구할 때 서로 다른 동작이 필요하면 명시적 구현이 유일한 방법. 동작이 같아도 되면 public 하나로 둘 다 만족시킬 수 있다.

면접 포인트

  • 명시적 인터페이스 구현이 필요한 두 가지 이유: (1) 같은 시그니처 다중 인터페이스의 동작 분리, (2) 인터페이스 멤버를 클래스 public 표면에서 숨기기.
  • 디스패치가 **정적 타입(호출 경로의 인터페이스 슬롯)**으로 결정된다는 점, 제네릭 제약이 슬롯을 고르는 방식.
  • struct + 명시적 구현의 박싱 비용과, IDisposable/IEnumerable 같은 실제 프레임워크 관용구.