6장: 클래스 다루기
6장: 클래스 다루기
- 클래스는 연관성이 높고 잘 정의된 기능을 공유하는 데이터와 루틴의 모음이다. 공통 데이터가 없더라도 관련 서비스를 제공하는 루틴의 집합이 될 수 있다.
6.1 클래스의 토대: 추상 데이터형
- 추상 데이터형(Abstract Data Type. ADT)은 데이터와 데이터를 처리하는 연산의 집합이다.
- 연산은 프로그램의 나머지 부분에 데이터가 무엇인지 설명해주는 역할과 나머지 프로그램에서 그 데이터를 변경할 수 있게 해주는 역할임.
- 객체지향 프로그래밍을 이해하기 위해서는 ADT를 반드시 이해해야함.
ADT가 필요한 예
폰트 루틴과 데이터 집합
currentFont.size = 16
currnetFont.sizeInPixels = PointsToPixels( 12 )
- ADT를 사용할 때 좋은 점
- 구현 세부 사항을 감출 수 있다. 영향없이 한 곳에서 변경 가능
- 변경이 전체에 영향을 미치지 않는다.
- 인터페이스가 더 많은 정보를 제공하도록 만들 수 있다.
- 성능을 향상시키기 쉽다.
- 프로그램이 명백하게 정확해진다.
- 프로그램의 가독성이 높아진다.
- 전체프로그램에 데이터를 넘길 필요가 없다.
- 저수준 구조체 대신 현실 세계의 개체를 다룰 수 있다.
- 몇 가지 원칙
- 전형적인 저수준 데이터형을 저수준 데이터형이 아닌 ADT로 만들거나 사용하라.
- 파일과 같은 일반적인 객체를 ADT로 취급하라
- 간단한 객체도 ADT로 취급하라
- ADT가 저장된 매체와 독립적으로 ADT를 참조하라
비객체지향 프로그래밍 환경에서 ADT로 여러개의 데이터 인스턴스 다루기 방법 1: ADT 서비스를 사용할 때 마다 명시적으로 인스턴스 식별 방법 2: ADT 서비스에서 사용되는 데이터를 명시적으로 제공 방법 3: 암시적인 인스턴스 사용
추상 데이터형 + 상속 + 다형성 = 클래스
6.2 좋은 클래스 인터페이스
좋은 추상화
class Employee {
public:
// 공개 생성자와 소멸자
Employee();
Employee(
FullName name,
String address,
String workPhone,
String homePhone,
TaxId taxIdnumber,
JobClassification jobClass
);
virtual ~Employee();
// 공개 루틴
FullName GetName() const;
String GetAddress() const;
String GetWorkPhone() const;
String GetHomePhone() const;
TaxId GetTaxIdNumber() const;
JobClassification GetJobClassification() const;
...
private:
...
};
// 잘못된 추상화 예제
class Program {
public:
...
// 공개 루틴
void InitializeCommonStack();
void PushCommand( Command command );
Command PopCommand();
void ShutdownCommandStack();
void InitializeReportFormatting();
void FormatReport( Report report );
void PrintReport( Report report );
void InitializeGlobalData();
void ShutdownGlobalData();
...
private:
...
};
// 개선된 추상화 예제
class Program {
public:
...
// 공개 루틴
void InitializeUserInterface();
void ShutdownUserInterface( Report report );
void InitializeReports();
void ShutdownReports();
...
private:
...
};
- 클래스 인터페이스가 일관된 추상화 수준을 갖도록 한다.
// 추상화 수준이 뒤섞인 예제 class EmployeeCensus: public ListContainer { public: ... // 공개 루틴 void AddEmployee( Employee employee ); void RemoveEmployee( Employee employee); Employee NextItemInList(); Employee FirstItem(); Employee LastItem(); ... private: ... };
// 추상화 수준이 일관성 있는 예제
class EmployeeCensus: public ListContainer {
public:
...
// 공개 루틴
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee);
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
...
private:
ListContainer m_EmployeeList;
...
};
- 클래스가 구현하고 있는 추상화가 무엇인지 이해해야한다.
- 서로 반대되는 기능을 갖는 서비스 쌍(pair)를 제공하라.: 대부분 연산은 유사하거나 같거나 정반대의 연산이 있다. 꼭 필요한지 확인하자
- 관련이 없는 정보를 다른 클래스로 옮겨라. : 다른 일을 하면 클래스 나누기
- 가능하면 인터페이스를 의미론적이기보다는 프로그래밍적으로 만들어라. 의미론적 인터페이스는 주석으로 설명. Assert나 다른 프로그래밍 기법을 모색해보자
- 코드 변경시 인터페이스의 추상화가 망가지지 않도록 주의한다.
- 인터페이스 추상화에 맞지 않는 공개 멤버를 추가하지 말라.: 이 루틴이 기존 추상화와 일관성이 있는가? 질문하기
- 추상화와 응집도를 함께 고려하라.: 강한 응집도는 대개 조흥 추상화를 제공한다. 응집력이 약하다면 추상화 관점에서 일관성을 지키려고 노력
좋은 캡슐화
- 클래스와 멤버의 접근성을 최소화하라: 일반적으로 숨기는 것이 좋다.
- 멤버 데이터를 public으로 노출하지 마라: 캡슐화 위반 Getter, Setter 사용
- 내부 구현 세부사항을 클래스의 인터페이스에 입력하지 마라: 진정한 캡슐화는 구현 세부사항을 볼 수 없다.
class Employee { public: // 공개 생성자와 소멸자 Employee(); Employee( FullName name, String address, String workPhone, String homePhone, TaxId taxIdnumber, JobClassification jobClass ); virtual ~Employee(); // 공개 루틴 FullName GetName() const; String GetAddress() const; ... private: String m_Name; String m_Address; int m_jobClass; };class Employee { public: // 공개 생성자와 소멸자 Employee(); Employee( ... ); // 공개 루틴 ... FullName GetName() const; String GetAddress() const; ... private: EmployeeImplementation *m_implementation; }; - 클래스의 사용자를 가정하지 말라: 클래스 인터페이스 계약대로
- 프렌드 클래스를 피하라: 서로 다른 클래스가 비공개 함수에 접근할 수 있도록 friend 키워드로 선언한 클래스. 캡슐화 위반
- 어떤 루틴이 공개 루틴만 사용한다고 해서 public 인터페이스에 두지 마라: 추상화와 일관성있는지를 고려하기
- 코드를 작성할 때의 편의성보다 가독성이 높은 코드를 작성하라
- 캡슐화의 의미론적인 위반을 각별히 주의하라: 인터페이스문서만으로 클래스를 어떻게 사용해야하는지 있도록 지속적인 개선이 필요.
- 지나치게 밀접한 결합을 주의하라: 일반적으로 결합은 느슨할 수록 좋다.
- 클래스와 멤버의 접근성을 최소화
- 프렌드클래스는 너무 밀접하게 결합되기 때문에 피하라
- 파생 클래스와 기본 클래스가 느슨하게 연결되도록 기본클래스의 데이터를 protected가 아니라 private로 선언하라
- 클래스의 공개 인터페이스에 멤버 데이터를 노출하지마라
- 의미론적 캡슐화를 유지하라
- “데미테르 법칙”을 준수하라
6.3 설계와 구현 문제
- 설계는 비결정적이므로, 효과적인 발견법을 능숙히 적용하는 것이 좋은 설계의 핵심 활동임
- 아래 발견법은 모두 복잡성 관리라는 한 가지 목적으로 수렴함
포함(“has a” 관계)
- 포함을 통해서 “갖다”를 구현하라 포함은 갖는 관계
- 최후의 수단으로 비공개 상속을 통해서 “has a”를 구현하라: 한 객체를 다른 객체의 멤버로 선언하는 것만으로 포함을 구현할 수 없을 때
- 약 7개 이상의 데이터 멤버를 포함하는 클래스를 주의하라: 5 - 9
상속(“is a” 관계)
- 공개 상속을 통해 “이다(is a)”관계를 구현하라: 상속을 통해 새로운 클래스를 작성한다면 새로운 클래스는 기존 클래스의 특수한 버전 “이다”
- 상속을 고려하여 설계하고 문서화하라. 그게 아니면 상속을 금지하라: 상속은 프로그램을 복잡하게만들기 때문에 설계가 필요..?
- 리스코프 치환 원칙을 따르라: 파생클래스가 기본 클래스의 특수한 버전이 아니라면 상속받아서는 안된다. “서브클래스는 사용자가 그 차이점을 모른채 기본 클래스의 인터페이스를 통해서 사용할 수 있어야한다”
- 상속받고 싶을 때만 상속받게 하라: 오버라이드 가능한 루틴, 오버라이드 불가능한 루틴, 오버라이드 가능한 추상 루틴 3 종류의 상속을 필요할때 사용
- 오버라이드가 불가능한 멤버 함수를 “오버라이드”하지 말라: 프로그래밍 언어가 오버라이드를 지원하더라도 혼란을 초래할 수 있으니 사용금지 “ 오버라이드가 불가능한 기본 클래스 루틴의 이름을 파생 클래스에서 재사용하지 마라”
- 공통으로 사용되는 인터페이스와 데이터, 행위를 상속 단게에서 가능한 한 가장 높은 곳까지 옮겨라
- 인스턴스가 하나뿐인 클래스를 의심하라: 싱글턴 패턴 제외. 새로운 클래스 대신 객체를 생성할 수 있는지, 파생 클래스가 아니라 데이터로 표현 가능한지
- 파생 클래스가 하나뿐인 기본 클래스를 의심하라: 직관적이고 단순하기 만들기
- 루틴을 오버라이드했는데 파생된 루틴 내부에서는 아무것도 하지 않는 클래스들을 의심하라: 가정을 재검토해야할 수도 있음.
- 깊은 상속구조를 피하라: 복잡성이 증가할 수 있음. 중복된 코드를 피하고 복잡성을 최소화하는지 확인하기
- 광범위한 타입 검사보다 다형성을 택하라:
// 다형성으로 대체해야하는 예제 switch ( shape.type ) { case Shape_Circle: shape.DrawCircle(); break; case Shape_Square: shape.DarwSquare(); break; ... }// 다형성으로 대체해선 안되는 예제 switch ( ui.Command() ) { case Command_OpenFile: OpenFile; break; case Command_Print: Print(); break; ... } - 모든 데이터를 보호가 아닌 비공개로 바꿔라: 상속은 캡슐화를 망가뜨리기 때문에 데이터가 필요하다면 protected로 함수를 통해 제공.
다중 상속
상속에 관한 규칙이 많은 이유 복잡성 관리가 어렵다. 가능한 멀리해야 함.
- 다중 클래스가 공통 데이터는 공유하지만 행위를 공유하지 않는다면 그 클래스가 포함할 수 있는 공통 객체를 만든다.
- 다중 클래스가 공통 행위를 공유하지만 데이터를 공유하지 않는다면 공통적인 루틴을 정의한 기본 공통 클래스를 정의한다.
- 다중 클래스가 공통 데이터와 행위를 공유한다면 공통적인 데이터와 루틴을 정의한 기본 공통 클래스를 정의한다.
- 인터페이스를 제어하기 위한 기본 클래스가 필요할 때는 상속을 하고 인터페이스를 제어하고 싶다면 포함한다.
멤버 함수와 데이터
- 클래스에 가능한 한 적은 수의 루틴을 유지하라: 루틴 수와 오류율 상승은 상관관계에 있다. 다른 요소가 미치는 영향이 더 크기 때문에 최소화와 다른 요인과 중요도를 비교해야함
- 원하지 않는 멤버 함수와 연산자가 암묵적으로 생성되지 않도록 하라: 생성자나 할당 연산자 등을 비공개로 선언하여 클라이언트 접근을 막음. 예) 싱글턴
- 클래스에서 호출되는 루틴의 수를 최소화하라: “팬 아웃” 클래스에서 사용하는 클래스가 많을 수록 오류도 증가
- 다른 클래스에 대한 간접적인 루틴 호출을 최소화하라: 직접적인 연결은 위험하지만 간점적인 연결은 더 위험할 수 있다. “데미테르법칙”
- 일반적인 클래스가 다른 클래스와 협력하는 정도를 최소화하라: 인스턴스로 만드는 객체 수, 직접적인 루틴 호출 수, 다른 객체가 반환하는 개체에 대한 루틴 호출 수
생성자
- 가능하다면 모든 멤버 데이터를 모든 생성자에서 초기화하라 방어적 프로그래밍 습관
- 비공개 생성자를 사용해 싱글턴 속성을 구현하라: 하나의 객체만 인스턴스로 만들도록 제한하기
// 비공개 생성자로 싱글턴을 구현한 자바 예제 public class MaxId { // 생성자와 소멸자 private MaxId() { ... } ... // 공개 루틴 public static MaxId GetInstance() { return m_instance; } ... // 비공개 멤버 private static final MaxId m_instance = new MaxId() } - 다른 사실이 증명될 때가지 앝은 복사보다 깊은 복사를 택하라: 얕은 복사는 주로 성능향상을 위해 하지만 성능면에 서 문제가 있는 경우는 거의 없다. 깊은 복사가 더 유지보수하기 편하다.
6.4 클래스를 작성하는 이유
- 현실 세계 객체 모방
- 추상 객체 모델링
- 복잡성 줄이기: 하나는 큰 문제를 잘게 나누고 다른 하나는 작은 조각에서 쌓아 올림, 경쟁이 아니라 상호 보완이므로 잘 되는 쪽을 찾을 때까지 오감
- 복잡성 고립: 특정 설계 질문에 답할 최소한의 “버릴 코드”만 작성함, 질문이 구체적이어야 하고 코드를 반드시 버린다는 태도를 지켜야 효과가 있음 — 예: DB 처리량이 걱정이면 Table1·Column1 같은 가짜 테이블에 임시 데이터를 넣어 성능만 측정
- 구현 세부사항 숨기기: 두 머리가 한 머리보다 나음, 오류를 찾는 게 목적이면 공식 검사가, 대안을 많이 만드는 게 목적이면 덜 형식적인 방식이 맞음
- 변경 효과 제한: 팀 경험·시스템 수명·신뢰도·규모에 따라 다름, 판단이 서지 않으면 더 상세히 하는 쪽이 나음 — 큰 설계 오류는 쉽다고 보고 아예 설계하지 않은 영역에서 나오기 때문임
- 전역 데이터 숨기기: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 매개변수 전달을 간소화: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 중앙 집중 관리: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 코드 재사용: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 프로그램 전체 고려하기: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 연관된 기능을 패키지로 구성: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
- 특정한 리팩터링 수행: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음
피해야할 클래스 갓 클래스를 생서하지 말라 관련이 없는 클래스를 제거하라 동사를 뒤에 붙이는 클래스를 피하라
클래스를 작성해야하는 이유 요약
- 현실 세계의 객체를 모델링한다.
- 추상 객체를 모델링한다.
- 복잡성을 줄인다.
- 복잡성을 고립시킨다.
- 구현 세부 사항을 숨긴다.
- 변경의 효과를 제한한다.
- 전역 데이트를 숨긴다.
- 매개변수 전달을 간소화한다.
- 중안 집중 관리한다.
- 코드 재사용을 돕는다.
- 프로그램 전체를 고려한다.
- 연관된 기능을 패키지로 구성한다.
- 특정한 리팩터링을 수행한다.
6.5 프로그래밍 언어와 관련된 이슈
- 프로그래밍 언어마다 클래스에 대한 접근 방법이 매우 다양하다.
- 자바에서는 모든 루틴이 기본적으로 오버라이드가 가능하며 파생 클래스에서 막기 위해서는 루틴을
final선언해야 함 - C++에서는 기본적으로 루틴 오버라이드가 불가능하고 오버라이드 하기위해서는 기본 클래스에서
virtual로 선언해야함 - 비주얼 베이직에서는 기본 클래스에서
overridable로 선언되어 있어야하며 파생클래스는override\
클래스와 관련하여 프로그래밍 언어마다 크게 다른 점
- 상속 트리에서 오버라이드 된 생성자와 소멸자의 작동 방식
- 예외처리 조건에서 생성자와 소멸자의 작동 방식
- 기본 생성자(인자가 없는 생성자)의 중요성
- 소멸자나 종결자(finalizer)가 호출되는 시기
- 할당과 동치 연산자와 같이 프로그래밍 언어에서 기본으로 제공하는 연산자들을 오버라이드하는 방법
- 객체가 생성되고 소멸될 때나 객체가 선언되고 범위를 벗어날 때 처리되는 메모리 처리 방식
6.6 클래스를 넘어서: 패키지
클래스는 현재 모듈화를 달성하기 위한 최고의 방법이다. 그러나 모듈화는 범위가 큰 문제이며 클래스를 넘어선다. 명령문 -> 서브루틴 -> 클래스 패키지를 만들어 다음과 같은 규약을 지킬 수 있음
- 어떤 클래스가 public 이고 어떤 클래스가 패키지 내부에서 사용되기 위한 것인지를 구별하는 이름 규약
- 각 클래스가 어떤 패키지에 속하는지를 식별하는 이름 규약이나 코드 구성 규약(프로젝트 구조), 또는 둘다
- 어떤 패키지가 다른 패키지를 사용할 수 있는지와 상속이나 포함 또는 두가지 방법으로 사용할 수 있는지 적용하는 규칙
체크리스트: 클래스 품질
추상 데이터형
- 프로그램에 있는 글래스를 추상 데이터형으로 생각하고 그러한 관점에서 클래스의 인터페이스를 평가했는가?
추상화
- 클래스에 핵심적인 목적이 있는가?
- 클래스의 이름이 잘 지어졌고 클래스의 이름이 핵심적인 목적을 설명하고 있는가?
- 클래스의 인터페이스가 일관성 있는 추상화를 제공하는가?
- 클래스의 인터페이스가 크랠스의 사용방법을 분명하게 만들고 있는가?
- 클래스의 구현 세부 사항을 전혀 알 필요가 없을 정도로 클래스의 인터페이스가 추상적인가? 클래스를 블랙박스로 취급할 수 있는가?
- 다른 클래스들이 클래스 내부 데이터를 쓸데없이 간섭할 필요가 없을 만큼 클래스의 서비스가 완전한가?
- 관련 없는 정보를 클래스에서 제거했는가?
- 클래스를 컴포넌트 클래스로 분할하는 것에 대해서 생각해 봤는가? 그리고 최대한 분할했는가?
- 클래스를 수정할 때 클래스 인터페이스의 무결성을 유지하고 있는가?
캡슐화
- 클래스가 멤버에 대한 접근성을 최소화하고 있는가?
- 클래스가 멤버 데이터의 노출을 피하고 있는가?
- 클래스가 프로그래밍 언어가 허용하는 만큼 다른 클래스로 구현 세부 사항을 감추고 있는가?
- 클래스가 파생 클래스를 포함해 그 사용자에 대한 가정을 피하고 있는가?
- 클래스가 다른 클래스에 독립적인가? 느슨하게 결합됐는가?
상속 상속이 “is a” 관계를 모델링하기 위해서만 사용됐는가? 다시말해 파생 클래스가 LSP를 따르고 있는가? 클래스의 설명 문서가 상속 전략을 기술하고 있는가? 파생 클래스가 오버라이드 불가능한 루틴에 대해서 “오버라이딩”을 피하고 있는가? 공통적인 인터페이스, 데이터, 행위가 상속 트리에서 최대한 높은 곳에 있는가? 상속 단계가 적절한 수준인가? 기본 클래스에 있는 모든 데이터 멤버가 protected가 아닌 private 인가?
그 밖의 구현 문제 클래스가 7개 이하의 데이터 멤버를 포함하고 있는가? 클래스가 다른 클래스에 대한 직접적이고 간접적인 루틴 호출을 줄였는가? 클래스가 꼭 필요한 범위에서만 다른 클래스와 협동 작업하는가? 모든 멤버 데이터를 생성자에서 초기화했는가? 얕은 복사를 해야 하는 합당한 이유가 없다면 얕은 복사 대신 깊은 복사로 사용되도록 클래스를 설계했는가?
언어에 따른 문제 사용 중인 프로그래밍 언어에서 클래스에 관한 이슈를 조사했는가?
요점정리
- 클래스의 인터페이스는 일관성 있는 추상화를 제공해야 한다. 이 규칙을 어기면 많은 문제가 발생한다.
- 클래스 인터페이스는 시스템 인터페이스나 설계 결정, 구현 세부 사항을 숨겨야 한다.
- “is a”관계를 모델링하고 있지 않다면 상속보다 포함을 선택하는 것이 좋다.
- 상속은 유용한 도구지만, 복잡성을 증가시키며 그로인해 복잡ㄴ성 관리가 어려워진다.
- 클래스는 복잡성을 관리하기 위해서 사용할 수 있는 기본적인 도구다. 복잡성을 관리할 수 있도록 설계에 많은 주의를 기울여랴.