public interface I
{
public void Test()
{
Console.WriteLine("A");
}
}
그래서 머지..? 싶다가 일단 넘어 갔는데
테스트를 위해 다음과 같이 코드를 작성했습니다.
public class Program
{
public static void Main()
{
A a = new();
var i = a as I;
i.Test();
a.Test();
}
}
public class A : B, I
{
}
public class B
{
public void Test()
{
Console.WriteLine("B");
}
}
public interface I
{
public void Test()
{
Console.WriteLine("I");
}
}
결과는 B 가 두줄 나왔습니다.
Test를 virtual로 바꿔도 결과는 같습니다.
위와 같은 중복 상속의 충돌을 방지 하기 위해
C#에서는 class 중복 상속은 안되고, interface 중복 상속만 허용 하는 것으로 알고 있는데
이렇게 interface안에 메소드 구현이 된다면 위 법칙이 깨지게 됩니다.
interface에서는 field 선언이 안되듯, 메소드 구현도 안되야 하는게 맞는거 같은데
@Vincent 님께서 말씀해주신 것과 같이 .NET Core로 넘어오고나서 본격적으로 .NET Framework와 궤를 달리하기 시작하면서 추가된 부분이 바로 이 부분입니다.
(1) 단순히 인터페이스에 대한 공통 로직을 확장하기 위함이라면 익스텐션 메서드를 쓰는게 opt-in/opt-out 관리가 쉽고 바람직한 것이지만, (2) 인터페이스 간에 상속이 있고, 그 관계안에서 “전문화”, "오버라이딩"이 발생하는 구조면 그 때 쓰는게 맞다고 생각합니다.
(1)의 예를 들어보면,
public interface ILogger
{
void Log(string message);
}
// 계약에 포함되지 않는 편의 기능 (선택적으로 사용)
public static class LoggerExtensions
{
public static void Info(this ILogger logger, string message)
=> logger.Log($"[INFO] {message}");
public static void Error(this ILogger logger, Exception ex)
=> logger.Log($"[ERROR] {ex.Message}");
}
위와 같이 Log 메서드를 좀 더 쓰기 좋게 래핑하는 것이 전부라면 위와 같이 확장 메서드로 정의하는게 관리가 쉽고 직관적입니다. 대개는 여기에 많이 걸치고요,
(2)의 경우는
public interface IRepository<T>
{
IQueryable<T> GetAll();
// 모든 구현체가 공통적으로 사용할 기본 구현 제공
T? GetById(int id)
=> GetAll().FirstOrDefault(e => e.Id == id);
}
// 캐시 적용 저장소: 상위 인터페이스의 기본 구현을 재정의
public interface ICachedRepository<T> : IRepository<T>
{
T? IRepository<T>.GetById(int id)
{
// 캐시 조회
var entity = Cache.Get<T>(id);
if (entity != null) return entity;
// 캐시에 없으면 기본 구현(for DB) 호출
return ((IRepository<T>)this).GetById(id);
}
}
이 경우에는 IRepository가 가진 기본적인 특성은 그대로 유지하면서도, “캐싱이 되는 리포지터리”라는 성격을 GetById 메서드의 원래 규약은 지키되, ICachedRepository만의 구현으로 전문화하고 있습니다.
같은 패턴을 추상 클래스로도 구현할 수 있지만, 인터페이스에 기본 구현을 두면 구현 타입이 다른 베이스 클래스를 상속받을 여지를 남겨 두면서도, 인터페이스 계층 안에서 기본 동작과 재정의를 동시에 표현할 수 있다는 것이 장점입니다. 추상 클래스로 같은 일을 하게 되면, 다른 클래스를 상속받을 여지가 사라지고 이는 @BigSquare 님께서 말씀하신 대로입니다.
그래서 실제로는 (1)의 방식만으로도 대부분의 문제를 충분히 해결할 수 있기 때문에, 해당 기능이 새로 추가되었다고 해서 굳이 의식적으로 익히고 적극적으로 사용하려고 노력할 필요까지는 없다고 생각합니다.