추상 클래스(Abstract class)와 인터페이스 클래스 (Interface class)
순수 가상 함수(Pure virtual function)
가상 함수의 선언만 있고 정의가 없는 클래스를 순수 가상 함수(Pure virtual function)이라고 한다. C++에서는 virtual function에 0을 붙여서 만들 수 있다. 순수 가상 함수를 가진 클래스는 객체를 생성할 수 없다.
class Animal {
public:
virtual ~Animal() = default;
virtual void Speak() = 0; // pure virtual function
private:
};
추상 클래스 (Abstract class)
순수 가상함수를 하나라도 가지고 있는 클래스를 추상 클래스 (Abstract class)라고 한다. 추상 클래스는 순수 가상 함수를 가지고 있기 때문에 클래스는 객체를 생성할 수 없다. 그 이유는 아래와 같다.
- 추상적인 클래스이기 때문에 구체적인 객체를 생성할 수 없는 것이다.
- 또한 위 코드에서 만약 Animal의 객체가 생성된다면 그 객체가 참조할 수 있는 Speak() 함수의 정의가 존재하지 않기 때문에 객체가 생성될 경우 문제가 발생할 수밖에 없다.
class Animal {
public:
virtual ~Animal() = default;
virtual void Speak() = 0; // pure virtual function
private:
};
// cat 클래스 생략
int main()
{
Animal *animal = new Animal(); // 에러: 추상 클래스로 객체를 생성할 수 없다.
return 0;
}
하지만 순수 가상 함수를 override 한 자식 클래스는 정상적으로 객체를 생성할 수 있다. 이때 반드시 순수 가상 함수를 정의 할 경우에만 호출 가능하다는 점이 중요하다.
// Animal 클래스 생략
class Cat : public Animal {
public:
void Speak() override
{
std::cout << "mew~" << std::endl;
}
private:
};
class Dog : public Animal {
};
int main()
{
Cat *cat = new Cat();
Dog *dog = new Dog(); // 에러: 순수 가상 함수 정의 필요
return 0;
}
추상 클래스를 사용하는 이유
기본 클래스가 추상 클래스라면 이를 상속받는 클래스들은 반드시 순수 가상 함수가 재정의 됨이 보장된다. 만약 게임 아키텍쳐를 설계하는 사람이 아래와 같이 Monster 클래스를 설계하였다고 가정하자.
class Monster {
public:
virtual void Attack() = 0;
virtual void Defense() = 0;
virtual ~Monster() = default;
};
그렇다면 이 Monster 클래스를 상속받아 게임의 디테일을 개발하는 개발자는 반드시 모든 몬스터에 Attack() 함수와 Defense() 함수를 재정의하도록 강제된다.
// Monster 클래스 생략
class Rat : public Monster{
public:
void Attack() override
{
// Rat Attack
}
void Defense() override
{
// Rat Defense
}
};
class Rabbit : public Monster {
public:
void Attack() override
{
// Rabbit Attack
}
void Defense() override
{
// Rabbit Defense
}
};
그리고 아키텍처를 설계하는 개발자는 하위 클래스의 순수 가상 함수가 재정의 됨을 보장받기 때문에 더 큰 설계에 집중할 수 있다.
인터페이스 클래스 (Interface class)
순수 가상함수를 통해서 부모 클래스 개발자는 인터페이스만 정의하면 되기 때문에 이러한 클래스를 Interface class라고도 한다. Interface class를 만들 때 더 유용한 Interface class를 만들기 위해서는 멤버 함수를 정의하지 않고, 멤버 변수도 정의하지 않는 것이 좋다. 아래의 예시를 살펴보자.
class Animal
{
public:
virtual void Speak() = 0;
void Walk()
{
// 내부 구현
};
protected:
int legCount; // 멤버 변수
};
class Cat : public Animal
{
public:
void Speak() override
{
std::cout << "mew~" << std::endl;
}
Cat()
{
legCount = 4;
}
};
만약 위와 같이 Speak 함수를 인터페이스로 제공하고 Walk
는 내부를 구현하고, legCount
라는 멤버 변수를 가지는 추상 클래스인 Animal 클래스가 존재한다고 생각해보자. 그러면 Animal 클래스를 상속받은 Cat 클래스는 문제없이 주어진 인터페이스를 구현하여 사용할 수 있을 것이다. 하지만 새로운 동물인 Dolphine을 추가하게 된다면 다음과 같은 문제가 생길 수 있다.
- Dolphine은 걸을 수 없는데 이미 Animal에 구현까지 끝나있기 때문에
Walk
를 반드시 재정의 해야하지만Walk
에 대한 재정의가 강제되지 않는다. - Dolphine은 사용하지 않는
legCount
라는 변수를 가져야 한다.
그래서 더욱 유연하고 재사용성이 높은 인터페이스 클래스를 정의하기 위해서는 다음과 같이 모든 함수는 순수 가상 함수로 정의하여 인터페이스만 제공하고, 멤버 변수는 정의하지 않으며 상속받은 클래스에서 필요한 멤버 변수를 정의할 수 있도록 하는 것이 바람직하다.
class Animal
{
public:
virtual void Speak() = 0;
virtual void Walk() = 0;
};
Interface와 Implementation의 분리
위와 같이 Interface class에 멤버함수의 구현과, 멤버 변수를 모두 빼게 되면 중복해서 정의해야 하는 문제가 발생할 수 있다. 그래서 Interface 클래스와 내부를 구현하는 Implementation 클래스를 분리하여서 구현한 후 필요한 것만 골라서 상속을 받는 방법을 활용할 수 있다. 이는 다음 글 다중 상속에서 더 다룬다.
출처: https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg