자습서: 기본 인터페이스 메서드를 사용하는 인터페이스를 통해 클래스를 만드는 경우의 기능 혼합

인터페이스 멤버 선언 시 구현을 정의할 수 있습니다. 이 기능은 인터페이스에 선언된 기능에 대한 기본 구현을 정의할 수 있는 새로운 기능을 제공합니다. 클래스는 기능을 재정의할 시기, 기본 기능을 사용할 시기 및 불연속 기능에 대한 지원을 선언하지 않을 시기를 선택할 수 있습니다.

이 자습서에서 학습할 방법은 다음과 같습니다.

  • 불연속 기능을 설명하는 구현을 사용하여 인터페이스를 만듭니다.
  • 기본 구현을 사용하는 클래스를 만듭니다.
  • 기본 구현의 일부 또는 전체를 재정의하는 클래스를 만듭니다.

필수 조건

C# 컴파일러를 포함하여 .NET을 실행하도록 컴퓨터를 설정해야 합니다. C# 컴파일러는 Visual Studio 2022 또는 .NET SDK에서 사용할 수 있습니다.

확장 메서드의 제한 사항

인터페이스의 일부로 나타나는 동작을 구현할 수 있는 한 가지 방법은 기본 동작을 제공하는 확장 메서드를 정의하는 것입니다. 인터페이스는 해당 멤버를 구현하는 클래스에 더 큰 노출 영역을 제공하는 동시에 최소 멤버 집합을 선언합니다. 예를 들어 Enumerable의 확장 메서드는 모든 시퀀스가 LINQ 쿼리의 원본이 되도록 구현합니다.

확장 메서드는 선언된 변수 형식을 사용하여 컴파일 시간에 확인됩니다. 인터페이스를 구현하는 클래스는 모든 확장 메서드에 대해 더 나은 구현을 제공할 수 있습니다. 컴파일러가 해당 구현을 선택할 수 있도록 변수 선언이 구현 형식과 일치해야 합니다. 컴파일 시간 형식이 인터페이스와 일치하는 경우 메서드 호출은 확장 메서드를 확인합니다. 확장 메서드의 또 다른 문제는 확장 메서드를 포함하는 클래스에 액세스할 수 있는 모든 위치에서 메서드에 액세스할 수 있다는 것입니다. 클래스는 확장 메서드에 선언된 기능을 제공해야 하는지 제공하지 않아야 하는지 여부에 관계없이 선언할 수 없습니다.

기본 구현을 인터페이스 메서드로 선언할 수 있습니다. 그러면 모든 클래스에서 자동으로 기본 구현을 사용합니다. 더 나은 구현을 제공할 수 있는 모든 클래스는 더 나은 알고리즘으로 인터페이스 메서드를 재정의할 수 있습니다. 어떤 의미에서 이 기술은 확장 메서드를 사용하는 방법과 비슷합니다.

이 문서에서는 기본 인터페이스 구현에서 새 시나리오를 사용하도록 설정하는 방법을 알아봅니다.

애플리케이션 설계

홈 자동화 애플리케이션을 고려합니다. 집 전체에서 사용할 수 있는 다양한 광원 및 표시등이 있을 수 있습니다. 모든 광원은 켜고 끄고 현재 상태를 보고할 수 있도록 API가 지원되어야 합니다. 일부 광원 및 표시등은 다음과 같은 다른 기능을 지원할 수 있습니다.

  • 광원을 켜고 타이머에 맞춰 끕니다.
  • 일정 시간 동안 광원이 깜박입니다.

이러한 확장 기능 중 일부는 최소 설정을 지원하는 디바이스에서 에뮬레이트될 수 있습니다. 이는 기본 구현을 제공한다는 의미입니다. 더 많은 기능이 내장된 디바이스의 경우 디바이스 소프트웨어는 기본 기능을 사용합니다. 다른 광원의 경우 인터페이스를 구현하고 기본 구현을 사용하도록 선택할 수 있습니다.

기본 인터페이스 멤버는 이 시나리오에서 확장 메서드보다 더 나은 솔루션입니다. 클래스 작성자는 구현할 인터페이스를 제어할 수 있습니다. 선택한 인터페이스는 메서드로 사용할 수 있습니다. 또한 기본 인터페이스 메서드는 기본적으로 가상이기 때문에 메서드 디스패치는 항상 클래스에서 구현을 선택합니다.

이러한 차이점을 보여주는 코드를 만들어 보겠습니다.

인터페이스 만들기

모든 광원의 동작을 정의하는 인터페이스를 만드는 것부터 시작합니다.

public interface ILight
{
    void SwitchOn();
    void SwitchOff();
    bool IsOn();
}

기본 오버헤드 광원 픽스쳐는 다음 코드와 같이 이 인터페이스를 구현할 수 있습니다.

public class OverheadLight : ILight
{
    private bool isOn;
    public bool IsOn() => isOn;
    public void SwitchOff() => isOn = false;
    public void SwitchOn() => isOn = true;

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

이 자습서에서 코드는 IoT 디바이스를 구동하지 않지만 콘솔에 메시지를 작성하여 해당 활동을 에뮬레이트합니다. 집을 자동화하지 않고 코드를 탐색할 수 있습니다.

다음으로 시간 제한 후 자동으로 꺼질 수 있는 광원의 인터페이스를 정의해 보겠습니다.

public interface ITimerLight : ILight
{
    Task TurnOnFor(int duration);
}

기본 구현을 오버헤드 광원에 추가할 수 있지만, 이 인터페이스 정의를 수정하여 virtual 기본 구현을 제공하는 것이 더 좋습니다.

public interface ITimerLight : ILight
{
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Using the default interface method for the ITimerLight.TurnOnFor.");
        SwitchOn();
        await Task.Delay(duration);
        SwitchOff();
        Console.WriteLine("Completed ITimerLight.TurnOnFor sequence.");
    }
}

OverheadLight 클래스는 인터페이스에 대한 지원을 선언하여 타이머 함수를 구현할 수 있습니다.

public class OverheadLight : ITimerLight { }

다른 광원 형식은 보다 정교한 프로토콜을 지원할 수 있습니다. 다음 코드와 같이 TurnOnFor에 대한 자체 구현을 제공할 수 있습니다.

public class HalogenLight : ITimerLight
{
    private enum HalogenLightState
    {
        Off,
        On,
        TimerModeOn
    }

    private HalogenLightState state;
    public void SwitchOn() => state = HalogenLightState.On;
    public void SwitchOff() => state = HalogenLightState.Off;
    public bool IsOn() => state != HalogenLightState.Off;
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Halogen light starting timer function.");
        state = HalogenLightState.TimerModeOn;
        await Task.Delay(duration);
        state = HalogenLightState.Off;
        Console.WriteLine("Halogen light finished custom timer function");
    }

    public override string ToString() => $"The light is {state}";
}

가상 클래스 메서드를 재정의하는 것과 달리 HalogenLight 클래스의 TurnOnFor 선언에는 override 키워드가 사용되지 않습니다.

조합 및 일치 기능

고급 기능을 도입할수록 기본 인터페이스 방법의 장점이 더 명확해집니다. 인터페이스를 사용하면 기능을 조합하고 일치시킬 수 있습니다. 또한 각 클래스 작성자가 기본 구현과 사용자 지정 구현 중에서 선택할 수 있습니다. 깜박이는 광원에 대한 기본 구현으로 인터페이스를 추가해 보겠습니다.

public interface IBlinkingLight : ILight
{
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("Using the default interface method for IBlinkingLight.Blink.");
        for (int count = 0; count < repeatCount; count++)
        {
            SwitchOn();
            await Task.Delay(duration);
            SwitchOff();
            await Task.Delay(duration);
        }
        Console.WriteLine("Done with the default interface method for IBlinkingLight.Blink.");
    }
}

기본 구현을 사용하면 모든 광원이 깜박일 수 있습니다. 오버헤드 광원은 기본 구현을 사용하여 타이머 및 깜박임 기능을 모두 추가할 수 있습니다.

public class OverheadLight : ILight, ITimerLight, IBlinkingLight
{
    private bool isOn;
    public bool IsOn() => isOn;
    public void SwitchOff() => isOn = false;
    public void SwitchOn() => isOn = true;

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

새로운 광원 형식인 LEDLight는 timer 함수와 blink 함수를 직접 지원합니다. 이 광원 스타일은 ITimerLightIBlinkingLight 인터페이스를 모두 구현하고 Blink 메서드를 재정의합니다.

public class LEDLight : IBlinkingLight, ITimerLight, ILight
{
    private bool isOn;
    public void SwitchOn() => isOn = true;
    public void SwitchOff() => isOn = false;
    public bool IsOn() => isOn;
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("LED Light starting the Blink function.");
        await Task.Delay(duration * repeatCount);
        Console.WriteLine("LED Light has finished the Blink function.");
    }

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

ExtraFancyLight는 blink 및 timer 함수를 직접 지원할 수 있습니다.

public class ExtraFancyLight : IBlinkingLight, ITimerLight, ILight
{
    private bool isOn;
    public void SwitchOn() => isOn = true;
    public void SwitchOff() => isOn = false;
    public bool IsOn() => isOn;
    public async Task Blink(int duration, int repeatCount)
    {
        Console.WriteLine("Extra Fancy Light starting the Blink function.");
        await Task.Delay(duration * repeatCount);
        Console.WriteLine("Extra Fancy Light has finished the Blink function.");
    }
    public async Task TurnOnFor(int duration)
    {
        Console.WriteLine("Extra Fancy light starting timer function.");
        await Task.Delay(duration);
        Console.WriteLine("Extra Fancy light finished custom timer function");
    }

    public override string ToString() => $"The light is {(isOn ? "on" : "off")}";
}

이전에 만든 HalogenLight는 깜박임을 지원하지 않습니다. 따라서 지원되는 인터페이스 목록에 IBlinkingLight를 추가하지 마세요.

패턴 일치를 사용하여 광원 형식 검색

다음으로 몇 가지 테스트 코드를 작성해 보겠습니다. C#의 패턴 일치 기능을 사용하여 지원되는 인터페이스를 검사하고 광원의 기능을 결정할 수 있습니다. 다음 메서드는 각 광원의 지원되는 기능을 연습합니다.

private static async Task TestLightCapabilities(ILight light)
{
    // Perform basic tests:
    light.SwitchOn();
    Console.WriteLine($"\tAfter switching on, the light is {(light.IsOn() ? "on" : "off")}");
    light.SwitchOff();
    Console.WriteLine($"\tAfter switching off, the light is {(light.IsOn() ? "on" : "off")}");

    if (light is ITimerLight timer)
    {
        Console.WriteLine("\tTesting timer function");
        await timer.TurnOnFor(1000);
        Console.WriteLine("\tTimer function completed");
    }
    else
    {
        Console.WriteLine("\tTimer function not supported.");
    }

    if (light is IBlinkingLight blinker)
    {
        Console.WriteLine("\tTesting blinking function");
        await blinker.Blink(500, 5);
        Console.WriteLine("\tBlink function completed");
    }
    else
    {
        Console.WriteLine("\tBlink function not supported.");
    }
}

Main 메서드의 다음 코드는 각 광원 형식을 차례로 만들고 해당 광원을 테스트합니다.

static async Task Main(string[] args)
{
    Console.WriteLine("Testing the overhead light");
    var overhead = new OverheadLight();
    await TestLightCapabilities(overhead);
    Console.WriteLine();

    Console.WriteLine("Testing the halogen light");
    var halogen = new HalogenLight();
    await TestLightCapabilities(halogen);
    Console.WriteLine();

    Console.WriteLine("Testing the LED light");
    var led = new LEDLight();
    await TestLightCapabilities(led);
    Console.WriteLine();

    Console.WriteLine("Testing the fancy light");
    var fancy = new ExtraFancyLight();
    await TestLightCapabilities(fancy);
    Console.WriteLine();
}

컴파일러가 최선의 구현을 결정하는 방법

이 시나리오에서는 구현이 없는 기본 인터페이스를 보여 줍니다. ILight 인터페이스에 메서드를 추가하면 새로운 복잡성이 발생합니다. 기본 인터페이스 메서드를 제어하는 언어 규칙은 여러 파생 인터페이스를 구현하는 구체적인 클래스에 대한 영향을 최소화합니다. 새 메서드로 원래 인터페이스를 개선하여 사용 방법을 변경할 수 있는 방법을 확인해 보겠습니다. 모든 표시등 광원은 해당 전원 상태를 열거형 값으로 보고할 수 있습니다.

public enum PowerStatus
{
    NoPower,
    ACPower,
    FullBattery,
    MidBattery,
    LowBattery
}

기본 구현에서는 전원이 없는 것으로 가정합니다.

public interface ILight
{
    void SwitchOn();
    void SwitchOff();
    bool IsOn();
    public PowerStatus Power() => PowerStatus.NoPower;
}

ExtraFancyLight에서 ILight 인터페이스 및 파생된 인터페이스, ITimerLightIBlinkingLight에 대한 지원을 선언하더라도 이러한 변경 내용은 완전히 컴파일됩니다. ILight 인터페이스에 선언된 "가장 가까운" 구현은 하나뿐입니다. 재정의를 선언한 모든 클래스는 하나의 "가장 가까운" 구현이 됩니다. 이전 클래스에서 다른 파생 인터페이스의 멤버를 재정의하는 예를 살펴보았습니다.

여러 파생 인터페이스에서 동일한 메서드를 재정의하지 마세요. 이렇게 하면 클래스가 파생된 두 인터페이스를 모두 구현할 때마다 모호한 메서드 호출을 만듭니다. 컴파일러는 더 나은 단일 메서드를 선택할 수 없으므로 오류가 발생합니다. 예를 들어 IBlinkingLightITimerLight 모두 PowerStatus 재정의를 구현하는 경우 OverheadLight는 보다 구체적인 재정의를 제공해야 합니다. 그렇지 않으면 컴파일러는 두 파생 인터페이스의 구현 사이에서 선택할 수 없습니다. 일반적으로 인터페이스 정의를 작게 유지하고 하나의 기능에 집중하여 이러한 상황을 방지할 수 있습니다. 이 시나리오에서 광원의 각 기능은 고유한 인터페이스입니다. 클래스만 여러 인터페이스를 상속합니다.

이 샘플은 클래스에 결합될 수 있는 불연속 기능을 정의할 수 있는 시나리오를 보여 줍니다. 클래스가 지원하는 인터페이스를 선언하여 지원되는 기능 집합을 선언합니다. 가상 기본 인터페이스 메서드를 사용하면 클래스가 일부 또는 모든 인터페이스 메서드에 대해 다른 구현을 사용하거나 정의할 수 있습니다. 이 언어 기능은 빌드 중인 실제 시스템을 모델링하는 새로운 방법을 제공합니다. 기본 인터페이스 메서드는 해당 기능의 가상 구현을 사용하여 다른 기능을 조합하고 일치시킬 수 있는 관련 클래스를 더 명확하게 표현하는 방법을 제공합니다.