1. "순수 계약"에서 유연한 아키텍처로
C# 8 이전에는 인터페이스가 완전한 계약서였어. 인터페이스를 구현하려면 마지막 콤마까지 다 구현해야 했지. 만약 새로운 멤버가 추가되면, 기존 구현체들은 전부 그걸 추가해야 했고, 안 그러면 컴파일러가 프로젝트 빌드를 막았어.
근데 현실은 더 복잡하지. 예를 들어, 네가 수백 개 프로젝트에서 쓰는 라이브러리를 관리하고 있는데, 갑자기 인터페이스에 새로운 메서드를 추가해야 해. 하위 호환성 깨고 싶지 않지? 이럴 때 기본 메서드(Default Interface Methods, DIM)가 등장하는 거야!
핵심이 뭐냐면?
기본 메서드는 인터페이스 안에서 구현을 바로 쓸 수 있게 해줘. 이제 계약이 더 유연해졌지. 만약 클래스가 "신상" 메서드를 구현 안 하면, 기본 구현이 대신 동작해. 영화에서 대역 배우처럼 — 배우가 다리에서 뛰기 싫으면 스턴트맨이 대신 뛰는 거지.
2. Default Interface Methods 문법
인터페이스에서 구현이 있는 메서드는 어떻게 선언해?
거의 일반 메서드랑 똑같아. 이제 메서드 본문을 인터페이스 안에 (꼭!) 써주면 돼:
public interface ILogger
{
void Log(string message);
// 기본 구현이 있는 새로운 메서드!
void LogWarning(string message)
{
Log("[WARNING] " + message);
}
}
여기서 LogWarning은 이미 구현이 들어있지! ILogger를 구현하는 클래스는 Log만 구현하면 되고, LogWarning은 (자기만의 게 없으면) 기본 구현이 동작해.
비교: 클래식 vs. 최신 시그니처
| 버전 | 인터페이스 선언 |
|---|---|
| C# 8 이전 | |
| C# 8 이상 | |
문법에서 중요한 디테일
- 구현이 있는 메서드는 반드시 중괄호로 본문을 써야 해.
- 기본 메서드는 abstract일 수 없어.
- 인터페이스의 모든 메서드는 여전히 암시적으로 public이야.
- get/set이 있는 기본 프로퍼티도 선언할 수 있어 (아래 참고).
3. 실전 예제
예제 1. 하위 호환성 보장하기
예를 들어, 네 앱에 데이터 저장용 인터페이스가 있다고 해보자:
public interface ISaveable
{
void Save(string filePath);
}
나중에 클라우드 저장 기능을 추가하고 싶어졌어. 수십 개 클래스를 다 고치기 싫지? 기본 메서드를 추가해!
public interface ISaveable
{
void Save(string filePath);
// "기본" 구현이 있는 새로운 메서드!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"클라우드 {cloudService}에 저장 중 (기본값 — 아무것도 안 함)");
}
}
이제 기존 클래스들도 자동으로 "클라우드 저장"을 지원해 (일단 메시지만 출력하더라도).
예제 2. 로거 인터페이스 확장하기
전에 간단한 로깅 인터페이스가 있었지:
public interface ILogger
{
void Log(string message);
}
에러 로깅용 기본 메서드를 추가해보자:
public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log("[ERROR] " + message);
}
}
ILogger를 구현하는 클래스는 LogError를 구현 안 해도 돼 — 기본 버전이 동작할 거야:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// LogError는 구현 안 함 — 기본 구현이 동작!
}
ILogger logger = new ConsoleLogger();
logger.Log("다 잘 되고 있어!");
logger.LogError("어이쿠, 뭔가 잘못됐어!"); // 기본 구현 호출됨
예제 3. 기본 메서드 + 확장 가능한 앱
네 앱이 여러 타입의 내보내기를 지원한다고 해보자: 파일, DB, 네트워크 등. 인터페이스:
public interface IExporter
{
void Export(string data, string destination);
// 새로운 기능 — 아카이브로 내보내기
void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("기본적으로 아카이브 지원 안 함.");
}
}
다른 개발자가 만든 플러그인도, 이 새로운 메서드를 몰라도 계속 잘 동작할 거야.
4. 기본 인터페이스 메서드 호출은 어떻게 동작해?
"옛날 클래스 — 새로운 인터페이스" 시나리오
클래스가 인터페이스의 기본 메서드를 구현 안 하면, 인터페이스 참조로 호출할 때 인터페이스 구현이 사용돼. 만약 구현하면, 그 클래스의 게 사용되고.
public class FileExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("파일에 저장 중...");
}
// ExportToArchive는 구현 안 함 — 기본 출력 사용
}
IExporter exporter = new FileExporter();
exporter.Export("데이터", "file.txt"); // FileExporter 구현 동작
exporter.ExportToArchive("데이터", "file.zip"); // 기본 구현 동작!
"클래스가 기본 메서드를 오버라이드" 시나리오
public class AdvancedExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("고급 모드로 저장 중...");
}
public void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("아카이브 지원됨!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("데이터", "file.zip"); // 이제 클래스 구현이 호출됨!
5. Default Interface Methods로 뭘 더 할 수 있을까?
기본 프로퍼티와 이벤트
get/set 본문이 있으면 기본 구현 프로퍼티도 선언할 수 있어:
public interface IHasId
{
// 오버라이드 전까지 자동으로 42 반환
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
인터페이스 코드에서 기본 메서드 호출
인터페이스 안에서 기본 메서드나 다른 멤버를 서로 호출할 수 있어:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Default Interface Methods의 제한과 특징
필드, 생성자 선언 가능?
안 돼. 기본 메서드가 있어도 인터페이스는 여전히 클래스가 아니야. 필드, 생성자, 소멸자는 못 써.
base를 인터페이스에 쓸 수 있을까?
쓸 수는 있는데, 약간 트릭이 있어. 기본 메서드 안에서 부모 인터페이스 메서드를 명시적으로 호출할 수 있어:
public interface IBase
{
void Greet() => Console.WriteLine("IBase에서 안녕");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("IDerived에서 안녕!");
IBase.Greet(this); // 부모 인터페이스 메서드 명시적 호출
}
}
근데 이런 건 기본적인 시나리오에선 거의 안 써.
기본 구현 충돌 시엔 어떻게 돼?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// 클래스가 Foo를 명시적으로 구현 안 함:
public class C : IA, IB { }
// 컴파일 에러: 어떤 구현을 쓸지 모호함!
7. 흔한 실수, 제한, 특징
흔한 실수:
DIM을 알게 된 뒤 인터페이스에 필드를 선언하려는 경우가 있는데 — 여전히 필드는 안 돼. 그리고 기본 static 메서드를 구현하려고 하면 — C# 8 이전엔 불가능. (8부터는 static 메서드가 인터페이스에 가능하지만, static 메서드의 기본 구현은 또 별개의 얘기야.)
특징: Diamond Problem ("다이아몬드 문제")
클래스가 동일한 기본 메서드를 가진 두 인터페이스를 구현하면, 반드시 그 메서드를 명시적으로 구현해야 해:
public class ConflictClass : IA, IB
{
public void Foo() // 직접 어떤 걸 쓸지 골라야 함!
{
// 필요한 구현을 인터페이스로 명시적 호출 (필요하다면)
((IA)this).Foo();
// 또는
((IB)this).Foo();
}
}
남용하지 마!
기본 메서드는 하위 호환성에 좋지만, 너무 남용하면 로직이 인터페이스에 흩어져서 "지저분한" 아키텍처가 될 수 있어. 중요한 건 클래스에, 인터페이스는 진짜 "계약" 용도로만 쓰는 게 좋아.
GO TO FULL VERSION