결론부터 놓고 들어가자. 자세한 내용은 표 아래에 설명하고 있다.
추상 클래스 | 인터페이스 | |
---|---|---|
접근 지정자 | 반드시 public이어야 하는 추상 메서드를 제외한 모든 멤버는 private으로도 선언될 수 있다. | 명시하지 않아도 모든 메서드는 public으로 고정이다. 재정의도 반드시 public이어야 한다. |
구현 | 추상 메서드를 제외한 모든 메서드는 구현을 포함할 수 있다. | 구현을 포함할 수 없다. |
속도 | 비교적 빠르다. | 비교적 느리다. 하지만 무시할 수 있는 수준이다. |
인스턴스화 | 할 수 없다. | 할 수 없다. |
필드 | 가질 수 없다. | 가질 수 있다. |
메서드 | 모든 형태의 메서드를 가질 수 있다. | 추상 메서드만 가질 수 있다. abstract 키워드를 포함하진 않는다. |
OOP의 개념들은 여러 언어에 다른 포맷으로 존재한다. C++의 추상 클래스는
class Test{
public:
Test() { }
virtual void func() = 0; // 순수 가상 함수
};
와 같은 형태의 순수 가상 함수를 포함하고 있는 클래스였다.
함수 선언에서 추상과 가상의 가장 큰 차이는 재정의를 반드시 해야 하는지 여부다.
추상 함수는 원형을 선언만 가능하고 정의할 수 없다. 추상 함수가 정의된 클래스를 추상 클래스라 하고, 추상 클래스를 상속한 클래스는 반드시 추상 함수를 재정의해야 한다. 하지만 가상 함수는 필요에 따라 재정의할 수 있다.
C#의 추상 클래스는 비슷하지만 조금 다른 문법으로 이루어진다.
abstract class AbstractTest
{
int num;
public void Func()
{
Console.WriteLine("Func");
}
virtual public void VirtualFunc()
{
Console.WriteLine("Virtual Func");
}
abstract public void AbstractFunc();
};
private
이다.virtual
대신 abstract
키워드로 선언하여 명확하게 구분할 수 있다.abstract
키워드를 포함한 추상 클래스가 되어야 한다.인터페이스는 쉽게 말해 함수 선언만 할 수 있는 추상 클래스라 보면 된다. 변수를 선언하는 것도, 함수를 정의하는 것도 불가능하다.
인터페이스는 함수 선언만 포함할 수 있으며,인터페이스 안에 선언한 모든 함수는 인터페이스를 상속하는 클래스에서 반드시 구현해야 한다. 인터페이스는 다중 상속이 가능하다.
interface InterfaceTest
{
void InterfaceFunc();
}
abstract class AbstractTest
{
...
class Test : AbstractTest, InterfaceTest
{
...
public void InterfaceFunc()
{
Console.WriteLine("Interface Func");
}
};
인터페이스가 왜 필요할까? 위의 표만 봐선 속도도 느리고 클래스를 선언하는 것이 더 좋은 선택인 것 같다. 인터페이스의 핵심은 '다형성'과 '다중 상속'이다. 인터페이스는 클래스와 개념적으로 다르지만 클래스와 유사한 부분이 많다.
우선, 다형성에 의해 기초 클래스로 선언하여 파생 클래스 인스턴스를 저장할 수 있는 것처럼, 인터페이스를 상속한 클래스는 인터페이스 인스턴스로서 사용될 수 있다.
...
Test t = new Test();
InterfaceTest t2 = t;
Unity 엔진을 활용한 게임 개발에서 예를 들어보자.
플레이어가 논타겟으로 공격했을 때 범위 안의 모든 공격 가능한 오브젝트에게 피해를 준다고 하자. 이 경우 대부분 몬스터가 해당하겠지만, 공격 가능한 오브젝트 등도 해당될 수 있다. 이때 간단하게는 다음과 같이 코드를 작성할 수 있다.
private void OnCollisionEnter(Collision collision)
{
if (CompareTag("Attackable"))
{
collision.gameObject.GetComponent<Status>().hp -= damage;
}
}
하지만, Tag를 위와 같이 너무 큰 범위로 묶는 것은 효율적이지 못하다. 그렇다고 태그별로 조건문을 일일이 달아줄 수도 없는 노릇이다. 이럴 때 인터페이스를 활용하면 다음과 같이 코드를 바꿀 수 있다.
private void OnCollisionEnter(Collision collision)
{
Attackable attackable = collision.gameObject.GetComponent<Attackable>();
if (attackable != null)
{
attackable.Damaged();
}
}
인터페이스를 상속하면 컴포넌트로써 읽는 것이 가능해지고, 다중 상속이 가능한 덕분에 어떤 클래스에라도 Attackable
인터페이스를 상속하여 공격 가능한 오브젝트임을 표현할 수 있다.
그리고 인터페이스에 Damaged()
메서드를 선언하여 필요한 경우 매개변수로 플레이어의 대미지를 전달하고, 공격 당한 클래스가 가진 다양한 변수를 Damaged()
메서드에서 활용해 대미지 적용식을 작성할 수도 있다. 다른 처리가 필요할 경우 역시 Damaged()
메서드에 추가해주면 된다.
일반적으로 유니티 게임오브젝트는 Monobehaviour
클래스를 상속하기 때문에 다른 클래스를 상속하기 어려운 경우가 많아 인터페이스를 위와 같이 적극적으로 활용할 수 있다.
▶ C# - 상속과 interface(인터페이스) 그리고 abstract class(추상 클래스)
▶ 가상함수, 순수가상함수, 추상클래스
▶ C# - interface 와 abstract class 의 차이
▶ C# - Virtual(가상) vs Abstract(추상) vs Interface(인터페이스)
▶ [Reddit] C# interface and performance
▶ C# 인터페이스 활용의 단편적인 예