프로그래밍 언어/Effective C++

[Effective C++]항목 32~34: 객체 지향 설계 1

우향우@ 2023. 5. 15. 20:22

항목 32 : public 상속은   is-a 관계이다. 

public 상속은 is-a관계를 의미한다. c가 p를 상속받는다면 c는 p이다. 하지만 p는 c가 아니다. 기본적인 상속 개념이다.객체 지향 설계에서 상속 계층구조를 설계할 때는 이러한 부분을 잘 고려해야한다.

대표적인 예시로 bird라는 클래스가 있다고하자. 이때 bird에 fly라는 함수를 넣어야할까? 만약 펭귄이 bird를 상속받는다면 이는 문제가 될 수 있다. 펭귄은 날 수 없기 때문이다. 이를 위한 해결책은 bird아래에 canflybird라는 계층을 하나더 두는 방법이 대표적이다. 아니면 bird에 fly를 가상함수로 넣고 펭귄에서 오버라이드하여 에러를 띄우는 등의 특수한 처리를 해주면 될 것이다. 어떤게 더 나은 설계인지는 자신이 만들고자하는 프로그램에 따라 다를테니 스스로 잘 판단할 필요가 있다.

좀 더 어렵고 애매한 경우를 보자. 사각형과 정사각형 클래스가 있다고 하자. 정사각형 클래스는 사각형을 상속받아야할까? 수학적으로는 당연히 그렇다. 하지만 정사각형은 사각형에 가로세로길이가 같아야한다는 제약이 추가된 객체이다. 만약 사각형에 setH와 setW함수가 있다면 정사각형에서는 이를 오버라이드하여 수정해야할것이다. 높이를 바꿔도 너비가 수정될 수 있는것이다. 만약 사각형 포인터를 통해 다형성을 살릴 때 정사각형의 저러한 특성을 고려하지않고 setH후 width가 기존과 그대로인 것을 기대한 상태로 로직을 짰다면 정사각형은 객체는 그 함수에 사용할 수 없을 것이다. 물론 사각형 다형성을 고려할때 이러한 정사각형의 특징까지 미리 고려하여 설계한다면 문제를 막을 수 있을 것이다. 이렇듯 여러 상황을 고려했을때 상황에 따라선 정사각형을 일반 사각형과 분리된 계층구조를 가지거나 형제수준으로 만들수 도 있을 것이다. 물론 필자의 경우 사각형 아래 정사각형을 두고 오버라이드한 뒤 다형성 설계에서 그부분까지 고려한 설계를 선호할 것 같지만 앞서 말했듯 설계하려는 프로그램에 따라 더 나은 선택이 있을 것이다.

 

항목 33 : 상속된 이름을 숨기는 일은 피하자.

c++에는 다음과 같은 문법 특성이 있다.

자식이 특정 이름으로 함수를 선언(그냥 선언 또는 오버라이드)하면 부모의 해당 이름을 가진 모든 시그니처의 함수가 가려진다.

c가 p를 상속받는다 하자. p에는 f1() 함수와 f1(int a)함수가 있다. 만약 여기서 c가 f1()함수를 재정의한다하자. 그리고 c를 가르키는 변수 a로 a.f1()을 호출하면 c의 f1()이 호출될 것이다. 여기서 a.f1(i)를 하면 어떻게 될까? 없는 함수라고 에러가 뜬다. c가 재정의한 f1()이 f1(int a)까지 다 가려버린것이다. 여기서 f1이 가상함수든 아니든 마찬가지로 적용된다.

여기서 c를 통해 p의 f1(int a)를 호출할 수 있도록 하고 싶다면 c의 클래스안에 using p::f1;구문을 추가해주면 된다. 이러면 p의 f1시그니처를 모두 보이게 해준다. 물론 자신이 재정의한 함수는 자신의 것이 보인다.

 

항목 34 : 인터페이스 상속과 구현 상속의 차이

c++에서 함수 선언은 크게 3종류로 나눌 수 있다.

1. 순수 가상 함수

2. 비 순수 가상 함수

3. 비 가상 함수

각 함수가 의미하는 바는 다음과 같다.

 

순수 가상 함수

자식에게 인터페이스만을 물려주는 것이 목적이다. 자식은 인스턴스를 가지기 위해선 자신이 이를 구현해야한다.

비 순수 가상 함수

자식에게 인터페이스와 기본 구현을 물려주는 것이 목적이다. 자식은 기본 구현을 그대로 쓰거나 자신이 재정의하여 사용할 수 있다.

비 가상 함수

자식에게 인터페이스 및 고정 구현을 물려주는 것이 목적이다. 디자인 상 비 가상함수를 재정의하는 것은 좋지않다. 자식은 구현을 그대로 쓰는 것이 설계상 이롭다.

 

사실 순수 가상 함수에도 정의를 붙일 수 있다. 함수 선언에는 = 0을 붙여 순수 가상 함수로 만들고 외부에서 정의하면 된다. 이렇게 하면 인스턴스를 못만들면서도 자식에게 기본 구현을 제공할 수 있다. 물론 자식은 명시적으로 재정의를 해야하며 parentName::functionName()으로 호출하여 사용하는 것을 의미한다. 자동으로 구현이 상속되지는 않는다. 이는 가상 함수의 기본 구현은 제공하되 기본 구현을 사용하고 싶으면 명시적으로 이를 호출하는 재정의를 두도록 설계하고 싶을 때 사용할 수 있다. 비행기등의 실수를 극도로 방지해야하는 설계에서 실수를 방지하기 위해 사용할 수 있다.

 

클래스의 함수를 설계할때는 위 세가지 중 어떤 함수로 만들 것인지 잘 생각해야한다. 절대적인 구현을 가지게 하고 싶다면 비 가상 함수를, 재정의를 할 수 있되 기본구현은 주고 싶다면 가상 함수를, 인터페이스만 줄 것이라면 순수 가상 함수를 넘겨주면 된다. 이러한 컨셉을 확실히 잡고 제공해주는 클래스는 사용하기 편하고 직관적이어 최종적으로는 좋은 유지보수성을 가지게 될 것이다.

 

 

 

 

참고서적

Effective C++