interface default 구현

해당 게시글을 보며, 갑자기 궁금한게 생겨서 주니어 개발자인 저에게는 추후 스펙을 잘못이해할 것 같아서 이것저것 실험해보고 공유드리려합니다.

제가 처음 해당 기능을 처음 접하고 든 생각은 Diamond Problem을 막기위해서 class 다중 상속을 막은게 아니었나?

Interface 를 다중 상속 받고 default 구현이 같다면? 무엇을 호출하지? 라는 생각이들어서
아래와 같은 코드를 작성해보았습니다.

public interface IFooA
{
    void Print()
    {
        Console.WriteLine($"{nameof(IFooA)} 기본 구현");
    }
}

public interface IFooB
{
    void Print()
    {
        Console.WriteLine($"{nameof(IFooB)} 기본 구현");
    }
}

public class FooA : IFooA
{
    
}

public class Foo : IFooA, IFooB
{
}

var fooA = new FooA();
fooA.Print(); // 컴파일 에러

var a = fooA as IFooA;
a?.Print(); // 출력: IFooA 기본 구현

var foo = new Foo();
foo.Print(); // 컴파일 에러

Safely update interfaces using default interface methods - C# | Microsoft Learn

However, the SampleCustomer class doesn’t inherit members from its interfaces.

위 링크처럼 interface에서 멤버를 상속하지 않는 다는 기본 규칙은 유지한다고 합니다.

위의 정보를 찾다보니 Reddit에서 못볼걸 또 발견하고 말았습니다.

In java, scala, and kotlin, they behave as expected, as the implementing class inherit the default methods, but in C#, “A class does not inherit members from its interfaces” (from the doc itself).

public interface IFooA {
    default void print() {
        System.out.println("FooA");
    }
}

public class Foo implements IFooA {
}

public static void main(String[] args) {
    var foo = new Foo();
    foo.print(); // 출력: "FooA";
}

(위의 코드는 JAVA 입니다)
엇! 그러면 IFooB를 추가하면 어떻게될까요?

그래도 메서드를 구현하라고 컴파일 에러를 발생시키고 있네요.

https://www.reddit.com/r/csharp/comments/120kr4p/default_interface_methods_and_inheritance/

reddit에 해당 답변을 자세히 달아주신분이 있어서 대체합니다!

혹시 읽기 싫으신분은 밑에 요약본을 봐주시면 될 것 같습니다!
Reddit에 달린 글 요약본을 번역해서 달아 놓겠습니다.

인터페이스는 본질적으로 구현을 가지지 않는 구조입니다.
인터페이스를 한 번 공개한 이후에 변경하는 것은 호환성을 깨뜨리는 변화 입니다.
이런 호환성 깨짐은, 아무리 고통을 줄이려 해도 항상 고통스럽습니다.

일부 사람들은 인터페이스에 메서드를 추가하는 것이 공개 이후에는 호환성을 깨뜨리는 변화라는 점을 좋아하지 않았습니다.
C# 팀은 이를 최대한 안전하게 만들기 위해 노력했습니다.
그럼에도 불구하고 호환성을 깨뜨리는 변화는 여전히 고통스럽습니다.

C#은 일관되게, 클래스 기반의 virtual과 abstract 메서드만 다형성을 가지며, 그 외의 기법들은 그렇지 않습니다.

6개의 좋아요

비슷한 이유로 저도 인터페이스 보다는 대리자를 많이 사용합니다.

대리자는 근본적으로 하나의 행위만 정의할 수 있다는 특징 때문에, 새로운 행위가 필요한 시점에 새로운 대리자를 정의하면 되는데, 새로운 대리자가 과거 코드에 미치는 영향이 없어, 고통을 겪을 확률도 제로입니다. 이에 반해, 이미 공개되어 구현 클래스가 수십개인 상황에서 인터페이스에 메서드 하나 추가하면, 고통이 시작되는 것이죠. ^^

(추상 클래스의 virtual 멤버와 인터페이스의 기본 구현은 이런 고통의 완화수단이 될 수 있지만, 해결책은 아닙니다.)

대리자는 다른 이점도 많은데, 우선 코드량이 적습니다.

public interface IFooA
{
   void Print();
}
class Foo : IFooA
{
   public void Print() // ...
}
class Bar : IFooA
{
   public void Print() // ...
}
public delegate void Print();

public static class Prints 
{
   public Print Foo() => () => { // ... }
   public Print Bar() => () => { // ... }
}

사실, 예제를 바탕으로 비교하느라 위와 같은 무쓸모 코드를 적었지만, 실무에서 사용되는 복잡한 도메인 프로세스를 나타낼 때는 한 층 더 강력합니다.

public delegate void Print(string message);
public static class Prints 
{
   public Print Stream(Stream stream) => 
      (m) => { // ... };

   public Print Post(HttpClient proxy, string path) =>
      (m) => { // ... };

   public static Print Then(this Print a, Print b) => 
      (m) => { a(m); b(m); };

   public static Print StreamAndPost(Stream stream, HttpClient proxy, string path) =>
      Stream(stream).Then(Post(http, path));
}

이런 특징으로 요즘 유행하는 CQRS 니 CQS 니 하는 것들과도 아주 잘 어울리고, DI 도 문제가 없습니다.(그러나 DI는 추천하지는 않습니다.)

사소하게는 집합을 다루기도 편합니다.

List<IFooA> _fooAs = [];

public void Remove(IFooA fooA)
{
   if (_fooAs.FirstOrDefault(x => x.Equals(fooA)) is IFooA exist)
   {
      _fooAs.Remove(exist);
   }
}
Print _prints;
public void Remove(Print print) => _prints -= print; // 없어도 에러 없음.
3개의 좋아요