명시적 인터페이스를 꽤 최근에서야 알게 되었는데 (ㅎㅎ)

처음 봤을 때 되게 독특한 녀석이라고 느꼈었다.

누가 봐도 평소 내가 하던 C# 코딩과는 좀 이질적으로 느껴졌었기 때문이다.

 

명시적 인터페이스 구현에 대하여

일반적으로 명시적 인터페이스 구현은 한 클래스가 여러 인터페이스를 구현할 때,

이름이 겹쳐서 생기는 문제를 방지하기 위해 사용되는 기능이다.

 

다음과 같은 코드가 예이다.

public interface IMachine
{
    string Id { get; set; }
    
    void Run();
}

public interface IRunnable
{
    int Id { get; set; }

    void Run();
}

public class Machine_A : IMachine
{
    string IRunnable.Id { get; set; }

    int IMachine.Id { get; set; }

    void IRunnable.Run()
    {
        Console.WriteLine("Run!");
    }
        
    void IMachine.Run()
    {
    	Console.WriteLine("Machine starts operation.");
    }
}

IMachine과 IRunnable의 Id라는 속성과 Run이라는 메소드가 겹친다.

일반적으로 이런 상황이 벌어지면 한쪽 이름을 적절하게 변경하면 되겠지만.

만약, IMachine이 A사의 라이브러리에서 정의한 인터페이스고 IRunnable이 B사의 라이브러리에서 정의한 인터페이스라,

내 코드가 아니라 수정할 수가 없다면?

(함수라면 하나의 메소드로 작동해서 IRunnable이건 IMachine이건 같은 동작을 하겠지만, 상식적으로 서로 다른 라이브러리 인터페이스를 사용하는데 같은 동작을 한다면 나중에 좋은 꼴을 보지 못할 것이란 걸 짐작할 수 있다.)

 

이 문제를 해결할 때 적절한 게 바로 명시적 인터페이스 구현이다.

 

근데 이 녀석 참 특이한 게 인터페이스명을 함수명 앞에 붙이고 접근 한정자도 붙이지 않는다.

참 어색하다.

 

더 특이한 건 이 명시적 인터페이스 구현의 특징인데,

이렇게 구현된 인터페이스 요소(속성, 메소드, 이벤트)들은 절대로 해당 클래스의 참조값으로는 접근할 수 없고,

반드시 인터페이스로 형변환을 해줘야 호출할 수 있다.

class MainClass
{
    public static void Main(string[] args)
    {
        Machine_A machine_A = new Machine_A();
        IMachine machine = machine_A as IMachine;

        // 인터페이스로 형변환을 해준뒤 인터페이스 참조값으로만 접근할 수 있다.
        machine.Run();
    }
}

 

심지어 구현된 같은 클래스 내부에서도 함부로 호출할 수가 없다. ㄷㄷ

반드시 같은 클래스라도 형변환을 해야 호출할 수 있다.

public class Machine_A : IRunnable, IMachine
{
    string IRunnable.Id { get; set; }

    int IMachine.Id { get; set; }

    void IRunnable.Run()
    {
        IRunnable thisRunnable = this as IRunnable;
        Console.WriteLine("Machine_A runs! " + thisRunnable.Id);
    }

    void IMachine.Run()
    {
        Console.WriteLine("Machine_A operates!");
    }

    public void WriteLog()
    {
        IRunnable thisRunnable = this as IRunnable;
        thisRunnable.Run();
    }
}

 

명시적 인터페이스 구현의 접근 한정자는 뭘까?

자, 그럼 이렇게 구현된 인터페이스 요소들은 한정자를 지정해주지 않는다.

그럼 제일 궁금한 점이 바로, 이 녀석의 접근 한정자가 뭘까이다.

 

일반적으로 그들은 클래스의 참조로 접근할 수 없는데, 인터페이스를 캐스팅한 참조 변수로는 참조가 가능하다.

길게 가지 않고, 결과만 얘기하면,

 

그들은 private이면서 public이다.

 

이상하게 들리지만 그렇다...

 

일단 왜 private이냐면, 그들이 컴파일로 나왔을 때, 컴파일에 의해 지정된 접근 한정자가 private이기 때문이다.

위의 Machine 클래스 코드를 컴파일하고 어셈블리를 뜯어보면 Machine.IRunnable.Run은 이렇게 되어 있다.

솔직히 필자는 IL 코드를 잘 모르기 때문에 이게 무슨 의미인지는 어렴풋이 만 추측할 뿐이지만

눈에 띄는 부분이 바로 함수명 쪽이다.

private final hidebysig newslot virtual instance void TestLibrary.IRunnable.Run() cil managed

 

함수가 private으로 되어있다.

그래서 명시적 인터페이스로 구현된 요소는 private이다.

 

하지만, 실제로는 조금 다르다.

interface의 기본적인 특징으로 모든 요소는 public이기 때문에, 다른 클래스에서도 인터페이스로 캐스팅만 하면 접근이 가능하다.

당연하지만, 다른 어셈블리에서도 호출이 가능하다.

(필자는 테스트를 해보았지만, 생략하겠다. ㅎㅎ)

 

private인데, public처럼 캐스팅을 하면 호출이 가능하기에 이 녀석은 private이면서 public인 것이다. 

 

이유?

솔직히 위에서 글을 마무리하려고 했는데, 몇 가지 il코드를 둘러보다가

뭔가 의심되는 부분을 발견했다.

이것은 IRunnable 인터페이스의 IL 코드이다.

그런데, 코드를 살펴보면 꽤 신기하게 생겼다.

.class interface public auto ansi abstract TestLibrary.IRunnable
{
    ....
    .method public hidebysig newslot abstract virtual instance void Run () cil managed 
    {
    }
}

인터페이스 코드가 마치 추상 클래스처럼 되어 있고 메소드는 마치 추상 메소드처럼 구현되어 있다!

 

헉, 너무 신기하다.

즉, IL 코드로 추측할 수 있는 의미는 인터페이스는 결국 내부적으론 추상 클래스처럼 다뤄지고 있다는 것이다.

헉 사실 내가 클래스를 사용하고 있었다니.

똑같이 class이면서 굳이 인터페이스라고 따로 둔 이유는, 아마, 인터페이스 같은 완전 추상 클래스의 이점은 가져가면서 다중 클래스 상속의 문제를 회피하기 위해서가 아닐까 추측해본다.

 

그렇다면, 명시적 인터페이스로 구현된 private 요소를 접근할 수 있는 이유에 대해서도 납득할 수 있는 추측을 할 수 있다.

기본적으로 클래스에 정의 된 요소는 private이기 때문에 접근할 수 없지만, 인터페이스에서는 public으로 선언되었기 때문에,

명시적으로 구현된 인터페이스 요소들은 해당 인터페이스로 형변환한 후에만 접근 가능한 것이다.

그리고 만약 해당 인터페이스 요소를 접근하게 되면, 이는 abstract로 선언된 것들이기 때문에,

결과적으로 오버라이드에 의해서 클래스 내에 구현된 요소들이 호출된다!

 

실제로 위 Machine_A.Run의 IL코드를 다시 보면,

private final hidebysig newslot virtual
    instance void TestLibrary.IRunnable.Run() cil managed
{
    .override method instance void TestLibrary.IRunnable::Run()
    
    ...
}

이렇게 되어 있는데, 이 코드를 아까 서술한 이유를 떠올리면서 읽어보면 이렇게 볼 수 있다.

'void IRunnable.Run 메소드는 가상 메소드이며, TestLibrary.IRunnable::Run()을 재정의 한다.'

 

즉, Run 함수는 인터페이스의 추상 메소드를 구현하는 것이기 때문에 virtual 속성이 붙고,

명시적 인터페이스 구현에 의해, 컴파일일 할 때, .override method instance void TestLibrary.IRunnable::Run() 코드가 추가되어

이 함수는 IRunnable.Run()을 오버라이드 한 것임을 명시적으로 정의하고 있는 것이다.

 

결론?

결론은 아까 내린 것과 변함이 없다.

명시적으로 구현된 인터페이스 요소들은 private이지만, 인터페이스에선 public으로 선언됐기 때문에 public이다.

하지만, 이유를 알게 되었기 때문에 좀 더 정확하게 이것의 접근자는 이렇다!라고 얘기할 수 있게 되었다.

 

[참고]

알다시피, 인터페이스는 public 외에 internal로도 지정할 수 있다.

해당 어셈블리 내에서만 사용되길 원한다면, internal로 지정하면 된다.

 

p.s.

원래는 이유를 빼고 끝내려고 했던 건데, 찾다 보니 처음 글 쓰던 의도와 다르게 꽤 재미난 사실을 알게 되어서 좋은 시간이었다.

 

 

+ Recent posts