본문 바로가기

C#

C# 인터페이스의 기본 구현(Default Interface Implementation) 활용

인터페이스의 기본 구현(Default Interface Implementation, DII)은 C# 8부터 도입된 기능으로, 인터페이스 멤버에 기본 구현을 제공할 수 있습니다. 이를 통해 인터페이스의 버전 호환성을 높이고, 공통 동작을 재사용하며, 구현 클래스에 선택적 오버라이드를 허용합니다. .NET Core 3.0 이상 또는 .NET 5+ 런타임에서 지원합니다.

1. 기본 개념과 장점

기본 구현이 있는 인터페이스는 구현 클래스가 해당 멤버를 명시적으로 구현하지 않아도 동작합니다. 주된 장점은 다음과 같습니다.

  • 이진 호환성: 인터페이스에 메서드를 추가해도 기존 구현체가 깨지지 않습니다.
  • 코드 중복 감소: 공통 동작을 인터페이스에서 한 번 정의합니다.
  • 선택적 오버라이드: 필요할 때만 구현 클래스에서 재정의합니다.

2. 기본 예제

구현체가 메서드를 제공하지 않으면 인터페이스 기본 구현이 실행됩니다.

using System; 

public interface ILogger
{
    void Log(string message)
    {
        Console.WriteLine($"[Default] {message}");
    }
}

public class FileLogger : ILogger { /* 기본 구현 사용 */ }

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[Console] {message}");
    }
}

public static class Program
{
    public static void Main()
    {
        ILogger a = new FileLogger();
        ILogger b = new ConsoleLogger();

        a.Log("Hello");   // [Default] Hello
        b.Log("Hello");   // [Console] Hello
    }
}

3. 버전 관리: 기존 구현체를 깨지 않기

인터페이스에 새 메서드를 추가하면서 기본 구현을 제공하면, 기존 구현체는 수정 없이도 동작합니다.

using System;
using System.Collections.Generic;

public interface ICache
{
    bool TryGet(TKey key, out TValue value);
    void Set(TKey key, TValue value);

    // C# 8 기본 구현으로 추가된 멤버
    TValue GetOrDefault(TKey key, TValue defaultValue = default)
    {
        return TryGet(key, out var v) ? v : defaultValue;
    }
}

public class MemoryCache : ICache<string, int>
{
    private readonly Dictionary<string, int> _store = new();
    public bool TryGet(string key, out int value) => _store.TryGetValue(key, out value);
    public void Set(string key, int value) => _store[key] = value;
    // GetOrDefault 미구현: 기본 구현이 동작함
}

4. 인터페이스 내부 캡슐화(Private 멤버)

기본 구현에서만 쓰이는 보조 로직은 인터페이스의 private 멤버로 캡슐화할 수 있습니다.

using System;
using System.Linq;
using System.Text;

public interface IHasher
{
    byte[] Hash(string input)
    {
        var bytes = Encoding.UTF8.GetBytes(input);
        return HashCore(bytes);
    }

    // 기본 구현 전용 헬퍼
    private static byte[] HashCore(byte[] bytes)
        => bytes.Reverse().ToArray();
}

public class SimpleHasher : IHasher { /* 기본 구현 사용 */ }

5. 다중 인터페이스 충돌 해결

여러 인터페이스가 동일 시그니처의 기본 구현을 제공하면 구현 클래스는 충돌을 해결해야 합니다. 명시적 인터페이스 구현으로 분기할 수 있습니다.

using System;

public interface ILeft
{
    void M() { Console.WriteLine("Left"); }
}

public interface IRight
{
    void M() { Console.WriteLine("Right"); }
}

public class Both : ILeft, IRight
{
    // 충돌 해결: 명시적 구현
    void ILeft.M()  => Console.WriteLine("Left chosen");
    void IRight.M() => Console.WriteLine("Right chosen");

    public void CallLeft()  => ((ILeft)this).M();
    public void CallRight() => ((IRight)this).M();
}

참고: 클래스가 자체적으로 같은 시그니처의 public 메서드를 구현하면, 인터페이스를 통해 호출하더라도 해당 클래스 구현이 선택됩니다. 기본 구현은 클래스가 구현하지 않을 때만 사용됩니다.

6. 확장 메서드 vs 기본 구현

  • 확장 메서드: 기존 인터페이스 변경 없이 기능을 추가할 수 있지만, 인터페이스의 다른 멤버 상태나 캡슐화에 접근하지 못합니다. 오버라이드가 불가능합니다.
  • 기본 구현: 인터페이스 멤버로서 동작하며 다른 인터페이스 멤버와 협업하고, 구현체가 필요 시 재정의할 수 있습니다.

7. 주의사항과 모범 사례

  • 런타임 요구 사항: .NET Core 3.0+, .NET 5+에서 지원합니다. .NET Framework에서는 TypeLoadException 등 호환성 문제가 발생할 수 있습니다.
  • 상태 보관 금지: 인터페이스는 인스턴스 필드를 가질 수 없습니다. 기본 구현은 순수 동작 중심으로 설계합니다.
  • API 설계: 기본 구현은 편의 기능, 어댑터, 폴백 로직에 적합합니다. 복잡한 비즈니스 규칙은 구체 클래스/서비스로 이동을 고려합니다.
  • 이진 호환성: 기존 멤버의 시그니처 변경은 여전히 위험합니다. 새 기능은 새 멤버 + 기본 구현으로 추가합니다.

8. 성능 요약

기본 구현 호출은 인터페이스 호출과 유사한 비용이 듭니다. 핫패스에서는 간단한 로직과 인라이닝을 고려하고, 필요 시 구체 구현에서 직접 구현하여 분기/가상 호출 비용을 줄입니다.

9. 체크리스트

  • 프로젝트 언어 버전: C# 8 이상
  • 대상/실행 런타임: .NET Core 3.0+ 또는 .NET 5+
  • 기본 구현은 단순하고 안전하게 설계
  • 충돌 가능성 있는 시그니처는 명확히 구분

인터페이스의 기본 구현을 활용하면 API 진화를 매끄럽게 하고, 구현체의 부담을 줄이며, 공통 동작을 일관되게 제공합니다. 적절한 범위에서 전략적으로 사용해보시길 권합니다.