C#

[C#] 인터페이스

sunlight-dby 2025. 4. 24. 01:29

 

'이것이 C#이다 개정판'의 chapter 8 정리한 글입니다.

 


 

인터페이스 (Interface)

interface 인터페이스_이름
{
    반환_형식 메소드이름1( 매개변수_목록);
    반환_형식 메소드이름2( 매개변수_목록);
    반환_형식 메소드이름3( 매개변수_목록);
    // ...
}

 

[특징]

  • 인터페이스는 메소드, 이벤트, 인덱서, 프로퍼티만 가질 수 있습니다. 또한, 이에 대한 구현부가 존재할 수 없습니다.
  • 인터페이스는 접근 제한 한정자를 사용할 수 없고 모든 것이 public으로 선언됩니다.
  • 인터페이스는 인스턴스를 만들수가 없습니다.
    • 인터페이스를 상속받는 클래스의 인스턴스를 만드는 것은 가능합니다.
  • 인터페이스를 상속받는 파생 클래스는 인스턴스의 선언된  모든 메소드 및 프로퍼티를 구현해줘야 하며, 이 메소드들을 public 하정자로 수식해야 합니다.
    • 인터페이스는 값 타입인 구조체에서도 상속받을 수 있습니다.
  • 인터페이스를 상속받는 클래스를 인스턴스화 할 때, 클래스의 인스턴스를 생성하고 그 인스턴스를 인터페이스 타입의 변수에 할당합니다.
interface ILogger
{
    void WriteLog( string message );
}

class ConsoleLogger : ILogger
{
    public void WriteLog( string message )
    {
        Console.WriteLine( "{0} {1}", DateTime.Now.ToLocalTime(), message);
    }
}

// ...
ILogger logger = new ConsoleLogger();
logger.WriteLog( "Hello, World!" );

 

[예제]

using System;
using System.IO;

namespace Interface
{
     // ConsoleLogger와 FileLogger는 ILogger를 상속하며, WriteLog() 메소드를 구현합니다.
    interface ILogger
    {
        void WriteLog(string message);
    }

    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }
    }

    class FileLogger : ILogger
    {
        private StreamWriter writer;

        public FileLogger(string path)
        {
            writer = File.CreateText(path);
            writer.AutoFlush = true;
        }

        public void WriteLog(string message)
        {
            writer.WriteLine("{0} {1}", DateTime.Now.ToShortTimeString(), message);
        }
    }

    class ClimateMonitor
    {
        private ILogger logger;

        public ClimateMonitor(ILogger logger)
        {
            this.logger = logger;
        }

        public void start()
        {
            while (true)
            {
                Console.Write("온도를 입력해주세요. : ");
                string temperature = Console.ReadLine();
                if (temperature == "")
                    break;

                logger.WriteLog("현재 온도 : " + temperature);
            }
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            // monitor 객체는 애플리케이션이 시작된 디렉터리에 MyLog.txt를 만들고 여기에 로그를 남깁니다.
            ClimateMonitor monitor = new ClimateMonitor(new FileLogger("MyLog.txt"));

            monitor.start();
        }
    }
}

실행 후 입력
MyLog.txt에 담긴 로그

 

 


인터페이스를 상속하는 인터페이스

인터페이스를 상속할 수 있는 것은 클래스뿐이 아닙니다. 구조체는 물론이고, 인터페이스도 인터페이스를 상속할 수 있습니다.

 

기존의 인터페이스에 새로운 기능을 추가한 인터페이스를 만들고 싶을 때 인터페이스를 상속하는 인터페이스를 만들면 됩니다.

인터페이스를 수정할 수 없을 때 주로 인터페이스를 상속하는 인터페이스를 이용합니다.

 

[인터페이스를 수정할 수 없는 경우]

  • 상속하려는 인터페이스가 소스 코드가 아닌 어셈블리로만 제공되는 경우
    • .NET SDK에서 제공하는 인터페이스들이 그 예입니다. 어셈블리 안에 있기 때문에 인터페이스를 수정할 수 없습니다.
       이 인터페이스에 새로운 기능을 추가한 인터페이스를 만들고 싶으면 상속하는 수밖에 없습니다.
  • 상속하려는 인터페이스의 소스 코드를 갖고 있어도 이미 인터페이스를 상속하는 클래스들이 존재하는 경우
    • 클래스는 반드시 인터페이스의 "모든" 메소드와 프로퍼티를 구현해야 합니다.  인터페이스에 사소한  수정이라도 이루어지면 이 인터페이스를 상속하는 기존 클래스들은 소스 코드를 빌드할 때 컴파일 에러가 발생하게 됩니다.
      이런 상황에서 기존의 소스 코드에 영향을 주지 않고 새로운 기능을 추가하기 위해서는 인터페이스를 상속하는 인터페이스를 이용하는 편이 좋습니다.

[문법]

interface 파생 인터페이스 : 부모_인터페이스
{
    // ... 추가할 메소드 목록
}

 

 [예제]

using System;

namespace DerivedInterface
{
    interface ILogger
    {
        void WriteLog(string message);
    }

    // IFormattableLogger는 ILogger를 상속합니다.
    interface IFormattableLogger : ILogger
    {
        void WriteLog(string format, params Object[] args);
    }

    // ConsoleLogger는 IFormattableLogger를 상속합니다.
    class ConsoleLogger : IFormattableLogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }

        public void WriteLog(string format, params Object[] args)
        {
            String message = String.Format(format, args);
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            IFormattableLogger logger = new ConsoleLogger();
            logger.WriteLog("The world is not flat.");
            logger.WriteLog("{0} + {1} = {2}", 1, 1, 2);
        }
    }
}

실행 결과


인터페이스의 다중 상속 (Muliple Inheritance)

C#에서는 어떻게 동작할지 정확하게 예측할 수 없는 다중 상속을 허용하지 않습니다.

다만, 인터페이스는 다중 상속이 가능합니다.

 

※ C++에서는 다중 상속을 허용합니다.

 

[예제]

using System;

namespace MultiInterfaceInheritance
{
    interface IRunnable
    {
        void Run();
    }

    interface IFlyable
    {
        void Fly();
    }

    class FlyingCar : IRunnable, IFlyable
    {
        public void Run()
        {
            Console.WriteLine("Run");
        }

        public void Fly()
        {
            Console.WriteLine("Fly");
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            FlyingCar car = new FlyingCar();
            car.Run(); 
            car.Fly();

            IRunnable runnable = car as IRunnable;
            runnable.Run();

            IFlyable flyable = car as IFlyable;
            flyable.Fly();
        }
    }
}

실행 결과


인터페이스의 기본 구현 메소드

인터페이스의 기본 구현 메소드는 이름처럼 구현부를 가지는 메소드입니다. 인터페이스의 다른 메소드와는 역할이 약간 다릅니다.

 

기본 구현 메소드는 인터페이스에 새로운 메소드를 추가해놓고 클래스에서 구현부를 갖추지 못했을 상황을 대비하는데 효과적입니다.

이렇게 구현한 기본 구현 메소드는 다른 기존 코드에는 아무런 영향을 주지 않습니다.

인터페이스의 기본 구현 메소드는 인터페이스 참조로 업캐스팅했을 때만 사용할 수 있다는 점 때문에 프로그래머가 파생 클래스에서 인터페이스에 추가된 메소드를 엉뚱하게 호출할 가능성도 없습니다.

 

[예제]

using System;

namespace DefaultImplementation
{
    interface ILogger
    {
        void WriteLog(string message);

        void WriteError(string error)  // 새로운 메소드 추가
        {
            WriteLog($"Error : {error}");
        }
    }

    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine($"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            logger.WriteLog("System Up");
            logger.WriteError("System Fail");

            ConsoleLogger clogger = new ConsoleLogger();
            clogger.WriteLog("System Up");  // 컴파일 OK
            // clogger.WriteError("System Fail");  // 컴파일 에러
        }
    }
}

컴파일 에러 주석 해제 시 발생되는 컴파일 에러 메시지
실행 결과

 

위의 코드에서 컴파일 에러를 발생시키는 이유는 ConsoleLogger가 WriteError()를 오버라이딩하지 않았기 때문입니다.

즉, 인터페이스에 선언된 기본 구현 인터페이스는 파생 클래스의 참조로 호출할 수 없습니다.


추상 클래스 (Abstract Class)

추상 클래스는 구현부를 갖되, 인스턴스는 생성하지 못합니다. 다만, 추상 클래스는 추상 메소드(Abstract Method)를 가집니다.

추상 메소드는 추상 클래스가 한편으로 인터페이스의 역할도 할 수 있게 해주는 장치입니다.

구현을 갖지는 못하지만 파생 클래스에서 반드시 구현하도록 강제하는 점이 인터페이스의 역할도 한다는 것입니다.

 

추상 클래스는 일반 클래스가 가질 수 있는 구현과 더불어 추상 메소드를 가지고 있습니다.

추상 메소드는 추상 클래스를 사용하는 프로그래머가 그 기능을 정의하도록 강제하는 장치입니다.

 

추상 클래스는 abstract 한정자와 class 키워드를 이용해서 선언합니다. 추상 메소드도 abstract 한정자를 이용해 선언합니다.

abstract class AbstractBase
{
    public abstract void SomeMethod();
    // ...
}

class Derived : AbstractBase
{
    public override void SomeMethod()
    {
        // ...
    }
}

 

※ 추상 클래스가 또 다른 추상 클래스를 상속하는 경우

추상 클래스는 또 다른 추상 클래스를 상속할 수 있으며, 이 경우 자식 추상 클래스는 부모 추상 클래스의 추상 메소드를 구현하지 않아도 됩니다. 추상 메소드는 인스턴스를 생성할 클래스에서 구현하면 되기 때문입니다.

 

[예제]

using System;

namespace AbstractClass
{
    abstract class AbstractBase
    {
        protected void PrivateMethodA()
        {
            Console.WriteLine("AbstractBase.PrivateMethodA()");
        }

        public void PublicMethodA()
        {
            Console.WriteLine("AbstractBase.PublicMethodA()");
        }

        public abstract void AbstractMethodA();
    }

    class Derived : AbstractBase
    {
        public override void AbstractMethodA()
        {
            Console.WriteLine("Derived.AbstractMethodA()");
            PrivateMethodA();
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            AbstractBase obj = new Derived();
            obj.AbstractMethodA();
            obj.PublicMethodA();
        }
    }
}

실행 결과