C#/C# 인터페이스

C# 인터페이스 vs 추상 클래스

로파이 2022. 4. 9. 14:21

인터페이스 interface

OOP에서 객체들의 공통적인 행동에 대한 명세를 규정하고 이를 상속(구현)하는 클래스는 인터페이스를 구현하도록 한다.

public interface INetwork
{
    public void Send();
}

INetwork라는 인터페이스는 Send()라는 함수적 기능만 있을 뿐이다. INetwork를 구현하는 구현체는 이제 Send() 함수 기능을 가지는 클래스라고 할 수 있다.

 

public partial class IPv4Network : INetwork
{
    public void Send()
    {
        Console.WriteLine($"{nameof(IPv4Network)}");
    }
}

public partial class IPv6Network : INetwork
{
    public void Send()
    {
        Console.WriteLine($"{nameof(IPv6Network)}");
    }
}

이를 구현한 다양한 구현체 클래스를 작성할 수 있다.

 

var ipv4 = new IPv4Network();
var ipv6 = new IPv6Network();

ipv4.Send(); // "IPv4Network"
ipv6.Send(); // "IPv6Network"

구현체의 함수를 호출하면 var가 실제로 해석된 클래스의 Send() 메서드를 찾아서 호출하게 된다. 이 과정은 정적으로 이루어지며 이미 해당 클래스가 INetwork를 직접 구현한 경우 컴파일 에러 없이 수행된다.

 

public partial class IPv4PrivateNetwork : IPv4Network
{
    public new void Send()
    {
        Console.WriteLine($"{nameof(IPv4PrivateNetwork)}");
    }
}

만약 구현체를 상속하여 인터페이스로 정의된 함수를 새로 정의하고 싶다면 인터페이스의 메서드는 가상 함수가 아니므로 new 키워드로 상위 클래스의 메서드를 숨김으로써 재정의해야한다.

 

var ipv4p = new IPv4PrivateNetwork();
ipv4p.Send(); // "Ipv4PrivateNetwork"

var ipv4p_c = ipv4p as IPv4Network;
ipv4p_c.Send(); // "Ipv4Network"

var ivp4p_i = ipv4p as INetwork;
ivp4p_i.Send(); // "Ipv4Network"

인터페이스가 추상 클래스와 다른 점은 여기서 볼 수 있는데, IPv4PrivateNetwork 타입의  ipv4p 인스턴스정적으로 해석되어 항상 캐스팅된 타입의 메서드를 호출한다. 이는 인터페이스를 구현한 구현체의 Send() 그리고 new 타입으로 새로 정의된 Send() 모두 정적으로 각각 IPv4Network와 IPv4PrivateNetwork 클래스 타입에 정의되어 있기 때문이다. 

추상 클래스 Abstract

추상 클래스 뿐만 아니라 하나의 가상 함수를 포함하는 모든 클래스를 가리킨다. 가상 함수를 가지는 객체 타입은 C++의 가상 함수 테이블 처럼 동적으로 객체 타입을 확인하여 override된 메서드를 호출한다.

 

public abstract class EndPoint
{
    public abstract void Configure();
}

public partial class IPv4Network : EndPoint
{
    public override void Configure()
    {
        Console.WriteLine("Configuration Ipv4 ...");
    }
}

public partial class IPv4PrivateNetwork : IPv4Network
{
    public override void Configure()
    {
        Console.WriteLine("Configuration Ipv4 as Private ...");
    }
}

C# partial 기능을 이용하여 EndPoint 라는 추상 클래스를 상속하는 것으로 명세를 추가해본다.

각각 IPv4Network와 IPv4PrivateNetwork는 Configure()라는 메서드를 자신의 기반 클래스의 Configure() 메서드를 override 한다.

 

var ipv4p = new IPv4PrivateNetwork();
ipv4p.Send(); // "Ipv4PrivateNetwork"

var ipv4p_c = ipv4p as IPv4Network;
ipv4p_c.Send(); // "Ipv4Network"

var ivp4p_i = ipv4p as INetwork;
ivp4p_i.Send(); // "Ipv4Network"

ipv4p.Configure(); // "Configuration Ipv4 as Private ..."
ipv4p_c.Configure(); // "Configuration Ipv4 as Private ..."
//ivp4p_i.Configure(); // "Configuration Ipv4 as Private ..."

이제 Ipv4p 인스턴스는 IPv4PrivateNetwork의 인스턴스로써 Configure()를 호출하면 동적으로 객체 타입을 확인하여 override된 메서드를 호출하게 된다. 

 

마지막 메서드는 iv4p_i가 같은 인스턴스를 지칭하지만 INetwork 인터페이스로 해석되기 때문에 해당 메서드 정의가 없으므로 컴파일 에러가 발생한다.

 

public interface INetwork
{
    public void Send();

    public abstract void Configure();
}

이렇게 사용하는 경우는 거의 없지만 C#은 고급 언어으로써 모든 메서드, 속성 마다 접근자와 virtual/abstract와 같은 상속 특성을 추가 할 수 있다. 

 

ipv4p.Configure(); // "Configuration Ipv4 as Private ..."
ipv4p_c.Configure(); // "Configuration Ipv4 as Private ..."
ivp4p_i.Configure(); // "Configuration Ipv4 as Private ..."

마지막 문장은 이제 오류없이 컴파일된다.

결론

인터페이스의 메서드는 정적으로 해석된 타입의 메서드를 호출하게 되며 보통 인터페이스를 직접 구현한 메서드를 가리킨다. 인터페이스의 메서드를 상속된 클래스에서 재정의하려면 new를 사용해야한다. new를 사용했더하더라도 상속된 클래스의 인스턴스가 인터페이스로 캐스팅되어 메서드를 호출하면 정적으로 확인된 기반 클래스가 직접 구현한 인터페이스 메서드를 호출하게 된다.

 

추상/가상 메서드는 항상 동적으로 타입을 확인하여 실제 타입에서 override된 메서드를 호출한다. 인스턴스는 C# 가상 함수 테이블을 자신의 메모리에 가지고 있다.


용법

- 인터페이스

인터페이스의 경우 상속보다는 여러 객체의 공통적인 기능을 묶는 기능을 정의한다. 인터페이스 구현체가 구현하는 경우외에 구현체를 상속하는 클래스에서 인터페이스 메서드를 재정의하는 경우는 인터페이스에 추상 함수로 정의하거나 new로 숨겨야하는 지 혹은 인터페이스가 아니라 추상 클래스로 정의해야 할 지 고려해야한다.

 

- 추상/가상 함수

여러 계층의 클래스에 대한 정의를 해야하고 파생 클래스에서 override한 경우 기반 클래스의 기존 기능이 필요하여 base.Call()를 사용해야하거나 혹은 반드시 동적으로 타입에 따라 결정되어야하는 메서드인 경우 추상/가상 함수로 정의하도록 한다.