인터페이스는 다음과 같이 interface 키워드를 이용해서 선언합니다.
interface ILogger
{
void WriteLog(string message); //반환_형식 메소드이름1 (매개변수_목록);
}
클래스를 선언하는것과 비슷하지만 인터페이스는 메소드,이벤트,인덱서,프로퍼티만 가질 수 있는데 그나마도 구현부가 없습니다. 클래스는 접근 제한 한정자로 수식하지 않으면 기본적으로 private 으로 선언 되지만,인터페이스는 접근 제한자를 사용할 수 없고 모든것이 public으로 선언됩니다.또한 인스턴스를 만들수 없습니다.
클래스는 객체의 청사진이지만 인터페이스는 클래스의 청사진입니다. 이 청사진은 인터페이스로부터 파생될 클래스가 어떤 메소드를 구현해야 할지를 정의합니다.
인터페이스를 상속하는 실체 클래스는 반드시 인터페이스에 선언된 모든 메소드를 구현해야합니다.
실체 클래스가 어떤 인터페이스의 파생클래스인지 알고 있다면 그 클래스가 어떤 public 메소드를 제공하는지도 알수 있습니다.
비록 인터페이스는 인스턴스를 가질 수 없지만, 이 인터페이스를 상속받는 클래스의 인스턴스를 만드는것은 가능합니다. 물론 이때도 규칙이 있긴 합니다. 파생 클래스는 인터페이스에 선언된 모든 메소드(및 프로퍼티)를 구현해줘야 하며, 이 메소드들은 public 한정자로 수식해야 합니다. 다음은 위에 있는 ILogger 인터페이스를 상속받는 파생클래스의 예입니다.
class ConsoleLogger : ILogger
{
public void WriteLog(string message)
{
Debug.Log($"{DateTime.Now.ToLocalTime()},{message}");
}
}
class MainApp
{
static void main(string[] args)
{
//인터페이스로 선언한 클래스는 다음과 같이 인스턴스화가 가능합니다.
ILogger logger = new ConsoleLogger();
logger.WriteLog("안녕");
}
}
이 코드에 나타난 것처럼 인터페이스는 인스턴스를 못 만들지만, 참조는(ILogger logger) 만들 수 있습니다. 이 참조에 파생클래스의 객체의 위치를 담는 것이죠.
파생클래스는 기반 클래스와 같은 형식으로 간주한다는 사실 기억하고 있죠? 이것은 인터페이스와 인터페이스로 부터 상속받는 클래스의 관계에도 그대로 적용됩니다.
즉,ConsoleLogger의 객체는 ILogger의 객체로 취급할 수 있다는 이야기입니다.
그렇기 때문에 인터페이스는 두 클래스를 연결하는 커넥터 역할도 수행합니다.
아래 예제는 인터페이스를 상속 받은 ConsoleLogger와 FileLogger를 ClimateMonitor를 통해 콘솔이나,텍스트파일에 현재 날짜와 현재 온도를 출력하는 프로그램입니다.
이 예제에서 ClimateMonitor가 Ilogger 참조에 인터페이스를 상속받은 파생클래스 ConsoleLogger와 FileLogger를 둘다 넣을 수 있는 부분을 통해 인터페이스가 커넥터 역할을 수행하는것을 알 수 있습니다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
//**(아래 예제는 유니티에서 작성한 코드입니다)**
//인터페이스 ILogger
interface ILogger
{
void WriteLog(string message);
}
//콘솔창에 메시지를 출력하는 클래스
class ConsoleLogger : ILogger
{
public void WriteLog(string message)
{
Debug.Log($"{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($"{DateTime.Now.ToShortTimeString()}, {message}");
}
}
//ILogger 인터페이스 참조를 이용하여 다양한 메시지를 출력할 수 있는 클래스
class ClimateMonitor
{
private ILogger logger;
public ClimateMonitor(ILogger logger)
{
this.logger = logger;
}
public void execute()
{
string temperature = "30";
logger.WriteLog("현재온도 : " + temperature);
}
}
public class 인터페이스예제 : MonoBehaviour
{
void Start()
{
//생성자 부분에 ConsoleMonitor,FileMonitor 인스턴스를 넣음으로서 다른 기능을 사용할 수있는것을 보여 줍니다.
ClimateMonitor ConsoleMonitor = new ClimateMonitor(new ConsoleLogger());
ConsoleMonitor.execute();
ClimateMonitor FileMonitor = new ClimateMonitor(new FileLogger("MyLog.txt"));
FileMonitor.execute();
}
}
인터페이스는 클래스와 구조체, 더불어 인터페이스에도 인터페이스를 상속할 수 있습니다. 그냥 인터페이스를 수정하여도 되지만 다음 두 경우 처럼 인터페이스를 수정할 수 없을때는 인터페이스를 상속하는 인터페이스를 이용해야 합니다.
상속하려는 인터페이스가 소스 코드가 아닌 어셈블리로만 제공되는 경우
: .NET SDK에서 제공하는 인터페이스들이 그 예입니다. 어셈블리 안에 있기 때문에 인터페이스를 수정할 수 없습니다. 이 인터페이스에 새로운 기능을 추가한 인터페이스를 만들고 싶다면 상속하는 수 밖에 없습니다.
상속 하려는 인터페이스의 소스 코드를 갖고 있어도 이미 인터페이스를 상속 하는 클래스들이 존재 하는 경우
:클래스는 반드시 인터페이스의 "모든" 메소드와 프로퍼티를 구현해야 합니다. 인터페이스에 사소한 수정이라도 이루어지면 이 인터페이스를 상속하는 기존 클래스들은 소스 코드를 빌드할 때 컴파일 에러가 발생할 겁니다. 이런 상황에서 기존의 소스 코드에 영향을 주지 않고 새로운 기능을 추가하기 위해서는 인터페이스를 상속하는 인터페이스를 이용하는것이 좋습니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//**(아래 예제는 유니티에서 작성한 코드입니다)**
namespace Derived_Interface
{
interface ILogger
{
void WriteLog(string message);
}
//ILogger를 상속한 IFormattableLogger 인터페이스
interface IFormattableLogger : ILogger
{
void WriteLog(string format, params object[] args);
}
//IFormattableLogger를 상속받은 ConsoleLogger2 클래스
class ConsoleLogger2 : IFormattableLogger
{
//두 가지 인터페이스의 메소드및 프로퍼티를 모두 구현해야합니다.
public void WriteLog(string message)
{
Debug.Log($"{DateTime.Now.ToLocalTime()} " + message);
}
public void WriteLog(string format, params object[] args)
{
String message = String.Format(format, args);
Debug.Log($"{DateTime.Now.ToLocalTime()} " + message);
}
}
public class 인터페이스를_상속받는_인터페이스 : MonoBehaviour
{
void Start()
{
IFormattableLogger logger = new ConsoleLogger2();
logger.WriteLog("The world is not flat");
logger.WriteLog("{0} {1} {2}", "배건하", 4.1234, 5);
}
}
}
출력
The world is not flat
배건하 4.1234 5
클래스는 여러 클래스를 한꺼번에 상속할 수 없습니다. 이른바 "죽음의 다이아몬드" 라는 문제 때문입니다.
아래 예제에서 MyVehicle은 어떤 Ride메소드를 상속받을지 알 수 없습니다.

그리고 또 하나의 문제인 업케스팅 문제도 있습니다. 다중상속이 허용된다면 다음과 같은 코드가 가능할겁니다.
Plane plane = new MyVehicle();
Car car = new MyVehicle();
//plane은 Run을 출력할까요? 아니면 Fly를 출력할까요?
//정답은 '알 수 없다'입니다.
plane.Ride();
이런 모호성 문제 때문에 C#은 클래스의 다중 상속을 허용하지 않습니다.
하지만,인터페이스는 내용이 아닌 외면을 물려줍니다. 속은 어떨지 몰라도 겉모습만큼은 정확하게 자신을 닮기를 강제합니다. 따라서 죽음의 다이아몬드같은 문제를 발생하지 않습니다.
인터페이스 다중 상속 예제
using Multi_Interface_Inheritance;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Multi_Interface_Inheritance
{
interface IRunnable
{
void Run();
}
interface IFlyable
{
void Fly();
}
class FlyingCar : IRunnable, IFlyable
{
public void Run()
{
Debug.Log("Run!,Run!");
}
public void Fly()
{
Debug.Log("Fly!,Fly!");
}
}
}
public class 인터페이스_다중상속 : MonoBehaviour
{
void Start()
{
FlyingCar car = new FlyingCar();
car.Run();
car.Fly();
//car가 IRunnable를 상속받은게 맞다면 runnable에 할당한다.
IRunnable runnable = car as IRunnable;
runnable.Run();
//car가 IFlyable를 상속받은게 맞다면 flyable에 할당한다.
IFlyable flyable = car as IFlyable;
flyable.Fly();
}
}
다른 클래스에 기능을 새로운 클래스에 넣는 방법은 꼭 상속만 있는것이 아닙니다. 포함이라는 기법이 있습니다. 사실 포함은 기법이랄것도 없는 클래스안에 물려받고 싶은 기능을 가진 클래스를 필드에 선언하는 간단한 방식입니다.
class MyVehicle
{
Car car = new Car();
Plane plane = new Plane();
//메소드안에 메소드를 넣은 간단한 형태
public void Run() {car.Ride()};
public void Fly() {plane.Ride()};
}
지금까지는 인터페이스에 선언하는 메소드에 구현부가 없었습니다.
인터페이스가 선언하는 메소드는 파생될 클래스가 무엇을 구현해야 할지를 정의하는 역할만 하면 됐기 때문입니다.
기본 구현 메소드는 이름처럼 구현부를 가지는 메소드 인데요.
인터페이스의 다른 메소드와는 역할이 약간 다릅니다.
interface ILogger
{
void WriteLog(string message);
}
class ConsoleLogger : ILogger
{
public void WriteLog(string message)
{
Debug.Log($"{DateTime.Now.ToLocalTime()}, {message}")
}
}
ILogger는 평범한 인터페이스이며, ConsoleLogger는 ILogger를 상속받은 평범한 클래스입니다. 여기까지는 우리가 잘 알고있는 내용입니다. 이제 이 코드에 무구한 역사가 더해져 ConsoleLogger도 업그레이드되었을 뿐 아니라, FileLogger같은 ILogger의 파생클래스가 수없이 생겼다고 가정해봅시다.
이런 코드를 레거시(Legacy : 유산) 코드라고 합니다.이런 레거시 코드를 업그레이드 할땐 각별한 주의가 필요합니다.
새로운 메소드를 추가하는 순간 ILogger를 상속하는 모든 클래스에서 컴파일 에러가 발생하기 때문입니다. 상속받은 클래스의 수가 많으면 많을수록 레거시 코드의 업그레이드는 더욱 복잡해질 겁니다.
위 레거시 코드 같은 초기 버전설계에서 놓친 메소드를 인터페이스에 안전하게 넣을때 기본 구현 메소드는 적절합니다.
인터페이스에 새로운 메소드를 추가할 때 기본적인 구현체를 갖도록 해서 기존에 있는 파생 클래스에서 컴파일 에러를 막을 수 있습니다.
또한, 기본 구현 메소드는 인터페이스 참조로 업캐스팅했을 때만 사용할 수 있다는 점때문에 프로그래머가 파생 클래스에서 인터페이스에 추가된 메소드를 엉뚱하게 호출할 가능성도 없습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Default_Implementation
{
interface ILogger
{
void WriteLog(string message); //기존에 ILogger에 있던 메소드
void WriteError(string error) //ILogger에새로운 메소드 추가했다고 가정했을때
{ //기본 구현을 제공하면 됩니다.
WriteLog($"Error: {error}");
}
}
class ConsoleLogger : ILogger //ConsoleLogger는 WriteError()를 오버라이딩 하지않음
{
public void WriteLog(string message)
{
Debug.Log(message);
}
}
}
public class 기본_구현_메소드 : MonoBehaviour
{
void Start()
{
ILogger logger = new ConsoleLogger(); //인터페이스 참조로 업캐스팅
logger.WriteLog("System On"); //OK
logger.WriteError("System Error"); //OK
ConsoleLogger clogger = new ConsoleLogger();
clogger.WriteLog("System On"); //OK
clogger.WriteError("System Error"); //컴파일 에러 : 업캐스팅이 필요합니다.
} //인터페이스에 선언된 기본 구현 인터페이스는
//파생클래스의 참조로 호출할 수 없습니다.
}
추상 클래스는 일반 클래스가 가질 수 있는 구현과 더불어 추상 메소드를 가지고 있습니다. 추상 메소드는 추상 클래스를 사용하는 프로그래머가 그 기능을 정의하도록 강제하는 장치입니다.
이와 똑같은 일을 보통의 클래스를 통해서도 할 수 있습니다.
그냥 메소드를 선언한 다음, 클래스에 대한 매뉴얼을 작성해서 코드와 함께 배포합니다. "이 클래스는 직접 인스턴스화 하지 말고 파생클래스를 만들어 사용하세요. 그리고 MethodA()와 MethodB()를 꼭 오버라이딩해야 합니다." 라는 식으로요 그러나 이를 프로그래머가 준수 하도록 강제시킬 수 는 없습니다.
추상클래스는 abstract 한정자와 class 키워드를 이용해서 선언합니다.
abstract class 클래스 이름
{
//클래스와 동일하게 구현
}
추상 메소드는 추상 클래스가 한편으로 인터페이스의 역할도 할수있게 해주는 장치 입니다. 구현을 갖지는 못하지만 파생클래스에 반드시 구현하도록 강제하거든요 다시말해, 추상클래스를 상속하는 클래스들이 반드시 이 메소드를 갖고있을거라는 약속입니다.
사실 추상클래스는 클래스에 더 가깝습니다. 추상클래스의 접근성이 그 예입니다. 인터페이스는 모드는 메소드가 public으로 선언되는 반면, 추상클래스는 클래스와 같이 한정자를 명시하지 않으면 private으로 선언됩니다. 하지만 약속 역할을 하는 추상메소드가 private면 의미가 없습니다. 그래서 추상 메소드는 반드시 public, protected, internal, protected internal 한정자중 하나를 수식될것을 강요합니다.
추상 메소드는 abstract 한정자와 public, protected, internal, protected internal 한정자중 하나를 써서 만듭니다.
구현할 때는 오버라이딩 하여서 구현합니다.
abstract class AbstractBase
{
public abstract void SomeMethod();
}
class Derived : AbstractBase
{
public override void SomeMethod()
{
//구현
}
}
추상 클래스가 다른 추상 클래스를 상속하는 경우
추상 클래스는 또 다른 추상클래스를 상속할 수 있으며,이 경우 자식 추상 클래스는 부모 추상클래스의 추상 메소드를 구현하지 않아도 됩니다. 추상 메소드는 인스턴스를 생성할 클래스에서 구현하면 되니까요.
using System.Collections;
using System.Collections.Generic;
using AbstractClass;
using UnityEngine;
namespace AbstractClass
{
abstract class AbstractBase //abstract를 사용하여 추상클래스로 선언
{
protected void PrivateMethodA() //추상클래스도 구현을 가질수 있다.
{
Debug.Log("AbstractBase.PrivateMethodA");
}
public void PublicMethodA()
{
Debug.Log("AbstractBase.PublicMethodA");
}
public abstract void AbstractMethodA(); //추상 메소드도 abstract을 사용하여 선언한다.
//구현이 존재하지 않는다. 구현은 파생클래스에서 작성한다.
}
}
class Derived : AbstractBase //추상클래스는 상속해서 사용한다.
{
public override void AbstractMethodA() //추상 메소드는 반드시 구현하여야한다.
{
Debug.Log("Derived.AbstractMethodA");
PrivateMethodA();
}
}
//유니티에서도 위 추상클래스 스크립트는 인스턴스로 생성할수 없었기 때문에 컴포넌트로서 오브젝트에 붙일 수 없다.
//스크립트를 나눠서 아래 코드만 따로 인스턴스에 붙여서 사용할수 있었다.
public class 추상클래스오브젝트에붙이기 : MonoBehaviour
{
void Start()
{
//AbstractBase Abase = new AbstractBase(); //추상클래스는 인스턴스를 만들수없다.
AbstractBase obj = new Derived();
obj.AbstractMethodC();
obj.PublicMethodB();
}
}
출력
Derived.AbstractMethodA
AbstractBase.PrivateMethodA
AbstractBase.PublicMethodA