CodeGym /행동 /C# SELF /인터페이스의 Default-메서드

인터페이스의 Default-메서드

C# SELF
레벨 24 , 레슨 0
사용 가능

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 이전
void DoSomething();
C# 8 이상
void DoSomething() { Console.WriteLine("작동 중!"); }

문법에서 중요한 디테일

  • 구현이 있는 메서드는 반드시 중괄호로 본문을 써야 해.
  • 기본 메서드는 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();
    }
}

남용하지 마!
기본 메서드는 하위 호환성에 좋지만, 너무 남용하면 로직이 인터페이스에 흩어져서 "지저분한" 아키텍처가 될 수 있어. 중요한 건 클래스에, 인터페이스는 진짜 "계약" 용도로만 쓰는 게 좋아.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION