[C++] 항목 32: public 상속 모형은 반드시 “is-a(…는 …의 일종이다)”를 따르도록 만들자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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 상속은 기본 클래스 객체가 가진 모든 것들이,
파생 클래스 객체에도 적용된다고 단정하는 상속이다.
📌 클래스 간의 관계들
클래스들 사이에 맺을 수 있는 관계:
- is-a(…는 …의 일종이다)
- public 상속으로 표현
- 예: Student is-a Person
- has-a(…는 …를 가짐)
- 멤버 변수로 표현
- 예: Person has-a Address
- is-implemented-in-terms-of(…는 …를 써서 구현됨)
- 구현 상속 또는 컴포지션으로 표현
- 예: Stack is-implemented-in-terms-of List
권장사항:
클래스 사이에 맺을 수 있는 관계들을 명확하게 구분할 수 있도록 하고,
C++로 가장 잘 표현하는 방법을 공부해두자.
🧐 정리
- public 상속의 의미는 “is-a(…는 …의 일종)”이다.
- 기본 클래스에 적용되는 모든 것들이 파생 클래스에 그대로 적용되어야 한다.
왜냐하면 모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문이다. - 유효하지 않은 코드는 컴파일 타임에 막을 수 있도록 설계하자.
런타임 에러로 처리하는 것은 좋은 설계가 아니다. - 수학적/일상적 관계가 프로그래밍에서 항상 맞는 것은 아니다.
소프트웨어 맥락에서 is-a 관계를 신중하게 판단하자.
댓글남기기