Quick Reference
interface는 클래스나 struct가 제공해야 할 메서드·프로퍼티·이벤트·인덱서의 계약입니다. 하나의 타입은 기반 클래스 하나만 상속하지만 여러 interface를 구현할 수 있습니다.
public interface INotifier
{
void Notify(string message);
}
public sealed class ConsoleNotifier : INotifier
{
public void Notify(string message) => Console.WriteLine(message);
}
static void Send(INotifier notifier) => notifier.Notify("saved");- 서로 다른 종류의 타입에 같은 능력을 붙이면 interface를 고릅니다.
- 공유 상태·생성자·비공개 보조 동작까지 물려줘야 하면
abstract class를 고릅니다. - 기본 interface member는 기존 구현체를 즉시 깨지 않게 하는 버전 관리 수단이며, 구현체 변수에서는 바로 보이지 않을 수 있습니다.
static abstractmember는 인스턴스 호출이 아니라 제네릭 type parameter 제약을 통한 정적 계약입니다.
계약을 구현하는 방법
구현 타입은 abstract member를 모두 제공해야 합니다. interface 참조에서는 계약에 선언한 멤버만 보이므로, 호출자는 구현 클래스의 부가 기능에 의존하지 않습니다.
public interface IDamageable
{
int Health { get; }
void TakeDamage(int amount);
}
public sealed class Enemy : IDamageable
{
public int Health { get; private set; } = 100;
public void TakeDamage(int amount)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
Health = Math.Max(0, Health - amount);
}
}
static void Hit(IDamageable target, int amount) => target.TakeDamage(amount);명시적 구현은 이름이 충돌하거나 구현체의 일반 public API에 계약 멤버를 노출하고 싶지 않을 때 씁니다. 이 멤버는 해당 interface로 변환한 뒤에만 호출합니다.
public interface IMetric { double Distance(); }
public interface IImperial { double Distance(); }
public sealed class Runway(double meters) : IMetric, IImperial
{
double IMetric.Distance() => meters;
double IImperial.Distance() => meters * 3.28084;
}
double feet = ((IImperial)new Runway(100)).Distance();기본 멤버와 정적 계약
기본 interface member는 interface 자체에 본문을 둡니다. 구현 클래스가 같은 시그니처의 public 멤버를 제공하면 그 구현을 사용하지만, 기본 본문만 있는 멤버는 interface 참조를 통해 호출합니다.
public interface IHealthCheck
{
bool IsHealthy() => true;
}
IHealthCheck check = new DefaultHealthCheck();
bool healthy = check.IsHealthy();
public sealed class DefaultHealthCheck : IHealthCheck { }기존 public interface에 추상 멤버를 추가하면 모든 구현체가 깨집니다. 기본 구현은 그 비용을 줄일 수 있지만, 호출자가 기대할 동작과 구현체별 재정의 필요성을 먼저 정해야 합니다.
static abstract member는 C# 11부터 정적 API에도 계약을 만들 때 사용합니다. 호출은 interface 인스턴스가 아니라 제약된 T를 통해 이뤄집니다.
public interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
static abstract TSelf Parse(string text);
}
static T Parse<T>(string text) where T : IParsable<T> => T.Parse(text);선택과 수명
| 필요 | 선택 | 이유 |
|---|---|---|
| 관련 없는 타입의 공통 능력 | interface | 다중 계약과 교체 가능한 호출 경계 |
| 공통 상태·생성 규칙·protected 보조 동작 | abstract class | 구현과 상태를 한 계층에서 관리 |
| 하나의 구현을 조립·교체 | composition | 상속 계층 없이 정책을 교체 |
| 두 계약의 동일 멤버 이름 | explicit implementation | interface별 의미를 분리 |
테스트를 위해 모든 클래스 앞에 interface를 만들 필요는 없습니다. 구현체가 하나이고 교체 가능성이나 외부 경계가 없다면 일반 class가 더 단순합니다. 반대로 public interface의 멤버를 변경하면 구현체뿐 아니라 소비자도 영향을 받으므로, 작은 역할 단위로 유지합니다.
자주 틀리는 부분
interface는 인스턴스 필드나 인스턴스 생성자를 선언할 수 없습니다. 상태를 공유해야 한다는 이유만으로 default member를 늘리기보다 abstract class나 합성을 검토하세요.
interface 호출의 내부 디스패치 방식이나 JIT 최적화는 언어 계약이 아닙니다. 호출 성능이 아니라 계약·대체 가능성·버전 비용을 기준으로 선택합니다.
참고 링크
2 sources