인터페이스의 기본 구현(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 진화를 매끄럽게 하고, 구현체의 부담을 줄이며, 공통 동작을 일관되게 제공합니다. 적절한 범위에서 전략적으로 사용해보시길 권합니다.
'C#' 카테고리의 다른 글
| C# 암시적/명시적 변환 연산자(implicit/explicit) 구현하기 (0) | 2026.05.25 |
|---|---|
| C# 구조적 비교와 EqualityComparer<T> 커스터마이징 (0) | 2026.05.25 |
| C# 디버깅 심화: Conditional Attribute와 DebuggerDisplay (0) | 2026.05.22 |
| C# nullable 값 형식(Nullable<T>) 고급 사용법 (0) | 2026.05.22 |
| C# Enumerator 커스터마이징과 상태 유지 (0) | 2026.05.21 |