19: 클래스 설계는 타입 설계와 똑같이 취급하자
새로운 클래스를 정의한다는 것은 새로운 타입을 하나 정의하는 것과 같다. 우리는 클래스 설계자로 그치지 않으며 타입 설계자라는 막강한 권위를 가지고 있다. 함수와 연산자를 오버로드하고, 메모리 할당 및 해제를 제어하며, 객체 초기화 및 종료처리를 정의하는 작업, 메모리 할당 및 해제를 제어하며, 객체 초기화 및 종료처리를 정의하는 작업, 이 모두가 우리의 손에 달려 있다.
좋은 타입은 일단 문법(syntax)가 자연스럽고, 의미구조(semantics)가 직관적이며, 효율적인 구현이 한 가지 이상 가능해야 한다. 효과적인 클래스를 설계할 때 고려사항은 무엇일까?
- 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
- 이 부분이 어떻게 되느냐에 따라 클래스 생성자 및 소멸자의 설계가 바뀐다. 그뿐 아니라 메모리 할당 함수(operator new, operator new[], operator delete, operator delete[])를 직접 작성할 경우에는 이들 함수의 설계에도 영향을 미친다.
- 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
- 생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정짓는 요소이다.
- 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
- '값에 의한 전달'을 구현하는 쪽은 복사 생성자이다.
- 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
- 전부는 아니지만, 클래스의 데이터 멤버의 몇 가지 조합 값만은 반드시 유효해야 한다. 이런 조합을 가리켜 클래스의 불변속성(invariant)라고 하며, 클래스 차원에서 지켜주어야 하는 부분이다. 이에 따라 에러 점검 루틴이 좌우된다.
- 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가?
- 이미 갖고 있는 클래스로부터 상속을 시킨다고 하면, 당연히 여러분의 설계는 이들 클래스에 의해 제약을 받게 된다. 특히 멤버 함수가 가상인가 비가상인가의 여부가 가장 큰 요인이다. 만든 클래스를 다른 클래스들이 상속할 수 있게 만들자고 결정했다면, 이에 따라 멤버 함수의 가상 함수 여부가 결정된다. 특히 소멸자가 그렇다.
- 어떤 종류의 타입 변환을 허용할 것인가?
- 우리가 만든 타입은 결국 기존의 수많은 타입과 어울려야 하는 운명이다. T1 타입의 객체를 T2 타입의 객체로 암시적으로(implicitly) 변환되도록 만들고 싶다면, T1 클래스에 타입 변환 함수를 하나 넣어두든가(이를테면 operator T2) 아니면 인자 한 개로 호출될 수 있는 비명시호출(non-explicit) 생성자를 T2 클래스에 넣어두어야 할 것이다. 명시적(explicit) 타입 변환만 허용하고 싶을 경우에는, 해당 변환을 맡는 별도 이름의 함수를 만들되 타입 변환 연산자 혹은 (인자 하나로 호출될 수 있는) 비명시호출 생성자는 만들지 말아야 할 것이다.
- 어떤 연산자와 함수를 두어야 의미가 있을까?
- 클래스 안에 선언할 함수가 바로 여기서 결정된다. 어떤 것들은 멤버 함수로 적당할 것이고, 또 몇몇은 그렇지 않을 것이다.
- 표준 함수들 중 어떤 것을 허용하지 말 것인가?
- private로 선언해야 하는 함수가 여기에 해당된다.
- 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
- 어떤 클래스 멤버를 public, protected, private 영역에 둘 것인가를 결정한다. 또한 프렌드로 만들어야 할 클래스 및 함수를 정하는 것은 물론이고 한 클래스를 다른 클래스에 중첩시켜도 되는가에 대한 결정을 내리는 데도 도움이 된다.
- '선언되지 않은 인터페이스'로 무엇을 둘 것인가?
- 타입이 제공할 보장이 어떤 종류일까에 대한 질문이다. 보장할 수 있는 부분은 수행 성능 및 예외 안정성 그리고 자원 사용이다.
- 새로 만드는 타입이 얼마나 일반적 인가?
- 정의하는 것이 동일 계열의 타입군(family of types) 전체일지도 모른다. 진짜 그렇다면 우리가 원하는 것은 '새로운' 클래스가 아니라 '새로운' 클래스 템플릿을 정의해야 할 것이다.
- 정말로 꼭 필요한 타입인가?
- 기존의 클래스에 대해 기능 몇 개가 아쉬워서 파생 클래스를 뽑고 있다면, 차라리 간단하게 비멤버 함수라든지 템플릿을 몇 개 더 정의하는 편이 낫다.
이것만은 잊지 말자!
- 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 21: 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (1) | 2023.10.18 |
---|---|
[Effective C++] 20: '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2023.10.17 |
[Effective C++] 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2023.10.15 |
[Effective C++] 17: new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2023.10.14 |
[Effective C++] 16: new 및 delete를 사용할 때는 형태를 반드시 갖추자 (1) | 2023.10.13 |