본문 바로가기
프로그래밍/Effective C++

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

by Ohdumak 2018. 6. 7.

2018/05/23 - [프로그래밍/Effective C++] - Chapter 5 구현


C++의 객체 지향 프로그래밍(object-oriented programming: OOP)는 익히 알고있는 OOP보다 조금 더 생각할 부분이 많다.


항목 32: public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따로도록 만들자


C++로 객체 지향 프로그래밍을 하면서 다른 건 잊더라도 꼭 잊지 말아야 하는 규칙이 public 상속은 "is-a(...는 ...의 일종이다)"를 의미한다는 이야기다.


최고의 설계는, 제작하려는 소프트웨어 시스템이 기대하는 바에 따라 달라지는 것이다.


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


이것만은 잊지 말자!

- public 상속의 의미는 "is-a(...는 ...의 일종)"이다. 기본 클래스에 적용되는 모든 것들이 파생 클래스에 그대로 적용되어야 한다. 왜냐하면 모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문이다.



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


상속이란 이름을 달고 이번 항목을 시작했지만, 사실 상속과는 별 관계가 없다. 관계가 있는 것은 유효범위(scope)이다.


어떤 기본 클래스로부터 상속을 받으려고 하는데, 오버로드된 함수가 그 클래스에 들어 있고 이 함수들 중 몇 개만 재정의(다른 말로 오버라이드)하고 싶다면, 각 이름에 대해 using 선언을 붙여 주어야 한다.


이것만은 잊지 말자!

- 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서는 이런 이름가림 현상은 바람직하지 않다.

- 가려진 이름을 다시 볼 수 있게 하는 방법으로, using 선언 혹은 전달 함수를 쓸 수 있다.



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


(public) 상속이라는 개념은 언뜻 보기에는 그다지 복잡하지 않은 것 같지만, 좀 더 자세히 들여다보면 사실 두 가지로 나뉜다. 하나는 함수 인터페이스의 상속이고, 또 하나는 함수 구현의 상속이다.


virtual void draw() const = 0;

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


virtual void error(const std::string& msg);

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


int objectID() const;

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


판단에 따라 인터페이스만 상속시켜도 되고, 인터페이스와 기본 구현을 함께 상속시킬 수도 있으며, 아니면 인터페이스와 필수 구현을 상속시킬 수 있는 것이다.


클래스에서 가장 흔히 발견되는 결정적인 실수 두 가지

첫 번째 실수는 모든 멤버 함수를 비가상 함수로 선언하는 것이다. 두 번째 실수는 모든 멤버 함수를 가상 함수로 선언하는 것이다.


이것만은 잊지 말자!

- 인터페이스 상속은 구현 상속과 다르다. public 상속에서, 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받는다.

- 순수 가상 함수는 인터페이스 상속만을 허용한다.

- 단순(비순수) 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다.

- 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정한다.



항목 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자


비가상 인터페이스 관용구를 통한 템플릿 메서드 패턴

사용자로 하여금 public 비가상 멤버 함수를 통해 private 가상 함수를 간접적으로 호출하게 만드는 방법; 비가상 함수 인터페이스(non-virtual interface: NVI) 관용구

공개되지 않은 가상 함수를 비가상 public 멤버 함수로 감싸서 호출하는, 템플릿 메서드 패턴의 한 형태이다.


함수 포인터로 구현한 전략 패턴

가상 함수를 함수 포인터 데이터 멤버로 대체한다: 군더더기 없이 전략 패턴의 핵심만을 보여주는 형태


tr1::function으로 구현한 전략 패턴

가상 함수를 tr1::function 데이터 멤버로 대체하여, 호환되는 시그너처를 가진 함수호출성 개체를 사용할 수 있똘고 만든다: 역시 전략 패턴의 한 형태이다.


"고전적인" 전략 패턴

한쪽 클래스 계통에 속해 있는 가상 함수를 다른 쪽 계통에 속해 있는 가상 함수로 대체한다: 전략 패턴의 전통적인 구현 형태이다.


이것만은 잊지 말자!

- 가상 함수 대신에 쓸 수 있는 다른 방법으로 NVI 관용구 및 전략 패턴을 들 수 있따. 이 중 NVI 관용구는 그 자체가 템플릿 메서드 패턴의 한 예이다.

- 객체에 필요한 기능을 멤버 함수로부터 클래스 외부의 비멤버 함수로 옮기면, 그 비멤버함수는 그 클래스의 public 멤버가 아닌 것들을 접근할 수 없다는 단점이 생긴다.

- tr1::function 객체는 일반화되 함수 포인터처럼 동작한다. 이 객체는 주어진 대산 시그너처와 호환되는 모든 함수호출성 개체를 지원한다.



항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!


이것만은 잊지 말자!

- 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자!



항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자


객체의 동적 타입(dynamic type)은 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입이다. 


가상 함수의 매개변수 값들 중 하나가 파생 클래스에서 재정의되면 여지없이 문제가 생긴다.


이것만은 잊지 말자!

- 상속받은 기본 매개변수 값은 절대로 재정의해서는 안 된다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수(오버라이드할 수 있는 유일한 함수)는 동적으로 바인딩 되기 때문이다.



항목 38: "has-a(...는...를 가짐)" 혹은 "is-implemented-in-terms-of(...는...를 써서 구현됨)"를 모형화할 때는 객체 함성을 사용하자


합성(composition)이란, 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 말한다. 포함된 객체들을 모아서 이들을 포함한 다른 객체를 함성한다.


개발자들 사이에선 '합성' 대신에 다른 용어들도 많이 쓰인다. 이를테면 레이어링(layering), 포함(containment), 통합(aggregation) 혹은 내장(embedding) 등으로 알려져 있다.


객체 중에는 우리 일상생활에서 볼 수 있는 사물을 본 뜬 것들이 있다. 사람, 이동수단, 비디오 프레임 등인데, 이런 객체는 소프트웨어의 응용 영역(application domain)에 속한다. 응용 영역에 속하지 않는 나머지들은 버퍼, 뮤텍스, 탐색 트리 등 순수하게 시스템 구현만을 위한 인공물이다. 이런 종류의 객체가 속한 부분은 소프트웨어 구현 영역(implementation domain)이라고 한다. 여기서 객체 합성이 응용 영역의 객체들 사이에서 일어나면 has-a 관계이다. 반면, 구현 영역에서 일어나면 그 객체 합성의 의미는 is-implemented-in-terms-of 관계를 나타내는 것이다.


상대적으로 오락가락하는 부분이 바로 is-a 관계와 is-implremented-in-terms-of 관계의 차이점이다.


이것만은 잊지 말자!

- 객체 합성(composition)의 의미는 public 상속이 가진 의미와 완전히 다르다.

- 응용 영역에서 객체 합성의 의미는 has-a(...는 ...를 가짐)이다. 구현 영역에서는 is-implemented-in-terms-of(...는 ...를 써서 구현됨)의 의미를 갖는다.



항목 39: private 상속은 심사숙고해서 구사하자


C++는 public 상속을 is-a 관계로 나타낸다. private 상속의 의미는 is-implemented-in-terms-of 입니다. B 클래스로 부터 private 상속을 통해 D 클래스를 파생시킨 것은, B 클래스에서 쓸 수 있는 기능들 몇 개를 활용할 목적으로 한 행동이지, B 타입과 D 타입의 객체 사이에 어떤 개념적 관계가 있어서 한 행동이 아니라는 것이다. private 상속은 소프트웨어 설계(design) 도중에는 아무런 의미도 갖지 않으며, 단지 소프트웨어 구현(implementation) 중에만 의미를 가질 뿐이다.


'하나의 설계 문제에 대한 접근 방법이 꼭 하나만 있는 것은 아니다', '여러 가지 방법을 실제로 고민하는 습관을 들이는 것이 좋다'


공간 절약 기법은 공백 기본 클래스 최적화(empty base optimization: EBO)라고 알려져 있으며, 알아두는 게 좋다.


"private 상속을 심사숙고해서 구사하자"라는 말의 의미는, 섣불리 이것을 쓸 필요가 없다는 생각을 갖고 모든 대안을 고민한 연후에, 주어진 상황에서 두 클래스 사이의 관계를 나타낼 가장 좋은 방법이 private 상속이라는 결론이 나면 써라.


이것만은 잊지 말자!

- private 상속의 의미는 is-implemented-in-terms-of(...는 ...를 써서 구현됨)이다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있다.상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자!

- 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 한다.


항목 40: 다중 상속은 심사숙고해서 사용하자


C++에서 다중 상속(multiple inheritance: MI)에 관한 견해는 두 가지의 진영으로 갈린다. 단일 상속(Single inheritance: SI)이 좋다면 다중 상속은 더 좋을 것이 분명하다는 입장과 단일 상속은 좋지만 다중 상속은 골칫거리밖에 안 된다는 주장이다. 두 가지 견해가 어떤 이야기인지부터 제대로 이해하는 것이 중요하다.


다중 상속의 의미는 그냥 '둘 이상의 클래스로부터 상속을 받는 것'일 뿐이지만, 이 MI는 상위 단계의 기본 클래스를 여러 개 갖는 클래스 계통에서 심심치 않게 눈에 띈다.


가상 상속을 사용하는 클래스로 만들어진 객체는 가상 상속을 쓰지 않은 것보다 일반적으로 크기가 더 크다. 게다가, 기본 클래스의 데이터 멤버에 접근하는 속도도 비가상 기본 클래스의 데이터 멤버에 접근하는 속도보다 느립니다. 가상 상속은 비싸다.


가상 기본 클래스(가상 상속)은 첫때, 구태여 쓸 필요가 없으면 가상 기본 클래스를 사용하지 말자. 비가상 상속을 기본으로 삼아라. 둘째, 가상 기본 클래스를 정말 쓰지 않으면 안 될 상황이라면, 가상 기본 클래스에는 데이터를 넣지 않는 쪽으로 최대한 신경쓰자. 데이터만 들어가지 않으면 가상 기본 클래스의 초기화 규칙만 생각하면 된다. 참고로, C++의 가상 기본 클래스와 여러가지 측면에서 비교가 되는 것이 자바와 닷넷의 Interface라는 개념인데, 자바와 닷넷의 인터페이스는 언어적으로 데이터를 아예 갖지 못하도록 정해져있다.


다중 상속은 대단한 게 아니다. 그냥 객체 지향 기법으로 소프트웨어를 개발하는 데 쓰이는 도구 중 하나로 보면 된다. 단일 상속과 비교해서 사용하기에도 좀 더 복잡하고 이해하기에도 좀 더 복잡하다는 것은 사실이다. MI 설계와 동등한 효과를 내는 SI 설계를 뽑을 수 있다면 SI 쪽으로 가는 것이 확실히 좋다. 다중 상속을 쓸 때 혹시라도 성급하지는 않았나, 확인하고 또 확인하라.


이것만은 잊지 말자!

- 다중 상속은 단일 상속보다 확실히 복잡하다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해 질 수도 있다.

- 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커진다. 따라서 가상 기본 클래스에는 데이터를 두지 않는 것이 현실적으로 가장 실용적이다.

- 다중 상속을 적법하게 쓸 수 있는 경우가 있다. 여러 시나리오 중 하나는 인터페이스 클래스로 부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것이다.

728x90

'프로그래밍 > Effective C++' 카테고리의 다른 글

Chapter 7 템플릿과 일반화 프로그래밍  (0) 2018.07.11
Chapter 5 구현  (0) 2018.05.23
Chapter 4 설계 및 선언  (0) 2018.04.17
Chapter 3 자원 관리  (0) 2018.04.11
Chapter 2 생성자, 소멸자 및 대입 연산자  (0) 2018.03.28

댓글