핵심 정리
public interface IDamageable
{
void TakeDamage(int amount);
int Health { get; }
}
public class Enemy : IDamageable
{
public int Health { get; private set; } = 100;
public void TakeDamage(int amount) => Health -= amount;
}
// 인터페이스 타입으로 다루기
void Hit(IDamageable target, int dmg) => target.TakeDamage(dmg);
Hit(new Enemy(), 10); // Enemy를 IDamageable로 취급문법
어떤 역할에 interface를 쓰나
interface는 아래 세 경우에서 가장 자주 씁니다.
public interface ILogger { void Log(string message); } // 계약
public class FileLogger : ILogger { ... } // 구현
void Write(ILogger logger) => logger.Log("saved"); // 교체 가능한 의존성- 여러 구현체를 같은 계약으로 다루고 싶을 때
- 의존성 주입 경계를 만들 때
- 테스트에서 대체 구현이나 mock이 필요할 때
인터페이스는 "할 수 있어야 하는 것"의 계약
interface 는 타입이 제공해야 하는 기능 목록만 정의하고 구현을 포함하지 않습니다. 클래스는 한 번에 여러 인터페이스를 구현할 수 있습니다. 반면 클래스 상속은 하나만 가능합니다.
public interface IMovable { void Move(float dx, float dy); }
public interface IAttacker { void Attack(IDamageable target); }
// 여러 인터페이스 동시 구현
public class Warrior : IMovable, IAttacker, IDamageable
{
public int Health { get; private set; } = 200;
public void TakeDamage(int amount) => Health -= amount;
public void Move(float dx, float dy) { /* ... */ }
public void Attack(IDamageable target) => target.TakeDamage(30);
}내부 동작 — vtable 기반 다형성
인터페이스 타입 변수를 통해 메서드를 호출하면 vtable(가상 메서드 테이블) 을 참조해 실제 구현 메서드를 찾습니다. 이것이 동적 디스패치입니다. 컴파일 타임에 어느 클래스인지 몰라도 런타임에 올바른 구현이 호출됩니다.
IDamageable target = new Enemy(); // Enemy 객체를 IDamageable로 참조
target.TakeDamage(10); // 런타임에 Enemy.TakeDamage 호출
IDamageable target2 = new Boss(); // Boss도 IDamageable 구현
target2.TakeDamage(10); // 런타임에 Boss.TakeDamage 호출
// 다형성 — 같은 인터페이스, 다른 구현체
var targets = new List<IDamageable> { new Enemy(), new Boss() };
foreach (var t in targets) t.TakeDamage(5); // 각각 다른 구현 호출기본 구현 메서드 (C# 8+)
인터페이스에 기본 구현을 포함할 수 있습니다. 기존 인터페이스에 메서드를 추가할 때 구현체를 모두 바꾸지 않아도 되는 하위 호환성 기능입니다.
public interface IDamageable
{
void TakeDamage(int amount);
int Health { get; }
// 기본 구현 — 구현체가 override하지 않으면 이것이 호출됨
bool IsAlive() => Health > 0;
}명시적 인터페이스 구현
같은 이름의 멤버가 두 인터페이스에 있을 때 명시적 구현으로 구분합니다.
interface IA { void Do(); }
interface IB { void Do(); }
class C : IA, IB
{
void IA.Do() => Console.WriteLine("A");
void IB.Do() => Console.WriteLine("B");
}
var c = new C();
((IA)c).Do(); // "A"
((IB)c).Do(); // "B"// ❌ 구현체 전용 멤버는 interface 타입에서 바로 안 보인다
ILogger logger = new FileLogger();
// logger.Path
// ✅ 계약에 들어 있는 멤버만 접근 가능
logger.Log("saved");선택 기준
| 상황 | 더 적합한 선택 |
|---|---|
| 여러 클래스가 교체 가능해야 함 | interface |
| 공통 상태와 구현을 함께 재사용 | abstract class |
| 하위 호환성 있는 인터페이스 확장 | interface 기본 구현 메서드 |
| 구현이 하나뿐, 외부 계약 불필요 | 일반 class |
체크포인트
| 항목 | 설명 |
|---|---|
interface | 기능 계약 정의, 구현 없음 (기본 구현 제외) |
: IFoo | 인터페이스 구현 선언 |
| 다중 구현 | 여러 인터페이스 동시 구현 가능 |
| vtable | 런타임에 실제 구현을 찾는 동적 디스패치 |
| 명시적 구현 | 이름 충돌 시 IFoo.Method() 형태로 구분 |
주의할 점
인터페이스는 "교체 가능성이 실제로 존재하는 경계" 에만 사용하는 것이 좋습니다. 단순히 "모든 클래스에 I 접두사를 붙이는" 습관적 적용은 코드를 복잡하게만 만들고 실질적 이점이 없습니다.
인터페이스는 인스턴스 상태를 가질 수 없습니다. 데이터를 담는 용도가 아니라 행위 계약을 정의하는 용도로 사용하세요.
참고 링크
2 sources