명시적 인터페이스
Coddy C# 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 70개 중 28번째.
클래스가 이름이 같은 멤버를 포함하는 여러 인터페이스를 구현할 때, 이름 충돌이 발생합니다. 명시적 인터페이스 구현(Explicit interface implementation)은 각 인터페이스에 대해 별도의 구현을 제공할 수 있도록 하여 이 문제를 해결합니다.
명시적 구현을 사용하면, 멤버 이름 앞에 인터페이스 이름을 접두사로 붙이고 접근 제한자를 생략합니다:
public interface IAmericanPlug
{
void Connect();
}
public interface IEuropeanPlug
{
void Connect();
}
public class UniversalAdapter : IAmericanPlug, IEuropeanPlug
{
void IAmericanPlug.Connect()
{
Console.WriteLine("Connected to 120V");
}
void IEuropeanPlug.Connect()
{
Console.WriteLine("Connected to 220V");
}
}명시적으로 구현된 멤버는 클래스 자체가 아니라 인터페이스 형식의 변수를 통해서만 액세스할 수 있습니다:
var adapter = new UniversalAdapter();
// adapter.Connect(); // 컴파일되지 않습니다!
IAmericanPlug american = adapter;
american.Connect(); // 120V에 연결됨
IEuropeanPlug european = adapter;
european.Connect(); // 220V에 연결됨이 기술은 클래스의 공개 API에서 인터페이스 멤버를 숨기고자 할 때도 유용합니다. 메서드가 객체를 특정 인터페이스로 취급할 때만 의미가 있는 경우, 명시적 구현(explicit implementation)을 사용하면 계약을 충족하면서도 클래스의 노출 영역(surface area)을 깔끔하게 유지할 수 있습니다.
챌린지
쉬움명시적 인터페이스 구현(explicit interface implementation)을 통해 하나의 클래스가 사용되는 인터페이스에 따라 어떻게 서로 다른 동작을 제공할 수 있는지 보여주는 보고서 생성 시스템을 만들어 보겠습니다. 화면 표시용과 인쇄용으로 각각 다르게 형식이 지정되는 문서를 만들 것입니다.
코드는 다음 네 개의 파일로 구성됩니다:
IDisplayable.cs:Reports네임스페이스 안에IDisplayable이라는 인터페이스를 정의합니다. 이 인터페이스는 화면에 표시될 수 있는 콘텐츠를 나타냅니다. 문자열을 반환하는Render()메서드를 선언해야 합니다.IPrintable.cs:Reports네임스페이스 안에IPrintable이라는 인터페이스를 정의합니다. 이 인터페이스는 프린터로 전송될 수 있는 콘텐츠를 나타냅니다. 이 인터페이스 또한 문자열을 반환하는Render()메서드를 선언해야 합니다.IDisplayable과 메서드 이름이 충돌한다는 점에 유의하세요!Report.cs:Reports네임스페이스 안에IDisplayable과IPrintable을 모두 구현하는Report클래스를 정의합니다. 보고서는Title속성(string)과Content속(string)을 가져야 하며, 두 값을 모두 설정하는 생성자가 있어야 합니다. 명시적 인터페이스 구현을 사용하여 서로 다른Render()동작을 제공하세요:- 화면 표시용으로 렌더링될 때:
"[SCREEN] {Title}: {Content}" - 인쇄용으로 렌더링될 때:
"[PRINT] {Title} | {Content}"
- 화면 표시용으로 렌더링될 때:
Program.cs: 메인 파일에서 입력값을 사용하여Report객체를 생성합니다. 명시적 구현은 인터페이스 변수를 통해서만 접근할 수 있으므로, 동일한 보고서 객체를IDisplayable변수와IPrintable변수에 각각 저장하세요. 그런 다음 각 변수에서Render()를 호출하여 동일한 객체가 사용하는 인터페이스에 따라 어떻게 다른 출력을 생성하는지 확인합니다.
두 개의 입력을 받게 됩니다:
- 보고서 제목 (Report title)
- 보고서 내용 (Report content)
출력은 다음 형식으로 인쇄하세요:
Display version:
{Render() via IDisplayable}
Print version:
{Render() via IPrintable}예를 들어, 입력값이 Sales Report와 Q4 revenue increased by 15%라면, 출력은 다음과 같아야 합니다:
Display version:
[SCREEN] Sales Report: Q4 revenue increased by 15%
Print version:
[PRINT] Sales Report | Q4 revenue increased by 15%동일한 Report 객체가 접근하는 인터페이스 참조에 따라 어떻게 다르게 동작하는지 확인해 보세요. 이것이 바로 이름 충돌을 해결하면서 상황에 적합한 동작을 제공하는 명시적 인터페이스 구현의 힘입니다!
치트 시트
클래스가 이름이 같은 멤버를 가진 여러 인터페이스를 구현할 때, 명시적 인터페이스 구현(explicit interface implementation)을 사용하여 각 인터페이스에 대해 별도의 구현을 제공할 수 있습니다.
인터페이스 멤버를 명시적으로 구현하려면 멤버 이름 앞에 인터페이스 이름을 접두사로 붙이고 액세스 한정자를 생략합니다.
public interface IAmericanPlug
{
void Connect();
}
public interface IEuropeanPlug
{
void Connect();
}
public class UniversalAdapter : IAmericanPlug, IEuropeanPlug
{
void IAmericanPlug.Connect()
{
Console.WriteLine("Connected to 120V");
}
void IEuropeanPlug.Connect()
{
Console.WriteLine("Connected to 220V");
}
}명시적으로 구현된 멤버는 클래스 인스턴스 자체를 통해서는 액세스할 수 없으며, 인터페이스 형식의 변수를 통해서만 액세스할 수 있습니다.
var adapter = new UniversalAdapter();
// adapter.Connect(); // 컴파일되지 않습니다!
IAmericanPlug american = adapter;
american.Connect(); // Connected to 120V
IEuropeanPlug european = adapter;
european.Connect(); // Connected to 220V이 기법은 다음과 같은 경우에 유용합니다.
- 인터페이스 멤버 간의 이름 충돌 해결
- 특정 인터페이스 컨텍스트에서만 의미가 있는 인터페이스 멤버를 클래스의 공개 API에서 숨기기
직접 해보기
using System;
using Reports;
class Program
{
public static void Main(string[] args)
{
// 입력 읽기
string title = Console.ReadLine();
string content = Console.ReadLine();
// TODO: title과 content를 사용하여 Report 객체 생성
// TODO: 리포트를 IDisplayable 변수에 저장
// TODO: 동일한 리포트를 IPrintable 변수에 저장
// TODO: IDisplayable의 Render()를 호출하여 디스플레이 버전 출력
Console.WriteLine("Display version:");
// 여기에 렌더링 결과 출력
// TODO: IPrintable의 Render()를 호출하여 프린트 버전 출력
Console.WriteLine("Print version:");
// 여기에 렌더링 결과 출력
}
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.