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 멤버이자, 명시적 구현이 없는 인터페이스 슬롯을 채우는 후보. 여기서는 두 인터페이스 모두 명시적 구현이 있으므로 publicMove()는 인터페이스 슬롯이 아니라 그냥 클래스 자체 메서드로 존재한다. - 인터페이스 디스패치: 인터페이스 타입 참조로 호출하면 그 인터페이스의 슬롯(=명시적 구현)으로 간다.
단계별 동작
a.Move()→walk(public): 정적 타입이Amphibian이라 클래스의 publicMove()를 호출. 명시적 멤버는 이 경로로 보이지 않는다.((IWalker)a).Move()→walk(IWalker):IWalker로 캐스트 후 호출 →IWalker.Move()슬롯 → 명시적 구현.((ISwimmer)a).Move()→swim(ISwimmer): 마찬가지로ISwimmer.Move()슬롯.IWalker w = a; w.Move()→walk(IWalker): 정적 타입이 인터페이스이므로 해당 슬롯으로 디스패치.ISwimmer s = a; s.Move()→swim(ISwimmer): 동일 원리.CallMove<T>(a) where T:IWalker→walk(IWalker): 제네릭 제약이IWalker라t.Move()는IWalker.Move()로 디스패치. 제약 인터페이스가 곧 슬롯을 결정한다.
흔한 오해·함정
- "public Move()가 인터페이스도 만족하니 그게 불리겠지": 아니다. 명시적 구현이 있으면 그것이 해당 인터페이스 슬롯을 덮어쓴다. 인터페이스 경로는 항상 명시적 구현으로 간다.
- 명시적 멤버에
public을 못 붙임: 명시적 구현은 접근 한정자를 가질 수 없고, 인스턴스에서 직접 호출 불가. 이는 "인터페이스로만 노출하고 클래스 public API 는 깔끔히 유지"하는 데 쓰인다(예:IDisposable.Dispose()를 숨기고Close()만 public 으로). - 박싱된 구조체: struct 가 인터페이스를 명시적 구현하면 인터페이스로 호출 시 박싱이 일어난다(값 복사 + 힙). 핫패스에서 주의.
- 이름 충돌 해결: 두 인터페이스가 같은 시그니처를 요구할 때 서로 다른 동작이 필요하면 명시적 구현이 유일한 방법. 동작이 같아도 되면 public 하나로 둘 다 만족시킬 수 있다.
면접 포인트
- 명시적 인터페이스 구현이 필요한 두 가지 이유: (1) 같은 시그니처 다중 인터페이스의 동작 분리, (2) 인터페이스 멤버를 클래스 public 표면에서 숨기기.
- 디스패치가 **정적 타입(호출 경로의 인터페이스 슬롯)**으로 결정된다는 점, 제네릭 제약이 슬롯을 고르는 방식.
- struct + 명시적 구현의 박싱 비용과,
IDisposable/IEnumerable같은 실제 프레임워크 관용구.