관리 메뉴

기억을 위한 기록들

[CPP-effective] 4-2 클래스 설계는 타입 설계와 똑같이 취급하자. 본문

C & CPP/Effective C++

[CPP-effective] 4-2 클래스 설계는 타입 설계와 똑같이 취급하자.

에드윈H 2021. 4. 12. 10:32

C++에서 새로운 클래스를 정의한다는 것은 새로운 타입 하나를 정의하는 것과 같다.

 

단순히 클래스 설계를 하는것이 아니라 타입 설계를 한다고 생각하니 더 대단한 것 같다(?).

함수와 연산자를 오버로드하고, 메모리 할당 및 해제를 제어하며, 객체 초기화 및 종료 처리를 정의하는 작업 등 신경 써야 할게 많아지기도 한다는 것이다.

 

좋은 클래스를 설계한다는 것은 좋은 타입을 설계하는 것이기도 한데, 마냥 쉽기만 하지는 않을 것이다.

 

1. 문법(syntax)이 자연스럽고,

2. 의미구조(sematics)가 직관적이며,

3. 효율적인 구현이 한 가지 이상 해야 하는데,

 

고민 없이 클래스 정의를 했다가는 이 중 한가지도 못할 수도 있다. 심지어 멤버 함수조차도 어떻게 선언되었느냐에 따라 수행성능이 달라진다.

 

고려사항이 무엇인지 먼저 확인해보면,

 

1.  새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?

이 부분이 어떻게 되느냐에 따라 생성자 및 소멸자의 설계가 달라진다. 그뿐 아니라 메모리 할당 함수(operator new, operator delete 등)를 직접 작성할 경우에도 이들 함수의 설계에도 영향을 미친다.

 

2.  객체 초기화는 객체 대입과 어떻게 달라야 하는가?

생성자와 대입 연산자의 동작 및 두개의 차이점을 결정짓는 요소이다. 초기화(initialization)와 대입(assignment)을 헷갈리지 않는 것이 중요한데, 각각에 해당되는 함수 호출이 아예 다르기 때문이다.

int x = 1; //초기화
int y;
y = x ; //대입

 

3. 새로운 타입으로 만든 객체가 '값에 의해 전달(Call By Value)'되는 경우에 어떤 의미를 줄 것인가?

어떤 타입에 대해 '값에 의한 전달'을 구현하는 쪽은 복사 생성자이다.

 

4. 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?

전부는 아니지만, 클래스의 데이터 멤버의 몇 가지 조합값만은 반드시 유효해야 한다.

이런 조합을 가리켜 클래스의 불변속성(invariant)이라고 하며, 클래스 차원에서 지켜주어야 하는 부분이다.

 

이 불변속성에 따라 클래스 멤버 함수 안에서 해주어야 할 에러 점검 루틴이 좌우되는데, 특히 생성자, 대입 연산자, 각종 쓰기(setter) 함수는 불변 속성에 많이 좌우된다. 뿐만 아니라 함수가 발생시키는 예외에도 영향을 미치며, 혹시나 예외 지정자(exception specification)를 쓴다면 그 부분에도 영향을 준다.

 

5. 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가?

이미 갖고 있는 클래스로부터 상속을 시킨다고 하면, 새로운 클래스의 설계는 이들 클래스에 제약을 받게 된다.

특히 멤버 함수가 가상인가 비가상인가의 여부가 가장 큰 요인이다.

만든 클래스가 다른 클래스들이 상속할 수 있게 만들게 될때도, 이에 따라 멤버 함수의 가상 함수 여부가 결정된다. 

특히 소멸자가 그렇다.

 

6. 어떤 종류의 타입 변환을 허용할 것인가?

만든 타입(클래스)가 기존의 수많은 타입들과 어울려야 할 수도 있다. 만든 타입이 다른 타입 사이에 변환 수단이 있어야 할지,

 

예를 들어, P1이라는 타입의 객체를 P2라는 타입의 객체로 암시적(implicitly)으로 변환되도록 만들고 싶다면, P1 클래스에 타입 변환 함수를 하나 넣어놓든가, 아니면 인자 한개로 호출될 수 있는 비명시 호출 생성자(non-explicit)를 P2클래스에 넣어두어야 한다.

 

명시적(explicit) 타입 변환만 허용하고 싶으면, 해당 변환을 맡는 별도 이름의 함수를 만들되 타입 변환 연산자 혹은(인자

하나로 호출될 수 있는) 비 명시 호출 생성자는 만들지 말아야 한다.

 

7. 어떤 연산자와 함수를 두어야 의미가 있을까?

클래스 안에 선언할 함수가 여기서 결정된다. 어떤 것들을 멤버 함수로 적당할 것이고, 몇몇은 그렇지 않을 것이다.

 

8. 표준 함수들 중 어떤 것을 허용하지 말 것인가?

private로 선언해야 하는 함수가 해당된다. (컴파일러가 자동으로 만드는 함수들 중 고려)

참고 : hyo-ue4study.tistory.com/288

 

9. 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?

클래스 멤버를 public, private, protected 영역 중 어디에 둘 것인가를 결정해야 하며, 또한 프렌드로 만들어야 할 클래스 및 함수를 정해야 하며, 한 클래스를 다른 클래스에 중첩시켜도 되는가에 대한 결정을 내리는 데도 이 질문이 도와줄 것이다.

 

10. '선언되지 않은 인터페이스'로 무엇을 둘 것인가?

만들 타입이 제공할 보장이 어떤 종류일까에 대한 질문으로, 보장할 수 있는 부분은 수행 성능 및 예외 안전성 그리고 자원 사용(잠금 및 동적 메모리 등)이다. 이들에 대해 보장하겠다고 결정한 결과는 클래스 구현에 있어서 제약으로 작용하게 된다.

 

11. 새로 만드는 타입이 얼마나 일반적인가?

실상은 타입 하나를 정의하는 것이 아닐지도 모른다. 정의하는 것이  동일 계열의 타입군(family of types) 전체일지도 모른다.  진짜 그렇다면 원하는 것은 '새로운' 클래스가 아니다. '새로운' 클래스 템플릿을 정의해야 할 것이다.

 

12. 정말로 꼭 필요한 타입인가?

기존의 클래스에 대해 기능 몇 개가 아쉬워서 파생 클래스를 새로 뽑고 있다면, 차라리 간단하게 비멤버 함수라든지, 템플릿을 몇 개 더 정의하는 편이 낫다.

 

 

어느 것 하나 만만하게 볼 수 없는 질문들이다. 효과적인 클래스를 정의한다는 일이 무척이나 어렵기도 하다. 하지만 이런 역경을 뚫고 멋지게 설계된 사용자 정의 클래스는 최소한 기본 제공 타입 정도의 든든함을 가진 타입, 설계에 들인 노력만큼의 가치를 지닌 타입으로 쓸 것이다.

 

정리

* 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 고려사항을 빠짐없이 점검해보자.