[C++] 항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 6. 상속, 그리고 객체 지향 설계

👉🏻 항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

✅ public 상속의 두 가지 측면

public 상속은 인터페이스 상속과 구현 상속으로 나뉜다.

클래스 설계자가 선택할 수 있는 사항:

  1. 인터페이스만 상속
  2. 인터페이스 및 구현 모두 상속받고, 구현 오버라이드 가능
  3. 인터페이스 및 구현 모두 상속받고, 어떤 것도 오버라이드 불가능

📌 세 가지 함수 타입의 차이

class Shape {
public:
	virtual void draw() const = 0; // 순수 가상 함수
	virtual void error(const string& msg); // 단순 가상 함수
	int objectID() const; // 비가상 함수
	...
};

class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };

핵심:

  • Shape 클래스는 순수 가상 함수인 draw()의 존재로 인해, 추상 클래스가 된다

✅ 1. 순수 가상 함수: 인터페이스만 상속

virtual void draw() const = 0;

특징:

  • 순수 가상 함수를 물려받은 파생 클래스는 해당 순수 가상 함수를 다시 선언해야 한다
  • 순수 가상 함수는 추상 클래스 안에서 정의를 갖지 않는다

목적:

순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스를 물려주는 것

Shape* ps = new Shape; // ❌ Shape는 추상 클래스

Shape* ps1 = new Rectangle; // ✅
ps1->draw(); // Rectangle::draw 호출
Shape* ps2 = new Ellipse; // ✅
ps2->draw(); // Ellipse::draw 호출

주의점:

  • 순수 가상 함수 Shape::draw에도 정의를 붙일 수는 있다
  • 단, 클래스 이름을 한정자로 붙여주어야 한다
ps1->Shape::draw(); // ✅ Shape::draw 호출

✅ 2. 단순 가상 함수: 인터페이스 + 기본 구현 상속

virtual void error(const string& msg);

목적:

단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려주는 것

특징:

  • 파생되는 클래스마다 기본으로 제공되는 error 함수를 써도 된다

단순 가상 함수의 예시

class Airport { ... };

class Airplane {
public:
	virtual void fly(const Airport& destination);
	...
};

// 목적지로 비행기를 날려보내는 단순 가상 함수
void Airplane::fly(const Airport& destination) { ... };

class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };

장점:

  • ModelA와 ModelB는 Airplane::fly 함수를 물려받는다
  • 클래스 사이의 공통적 특징이 명확해진다
  • 코드가 중복되지 않는다
  • 확장에 열려있다
  • 유지 보수가 쉽다

⚠️ 단순 가상 함수의 문제점

비행 방식이 다른 ModelC 비행기에서 fly 함수 재정의하는 것을 빼먹음

class ModelC : public Airplane {
	...
};

int main() {
	Airport PDX(...);
	Airplane* pa = new ModelC;
	...
	// Airplane::fly 함수 호출
	// 재정의를 깜빡함!
	pa->fly(PDX);
}

문제:

  • 재정의를 강제할 방법이 없다
  • 기본 구현이 적절하지 않은 경우에도 사용될 수 있다

✅ 해결 방법 1: 인터페이스와 구현 분리

class Airplane {
public:
	virtual void fly(const Airport& destination) = 0;
	...

protected:
	void defaultFly(const Airport& destination);
};

// 목적지로 비행기를 날려보내는 기본 구현
void Airplane::defaultFly(const Airport& destination) { ... }

class ModelA : public Airplane {
public:
	virtual void fly(const Airport& destination)
	{ defaultFly(destination); }
	...
};

class ModelB : public Airplane {
public:
	virtual void fly(const Airport& destination)
	{ defaultFly(destination); }
	...
};

class ModelC : public Airplane {
public:
	virtual void fly(const Airport& destination);
	...
};

// 목적지로 'ModelC 비행기'를 날려보내는 구현
void ModelC::fly(const Airport& destination) { ... }

개선된 점:

  • Airplane::fly 함수를 순수 가상 함수로 변경 → fly 함수의 인터페이스 제공 역할
  • defaultFly 함수를 일반(비가상) 함수로 구현 → 파생 클래스에서 이 함수를 재정의할 수 없도록 함

단점:

  • 클래스의 네임스페이스가 더러워진다 (fly 함수, defaultFly 함수 때문)

✅ 해결 방법 2: 순수 가상 함수에 구현 제공

class Airplane {
public:
	virtual void fly(const Airport& destination) = 0;
	...
};

// 순수 가상 함수 구현
void Airplane::fly(const Airport& destination) { ... }

class ModelA : public Airplane {
public:
	virtual void fly(const Airport& destination)
	{ Airplane::fly(destination); }
	...
};

class ModelB : public Airplane {
public:
	virtual void fly(const Airport& destination)
	{ Airplane::fly(destination); }
	...
};

class ModelC : public Airplane {
public:
	virtual void fly(const Airport& destination);
	...
};

// 목적지로 'ModelC 비행기'를 날려보내는 구현
void ModelC::fly(const Airport& destination) { ... }

개선된 점:

  • 순수 가상 함수를 구현함으로써 문제 해결
  • 이전보다 더 깔끔하다
  • fly와 defaultFly가 합쳐짐

단점:

  • 서로 다른 보호 수준(public, protected)으로 부여할 수 없어졌다

✅ 3. 비가상 함수: 인터페이스 + 필수 구현 상속

int objectID() const;

목적:

비가상 함수를 선언하는 목적은 파생 클래스가 함수 인터페이스와 더불어, 그 함수의 필수적인 구현을 물려받게 하는 것

핵심:

  • 파생 클래스는 이 함수를 바꿀 수 없다

📌 함수 타입별 상속 특성 정리

함수 타입 인터페이스 상속 구현 상속 오버라이드
순수 가상 함수 ❌ (선택적) 필수
단순 가상 함수 ✅ (기본) 선택
비가상 함수 ✅ (필수) 불가

⚠️ 피해야 할 설계 실수

1. 모든 멤버를 비가상 함수로 선언

문제점:

  • 기본 클래스의 동작을 특별하게 만들 이유 사라짐
  • 비가상 소멸자 문제(항목 7 참조)
  • 가상 함수/비가상 함수의 성능 차이는 없다

2. 모든 멤버를 가상 함수로 선언

문제점:

  • 파생 클래스에서 재정의가 안 되어야 할 함수는 비가상 함수로 만들어야 한다

🧐 정리

  1. 인터페이스 상속은 구현 상속과 다르다. public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받는다.
  2. 순수 가상 함수는 인터페이스 상속만을 허용한다. 파생 클래스가 반드시 구현해야 한다.
  3. 단순 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다. 파생 클래스가 선택적으로 재정의할 수 있다.
  4. 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정한다. 파생 클래스가 변경할 수 없다.
  5. 클래스 설계 시 각 함수의 특성에 맞는 타입을 선택하자. 모든 함수를 가상으로 만들거나 비가상으로 만드는 것은 좋지 않다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기