인터페이스는 계약입니다. 타입이 제공하겠다고 약속하는 멤버의 이름 붙은 목록입니다. 타입이 무엇을 할 수 있는지만 말하고 어떻게 하는지는 말하지 않습니다. 클래스와 구조체가 인터페이스를 구현하고, 인터페이스를 대상으로 작성한 코드는 나중에 작성될 것을 포함해 모든 구현과 동작합니다.
인터페이스 선언과 구현
인터페이스는 본문 없이 멤버를 선언합니다. 관례상 이름은 I로 시작합니다. 클래스는 콜론 뒤에 인터페이스를 나열하고 각 멤버에 대해 public 멤버를 제공해 구현합니다:
출력:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice는 PercentOff나 FixedOff에 대해 아무것도 모릅니다. IDiscount에만 의존하므로, 나중에 BuyOneGetOne 클래스를 추가해도 바꿀 필요가 없습니다. 이 분리가 인터페이스가 존재하는 이유의 전부입니다.
선언에 관한 몇 가지 규칙:
- 인터페이스 멤버는 기본적으로 public이고, 구현하는 멤버도 public이어야 합니다(아래의 명시적 구현이 아니라면).
- 인터페이스는 메서드, 속성, 인덱서, 이벤트를 선언할 수 있습니다. 인스턴스 필드나 생성자는 선언할 수 없습니다.
- 멤버를 빠뜨린 클래스는 컴파일되지 않습니다: CS0535 오류 "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
- 인터페이스를
new로 만들 수 없습니다. 구현 클래스를 만들고 인터페이스 타입의 변수에 담을 수 있습니다:IDiscount d = new FixedOff(5);.
타입으로서의 인터페이스
변수, 매개변수, 필드, 반환 타입을 인터페이스로 선언할 수 있습니다. 그러면 그것을 구현하는 모든 객체를 받고, 인터페이스의 멤버만 노출합니다. 어떤 구현을 받든 상관없는 코드를 작성하는 방법이자, 테스트에서 가짜로 바꿔 끼우는 방법입니다:
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
ASP.NET Core의 의존성 주입 컨테이너가 이것 위에 만들어져 있습니다. 서비스는 인터페이스로 등록되고 요청됩니다.
여러 인터페이스 구현하기
클래스는 기본 클래스를 최대 하나 갖지만 인터페이스는 몇 개든 구현할 수 있습니다. 기본 클래스가 먼저, 그다음 인터페이스를 쉼표로 구분합니다:
출력:
INVOICE Studio rent March: 950.00
True
True
인터페이스는 다른 인터페이스를 상속할 수도 있습니다. interface IRepository<T> : IReadRepository<T>는 상속받은 멤버에 멤버를 더하며, IRepository<T>를 구현하는 클래스는 두 집합을 모두 제공해야 합니다.
명시적 인터페이스 구현
때로는 두 인터페이스가 같은 이름에 다른 의미의 멤버를 선언하거나, 인터페이스 멤버가 클래스 자체의 공개 표면에서는 말이 되지 않습니다. 인터페이스 이름을 앞에 붙이고 접근 한정자를 빼서 명시적으로 구현하세요:
출력:
Desk lamp,34.90
Desk lamp for 34.90
명시적으로 구현한 멤버는 인터페이스를 통해서만 접근할 수 있습니다. 프레임워크는 이를 써서 흔한 타입의 잡음을 줄입니다. 배열은 IList.Add를 명시적으로 구현하므로(배열은 크기가 고정이라 NotSupportedException을 던집니다), array.를 입력할 때 그 메서드가 보이지 않습니다.
구현하게 될 프레임워크 인터페이스
표준 인터페이스를 구현하면 타입이 기존 프레임워크 코드에 연결됩니다. 세 가지가 끊임없이 등장합니다.
**IComparable<T>**는 타입에 자연 순서를 주며, List<T>.Sort(), Array.Sort, Max()가 이를 씁니다. CompareTo는 음수, 0, 양수를 반환합니다:
출력:
1.4, 2.9, 2.10, 10.0
같은 버전을 문자열로 정렬하면 1.4, 10.0, 2.10, 2.9가 됩니다. IComparable<T>가 없으면 Sort()는 두 Version 객체를 비교할 방법이 없으므로 InvalidOperationException을 던집니다.
**IEnumerable<T>**는 타입을 foreach와 LINQ에서 쓸 수 있게 만듭니다. GetEnumerator를 구현하는 쉬운 방법은 yield return이며, IEnumerable과 yield 페이지에서 다룹니다.
**IDisposable**은 해제해야 하는 무언가(파일 핸들, 연결, 타이머)를 붙잡고 있는 타입을 표시합니다. 유일한 메서드 Dispose는 예외가 발생하더라도 블록이 끝날 때 using 문이 호출하는 것입니다:
출력:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
기본 인터페이스 메서드(C# 8)
예전에는 공개한 인터페이스에 멤버를 추가하면 그것을 구현하는 모든 클래스가 깨졌습니다. C# 8부터는 인터페이스 멤버가 본문을 가질 수 있으며, 구현하는 클래스는 자기 버전을 제공하지 않는 한 이를 물려받습니다:
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
마지막 줄이 사람들을 놀라게 하는 부분입니다. 기본 메서드는 클래스 자체의 멤버로 상속되지 않으므로 인터페이스 타입을 통해서만 호출할 수 있습니다. 기본 메서드는 인스턴스 필드도 쓸 수 없고(인터페이스에는 없으니까요), 다른 인터페이스 멤버를 통해 일합니다. .NET Core 3.0 이상이 필요하며 .NET Framework에서는 쓸 수 없습니다.
C# 8은 인터페이스의 정적 멤버도 허용했고, C# 11은 제네릭 코드가 T의 정적 메서드나 연산자를 호출할 수 있게 해 주는 정적 추상 멤버를 추가했습니다. .NET 7의 제네릭 수학(INumber<T>, where T : INumber<T>)이 이 위에 만들어져 있습니다.
인터페이스인가 추상 클래스인가?
서로 관련 없는 타입이 공유할 수 있는 능력, 구조체가 참여해야 할 때, 클래스에 그런 역할이 여러 개 필요할 때는 인터페이스를 쓰세요. 구현들이 한 가지의 변형이고 상태나 고정된 알고리즘을 공유할 때는 추상 클래스를 쓰세요. 추상 클래스 페이지에 나란히 비교한 표가 있습니다. 많은 설계가 둘 다 씁니다. 소비하는 쪽을 위한 인터페이스와, 구현하는 쪽을 위해 지루한 부분을 구현해 주는 추상 기본 클래스입니다.
흔한 실수
- 빠지거나 public이 아닌 구현. 모든 인터페이스 멤버에는 시그니처가 맞는 public 멤버나 명시적 구현이 필요합니다. 그렇지 않으면 CS0535(메서드가 있지만 public이 아니면 CS0737)입니다.
- "혹시 몰라서" 구현이 하나뿐인 인터페이스. 인터페이스는 구현이 여럿이거나 테스트용 가짜가 있을 때 제 몫을 합니다. 그렇지 않으면 탐색할 파일이 하나 더 늘 뿐입니다.
- 비대한 인터페이스. 멤버가 스무 개인 인터페이스는 모든 구현자에게 스무 개의 멤버를 쓰게 합니다. 클래스가 조합할 수 있는 더 작은 역할(
IReader,IWriter)로 나누세요. - 인터페이스를 다시 클래스로 캐스트하기.
((StripeGateway)gateway).Refund()는 분리를 되돌립니다. 호출하는 쪽에Refund가 필요하다면 인터페이스에 있어야 합니다. - 클래스에서 기본 인터페이스 메서드를 호출하리라 기대하기. 인터페이스 타입을 통해 접근합니다.
자주 묻는 질문
C#에서 인터페이스란 무엇인가요?
인터페이스는 타입이 제공하겠다고 약속하는, 인스턴스 상태 없는 멤버(메서드, 속성, 이벤트, 인덱서)의 이름 붙은 집합입니다. interface IDiscount { decimal Apply(decimal price); }는 "IDiscount인 것은 무엇이든 가격에 자신을 적용할 수 있다"고 말합니다. 클래스와 구조체가 콜론으로 이를 구현하면, 코드는 인터페이스 타입을 통해 그 모두를 다룰 수 있습니다.
C#에서 인터페이스는 어떻게 구현하나요?
클래스 선언에서 콜론 뒤에(기본 클래스가 있다면 그 뒤에) 인터페이스를 나열하고, 각 인터페이스 멤버에 대해 public 멤버를 제공합니다: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. 빠진 멤버가 있으면 CS0535 오류입니다. Visual Studio와 Rider는 빠른 수정으로 스텁을 생성해 줍니다.
C# 클래스는 여러 인터페이스를 구현할 수 있나요?
네, 필요한 만큼 쉼표로 구분해 구현할 수 있습니다: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. 이것이 다중 상속에 대한 C#의 답입니다. 클래스는 기본 클래스를 하나 갖지만 여러 역할을 할 수 있습니다. 두 인터페이스가 같은 멤버를 선언하면 public 메서드 하나로 둘 다 만족시키거나, 명시적 구현으로 각자에게 따로 줄 수 있습니다.
C#의 명시적 인터페이스 구현이란 무엇인가요?
앞에 인터페이스 이름을 붙이고 접근 한정자 없이 멤버를 구현하는 것입니다: string IExportable.Format() { ... }. 그러면 그 멤버는 클래스가 아니라 인터페이스 타입의 변수를 통해서만 호출할 수 있습니다. 두 인터페이스 사이의 이름 충돌을 해결하고, 거의 쓰지 않는 인터페이스 멤버를 클래스의 공개 표면에서 빼 두는 데 쓰입니다.
C#의 기본 인터페이스 메서드란 무엇인가요?
C# 8부터 인터페이스 멤버가 본문을 가질 수 있습니다: void Error(string m) => Write("ERROR: " + m);. 구현하는 클래스는 이를 공짜로 얻고, 자기 버전을 제공할 수도 있습니다. 라이브러리 작성자가 모든 구현체를 깨뜨리지 않고 이미 공개한 인터페이스에 멤버를 추가할 수 있도록 존재하는 기능입니다. 기본 메서드는 클래스가 아니라 인터페이스 타입을 통해 접근합니다.
C# 인터페이스 이름이 I로 시작하는 이유는 무엇인가요?
IEnumerable, IDisposable, IComparable<T>처럼 .NET의 명명 규칙입니다. 대문자 I 뒤에 PascalCase 이름을 붙이며, 흔히 "-able"로 끝나는 형용사입니다. 컴파일러가 요구하지는 않지만, 따르면 인터페이스 타입을 한눈에 알아볼 수 있습니다. 특히 기본 클래스와 인터페이스가 한 목록을 공유하는 클래스 선언에서 그렇습니다.