[C++] 항목 32: public 상속 모형은 반드시 “is-a(…는 …의 일종이다)”를 따르도록 만들자

업데이트:     Updated:

카테고리:

태그:

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

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

👉🏻 항목 32: public 상속 모형은 반드시 “is-a(…는 …의 일종이다)”를 따르도록 만들자

✅ public 상속의 의미: is-a 관계

public 상속은 “is-a(…는 …의 일종이다)”를 의미한다.

핵심 원칙:

  • 클래스 D(Derived)를 클래스 B(Base)로부터 public 상속을 통해 파생시켰다면,
  • D 타입으로 만들어진 모든 객체는 B타입의 객체이지만 역은 성립하지 않는다

👉🏻 기본 예시: 사람과 학생

class Person { ... };
class Student : public Person { ... };

void eat(const Person& p);
void study(const Student& s);

int main() {
	Person p;
	Student s;

	eat(p); // O
	eat(s); // O - 학생은 사람이다

	study(s); // O
	study(p); // X - 사람이 모두 학생은 아니다
}

is-a 관계 확인:

  • 모든 ‘학생’은 ‘사람’이다 ✅
  • 모든 ‘사람’은 ‘학생’이 아니다

⚠️ 잘못된 예시 1: 펭귄과 새

문제가 있는 설계

class Bird {
public:
	virtual void fly();
	...
};

class Penguin : public Bird {
	...
};

논리적 모순:

  • ‘펭귄’은 ‘새’다 ✅
  • ‘새’는 날 수 있다 ✅
  • ‘펭귄’은 날 수 있다 ❌ (실제로는 날 수 없음!)

✅ 해결 방법 1: 상속 계층 재설계

class Bird {
	...
};

class FlyingBird : public Bird {
public:
	virtual void fly();
	...
};

class Penguin : public Bird {
	... // fly 함수 선언되지 않음
};

개선된 점:

  • 이 코드를 통해 위 코드의 실수를 해결하였다
  • 날 수 있는 새와 날 수 없는 새를 명확히 구분

주의점:

  • 어떤 시스템은 새를 비행 능력으로 구분할 필요가 없을 수도 있다
  • 그러므로 소프트웨어에는 이상적인 설계가 없다

❌ 잘못된 해결 방법: 런타임 에러

void error(const string& msg); // 어딘가 정의해 둠

class Penguin : public Bird {
public:
	virtual void fly() {
		error("Attempt to make a penguin fly!");
	}
};

문제점:

  • 펭귄의 fly 함수를 재정의하여, 고의적으로 런타임 에러를 낸다
  • “펭귄은 날 수 있지만, 실제로 날려고 하면 에러가 난다”
  • 프로그램이 실행될 때만 발견할 수 있다
    잘못되었다

✅ 올바른 설계 원칙: 컴파일 타임 검사

class Bird {
	... // fly 선언 안됨
};

class Penguin : public Bird {
	... // fly 선언 안됨
};

int main() {
	Penguin p;
	p.fly(); // 에러 - 컴파일 타임에 잡힘
};

핵심 원칙:

“유효하지 않은 코드를 컴파일 단계에서 막아주는 인터페이스가 좋은 인터페이스”이다.


⚠️ 잘못된 예시 2: 정사각형과 직사각형

class Rectangle {
public:
	virtual void setHeight(int newHeight);
	virtual void setWidth(int newWidth);

	virtual int height() const;
	virtual int width() const;
	...
};

// r의 넓이를 늘리는 함수
// 가로 길이만 10 늘림
void makeBigger(Rectangle& r) {
	int oldheight = r.height();
	r.setWidth(r.width() + 10);

	// r의 세로 길이가 변하지 않는다는 조건에 단정문
	assert(r.height() == oldHeight);
}

class Square : public Rectangle { ... };

int main() {
	Square s;
	...

	assert(s.width() == s.height());
	makeBigger(s);
	assert(s.width() == s.height()); // 단정문 실패!
}

문제 분석:

  • ‘정사각형(Square)’은 ‘직사각형(Rectangle)’이다? → 수학적으로는 맞지만…
  • 그러나, 직사각형의 성질 중 어떤 것은 정사각형에 적용할 수 없다
  • 직사각형은 가로와 세로를 독립적으로 변경할 수 있지만, 정사각형은 불가능
  • makeBigger 함수는 직사각형의 가로만 변경한다고 가정하지만, 정사각형에서는 이 가정이 깨진다

핵심 교훈:

public 상속은 기본 클래스 객체가 가진 모든 것들이,
파생 클래스 객체에도 적용된다고 단정하는 상속이다.


📌 클래스 간의 관계들

클래스들 사이에 맺을 수 있는 관계:

  1. is-a(…는 …의 일종이다)
    • public 상속으로 표현
    • 예: Student is-a Person
  2. has-a(…는 …를 가짐)
    • 멤버 변수로 표현
    • 예: Person has-a Address
  3. is-implemented-in-terms-of(…는 …를 써서 구현됨)
    • 구현 상속 또는 컴포지션으로 표현
    • 예: Stack is-implemented-in-terms-of List

권장사항:

클래스 사이에 맺을 수 있는 관계들을 명확하게 구분할 수 있도록 하고,
C++로 가장 잘 표현하는 방법을 공부해두자.


🧐 정리

  1. public 상속의 의미는 “is-a(…는 …의 일종)”이다.
  2. 기본 클래스에 적용되는 모든 것들이 파생 클래스에 그대로 적용되어야 한다.
    왜냐하면 모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문이다.
  3. 유효하지 않은 코드는 컴파일 타임에 막을 수 있도록 설계하자.
    런타임 에러로 처리하는 것은 좋은 설계가 아니다.
  4. 수학적/일상적 관계가 프로그래밍에서 항상 맞는 것은 아니다.
    소프트웨어 맥락에서 is-a 관계를 신중하게 판단하자.

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

댓글남기기