•
클래스는 연관성이 높고 잘 정의된 기능을 공유하는 필드와 메소드의 모음으로 비교적 최근 자리잡은 개념이다.
•
이 장은 고급 클래스를 만드는 데 필요한 핵심적인 조언을 소개한다.
1. 클래스의 토대: 추상 데이터형(Abstract Data Type)
•
추상 데이터형이란 데이터와 데이터를 처리하는 연산의 집합이다.
◦
연산은 프로그램의 나머지 영역들에게 데이터가 무엇인지를 설명해주는 역할과 그 데이터를 변경할 수 있게 해주는 역할을 한다.
•
객체지향 프로그래밍을 이해하기 위해서는 ADT를 반드시 이해해야 한다. 이를 이해하지 않는다면 클래스명만 클래스인 코드를 작성하게 될 것이다.
◦
그런 클래스는 실제로는 연관성이 높지 않은 필드와 메소드를 편의를 위해 보관하는 도구 상자와 다를 게 없다.
•
전통적으로 프로그래밍 책에서는 추상 데이터형을 소개할 때, 수학적으로 설명한다.
◦
추상 데이터형은 연산자들이 모여있는 수학적 모델로 생각할 수 있다와 같이 표현하는 경향이 있다.
•
이는 저수준의 구현 엔티티가 아닌 실제 세계에 존재하는 엔티티를 조작할 수 있게 해준다.
◦
링크드 리스트에 노드를 삽입하는 것과 같이 구체적인 수준이 아닌 표에 행을 추가하는 등 추상화시켜서 문제를 다룰 수 있게 한다는 뜻이다.
ADT를 사용할 때 좋은 점
•
구현 세부 사항을 감출 수 있다.
◦
구현을 숨기게 되면 해당 구현에 대해서 나머지 영역들은 몰라도 된다. 구현이 바뀌게 된다면 해당 범위만 수정해주면 된다.
•
변경이 전체에 영향을 미치지 않는다.
•
인터페이스가 더 많은 정보를 제공하도록 만들 수 있다.
◦
유사한 모든 연산을 하나의 ADT에 모아놓으면 그 자체로 맥락을 제공해준다.
•
성능을 향상시키기 쉽다.
◦
구현 부분만 성능을 향상시키면 된다.
•
프로그램이 명백하게 정확해진다.
◦
실제 세계의 엔티티를 조작하기 때문에 조금 더 명확하고 이해하기 쉽게 코드를 짤 수 있다.
•
프로그램의 가독성이 높아진다.
•
전체 프로그램에 데이터를 넘길 필요가 없다.
◦
속성에 대한 책임을 그에 대한 처리 책임을 가지고 있는 객체가 도맡아 처리하면 된다. 여기저기 해당 속성이 돌아다닐 필요가 없다.
•
저수준 구현 구조체 대힌 현실 세계의 개체를 다룰 수 있다.
ADT 사용 시 원칙
•
전형적인 저수준 데이터형을 저수준 데이터형이 아닌 ADT로 만들거나 사용하라.
◦
결국 뭔가를 표현하기 위해 저수준 데이터형을 사용하므로 그것들을 ADT로 만들었을때, 그것들의 책임을 오롯이 그 객체에게 전담할 수 있다. 다른 처리 메소드를 만들 필요가 없다.
•
파일과 같은 일반적인 객체를 ADT로 취급하라.
◦
파일도 추상화된 영역이다. 구체적으로는 디스크의 어떠한 영역이다.
•
간단한 객체도 ADT로 취급하라.
•
ADT가 저장된 매체와 독립적으로 ADT를 참조하라.
◦
요즘은 데이터베이스를 많이 쓰니 데이터베이스와 독립적으로 비즈니스 로직을 참조하라라고 이해해도 될 것 같다.
•
비객체지향 프로그래밍 환경에서 ADT로 여러 개의 데이터 인스턴스를 다루기
◦
과거의 잔재
•
ADT와 클래스
◦
추상 데이터형은 클래스의 기본 개념의 바탕을 이룬다.
◦
클래스는 일반적으로 상속과 다형성이라는 개념도 지원하기 때문에 추상 데이터형에 상속과 다형성을 더한 것을 클래스로 생각하자.
2. 좋은 인터페이스
•
좋은 클래스를 만들기 위한 가장 좋은 방법은 좋은 인터페이스를 만드는 것에서 시작한다.
⇒ 인터페이스를 꼭 만들 필요는 없다고 생각하기 때문에 좋은 클래스 초안의 특징으로 생각해도 좋을 것 같다.
좋은 추상화
•
추상화는 복잡한 연산을 단순한 형태로 보여주는 능력이다. 인터페이스는 인터페이스 내부에 숨겨져 있는 구현 세부 사항에 대한 추상화를 제공한다.
◦
인터페이스는 서로 같은 도메인에 책임을 가지고 있는 메소드들을 제공해야만 한다.
•
직원을 구현하는 클래스가 있다고 하자. 우리의 도메인의 책임은 직원 정보를 관리하거나 사용하는 책임을 가지고 있다.
◦
필드로는 이름, 주소, 전화번호 등이 존재할 것이다.
◦
메소드로는 각 정보들을 조회하고 초기화하는 기능들이 존재할 것이다.
•
이처럼 메소드들 간의 밀접한 관계를 맺고 있고 유사한 책임을 달성하기 위한 목적을 지향하고 있어야 좋은 클래스가 될 수 있다.
•
클래스 추상화에 대한 평가는 public 메소드의 집합을 기초로 한다. 이는 곧 인터페이스를 의미한다.
좋은 추상화 수준을 가지는 인터페이스를 만드는 데 도움을 주는 원칙들
•
인터페이스가 일관된 추상화 수준을 갖도록 한다.
◦
클래스는 추상 데이터형의 구현체라고 생각하면 이해하기 쉽다.
◦
각 클래스는 오직 하나의 ADT만 상속해야 한다. 다중 상속은 허용되지 않는다. 책임은 하나여야만 한다.
▪
만약 여러 ADT가 하나의 클래스에 있다면 클래스를 분리할 때가 온 것이다.
•
클래스가 구현하고 있는 추상화가 무엇인지 이해해야 한다.
◦
클래스가 가지고 있는 책임이 뭔지 명확하게 정의해야 한다는 원칙같다.
◦
자신이 이 클래스에게 어떤 책임을 부여하려 하는지, 명확하게 알고 있는 상태에서 유사한 추상화들 중 하나를 선택해야 한다.
•
서로 반대되는 기능을 갖는 서비스 쌍을 제공하라.
◦
대부분의 연산은 유사하거나 정반대의 연산을 가지고 있다. 불을 켜는 연산이 있으면 끄는 연산도 있듯이 말이다.
◦
굳이 만들 필요는 없지만 이런 사례들은 참고할만큼 충분히 많으므로 기능을 만들 때 한번 고려하는 것이 좋다.
•
관련이 없는 정보를 다른 클래스로 옮겨라.
•
가능하면 인터페이스를 의미론적이기보다는 프로그래밍적으로 만들어라.
◦
인터페이스는 프로그래밍적인 부분과 의미론적인 부분으로 구성된다.
◦
프로그래밍적인 부분은 인터페이스에서 컴파일러로 강제할 수 있는 속성으로 구성된다.
◦
의미론적인 부분은 인터페이스가 어떻게 사용될 것인지에 대한 가정으로 구성된다. 이 부분은 컴파일러로 강제할 수 없다.
▪
이는 methodA는 methodB보다 먼저 호출되어야 한다와 같은 고려사항을 포함한다.
◦
Assert같은 기법을 사용해서 의미론적인 부분을 프로그래밍적인 인터페이스 요소로 강제화하는 방법을 모색해보자.
•
코드 변경 시 인터페이스의 추상화가 망가지지 않도록 주의한다.
◦
클래스를 변경, 확장하다보면 인터페이스와 애매한 기능을 추가해야하는 상황이 발생한다.
class Employee {
public:
...
// public routines
FullName GetName() const;
Address GetAddress() const;
PhoneNumber GetWorkPhone() const;
...
bool IsJobClassificationValid( JobClassification jobClass );
bool IsZipCodeValid( Address address );
bool IsPhoneNumberValid( PhoneNumber phoneNumber );
SqlQuery GetQueryToCreateNewEmployee() const;
SqlQuery GetQueryToModifyEmployee() const;
SqlQuery GetQueryToRetrieveEmployee() const;
...
private:
...
};
TypeScript
복사
◦
SqlQuery를 반환하는 메소드는 Employee 클래스보다 추상화 수준이 너무 구체적이고 그것들이 이 클래스의 추상화 수준을 망가뜨린다.
•
인터페이스 추상화에 맞지 않는 공개 멤버를 추가하지 말라.
◦
인터페이스에 메소드를 추가할 때마다 이 메소드가 기존 인터페이스가 제공하는 추상화 수준과 연관성, 일관성이 있는가 질문해보도록 한다.
◦
그렇지 않다면 인터페이스에서 당장 손을 떼고 다른 방법을 모색해보도록 하자.
•
추상화와 응집도를 함께 고려하라.
◦
추상화와 응집도는 개념적으로 밀접한 연관이 있다.
◦
좋은 추상화를 제공하는 인터페이스는 일반적으로 강한 응집도를 갖는다. 강한 응집도를 보이는 인터페이스는 무조건이라고 할 순 없지만 일반적으로 좋은 추상화를 갖는다.
◦
인터페이스를 응집력이라는 관점보다 추상화 관점으로 봐야 클래스에 대해서 더 깊게 이해할 수 있다. 클래스의 응집력이 약하다면 추상화의 관점에서 일관성을 갖도록 하면 된다.
좋은 캡슐화
•
캡슐화는 추상화보다 더 강력한 개념이다. 추상화는 구현 세부 사항을 무시할 수 있는 모델을 제공함으로써 프로그램의 복잡도 관리에 도움을 준다.
◦
캡슐화는 세부 사항을 알고 싶어도 알 수 없게 만드는 강력한 방법이다.
•
클래스와 멤버의 접근성을 최소화하라.
◦
접근성의 최소화는 캡슐화를 장려하기 위해서 고안된 여러 가지 규칙 중 하나다.
◦
확신이 서지 않는다면 일단 숨기는 것이 숨기지 않는 것보다 일반적으로 더 낫다.
•
멤버 데이터를 public으로 노출하지 말라.
◦
멤버 데이터 노출은 캡슐화에 위반되고 추상화를 어렵게 만든다.
▪
클라이언트 코드가 이들을 자유롭게 참조하여 값을 읽거나 변경할 수 있다.
▪
이는 프로그램의 흐름을 해친다.
•
구현 세부 사항을 인터페이스에 입력하지 말라.
◦
진정한 캡슐화는 개발자가 구현 세부 사항을 전혀 볼 수 없다. 말 그대로 모든 것이 캡슐 안에 있다.
•
클래스의 사용자를 특정하지 말라.
◦
클래스는 인터페이스에 정의된 계약대로만 설계하고 구현해야 한다. 그 과정에서 누가 사용할 것인지 어떻게 사용될 것인지와 같은 가정은 해선 안 된다.
•
friend 클래스를 피하라.
◦
상태 패턴과 같은 특정 환경에서는 복잡도를 관리하기 위해 friend 클래스를 사용한다. 하지만 일반적으로 friend 클래스는 캡슐화를 위반한다.
•
어떤 메소드가 public 메소드를 사용한다고 해서 public 인터페이스에 두지 말라.
•
코드를 작성할 때의 편의성보다 가독성이 높은 코드를 작성하라.
•
캡슐화의 의미론적인 위반을 각별히 주의하라.
◦
다음 예는 클래스의 클라이언트가 의미론적인 캡슐화를 망가뜨리는 몇 가지 방법이다.
▪
클래스 A의 PerformFirstOperation() 루틴이 InitializeOperations() 루틴을 자동으로 호출하는 것을 알고 있기때문에 클래스 A의 InitializeOperations() 루틴을 호출하지 않는다.
▪
employee.Retrieve() 함수가 데이터베이스에 연결되어 있지 않을 때 데이터베이스에 연결한다는 것을 알고 있기 때문에 employee.Retrieve(database) 함수를 호출하기 전에 database.Connect() 루틴을 호출하지 않는다.
▪
클래스 A의 PerformFirstOperation() 루틴이 이미 호출되었다는 것을 알고 있기 때문에 클래스 A의 Terminate()루틴을 호출하지 않는다.
▪
ObjectA가 ObjectB를 정적 공간에 보관해서 ObjectB 는 계속 접근할 수 있다는 것을 알고 있기 때문에 ObjectA에 의해서 생성된 ObjectB에 대한 포인터나 참조를 ObjectA가 영역을 벗어난 후에도 사용한다.
▪
두 상수의 값이 같다는 것을 알고 있기 때문에 ClassA.MAXIMUN_ELEMENTS 대신 클래스 B의 MAXIMUM_ELEMENTS 상수를 사용한다.
◦
이 예시들의 문제는 클라이언트 코드가 클래스의 public 인터페이스의 정보에만 의존하지 않고 private 구현들을 알고 사용한다는 점이다.
•
지나치게 밀접한 결합을 주의하라.
◦
결합은 두 클래스 사이가 서로 얼마나 서로의 구현에 의존하고 있는가를 가리킨다.
◦
일반적으로 결합은 느슨할 수록 좋다. 다음은 이 기본적인 개념으로부터 생겨난 일반 지침들이다.
▪
클래스와 멤버의 접근성을 최소화하라.
▪
friend 클래스는 너무 밀접하게 결합되기 때문에 피하라.
▪
자식 클래스와 부모 클래스가 느슨하게 연결되도록 부모 클래스의 데이터를 protected가 아닌 private으로 선언하라.
▪
클래스의 public 인터페이스에서 멤버를 노출하지 말라.
▪
의미론적인 캡슐화를 유지하라.
▪
디미터 법칙을 준수하라.
◦
결합은 추상화, 캡슐화와 관련이 깊다. 추상화나 캡슐화 둘 중 하나가 망가질 때 강한 결합이 발생한다.
◦
클래스가 완전하지 않은 경우, 이 클래스는 다른 메소드가 해당 클래스의 내부 데이터를 읽거나 쓰게 된다.
3. 설계와 구현 문제
•
좋은 인터페이스를 정의하는 것은 고급 프로그램을 만드는 데 큰 도움을 준다. 내부적인 클래스 설계와 구현도 중요하다.
•
이 절에서는 포함과 상속 관계, 멤버 함수와 데이터, 클래스 결합, 생성자, 값/참조 객체와 관련된 문제를 논의한다.
포함(has a 관계)
•
포함은 클래스가 데이터 요소나 객체를 포함하는 간단한 개념이다.
•
많은 글에서 포함보다는 상속을 언급하는 것은 상속이 더 나은 설계가 아니라 상속이 더 까다롭고 오류가 발생하기 쉽기 때문이다.
•
포함은 객체지향 프로그래밍에서 많은 부분을 쉽게 처리할 수 있는 좋은 기법이다.
•
포함을 이용해서 갖다를 구현하라.
◦
예를 들면 직원은 이름을 갖고, 전화번호를 갖는다. 이때 이름과 전화번호를 Employee 클래스의 멤버 데이터로 만들어 포함을 구현할 수 있다.
•
최후의 수단으로 private 상속을 통해서 포함을 구현하라.
◦
대로는 한 객체를 다른 객체의 멤버로 선언하는 것만으로는 포함을 구현할 수 없다.
◦
이때, 도저히 답이 없다면 c++의 문법 중 하나인 private 상속을 사용하라는 의민데, 다른 언어들엔 없는 개념같아서 무시해도 될 것 같다.
•
약 7개 이상의 데이터 멤버를 포함하는 클래스를 주의하라.
◦
개인이 작업을 수행하고 있을 때, 기억할 수 있는 개별적인 항목의 수는 일반적으로 5 ~ 9개다.
◦
클래스가 만약 7개 이상의 필드를 가지고 있다면 클래스를 더 작은 클래스로 나눌 수 있는지 고민해보라.
상속(is a 관계)
•
상속은 한 클래스가 다른 클래스의 특별한 형태, 확장이라는 개념이다.
•
상속의 목적은 두 개 이상의 자식 클래스에서 공통으로 사용되는 요소를 갖는 부모 클래스를 추출하여 코드의 중복을 줄이는데 있다.
◦
공통적인 요소는 메소드 인터페이스가 구현부, 필드, 데이터형이 될 수 있다.
•
상속을 사용하기로 결심했다면 다음 사항들을 결정해야 한다.
◦
각 멤버 메소드의 경우, 메소드가 자식 클래스에서 보일 것인가? 기본 구현을 포함할 것인가? 기본 구현의 재정의가 가능할 것인가?
◦
각 필드의 경우, 필드가 자식 클래스에서 보일 것인가?
•
이러한 결정의 구체적인 내용은 다음과 같다.
◦
상속을 통해 이다(is a)를 구현하라.
▪
개발자가 부모 클래스를 상속하여 새로운 클래스를 작성하기로 결정할 때, 새로운 클래스는 기존 클래스의 확장 버전이다.
▪
부모 클래스는 자식 클래스가 어떻게 작동할 것인지를 예측하고 자식 클래스가 작동하는 방법에 제약을 가할 수 있다.
▪
자식 클래스가 부모 클래스에 정의된 인터페이스를 완벽하게 따르지 않는다면 상속이 적합한 해결법이 아닌 것이다. 포함이나 상속 계층 변경을 고려하라.
◦
상속을 고려해서 설계하고 문서화하라. 그게 아니면 상속을 금지하라.
▪
상속은 프로그램을 복잡하게 만들기 때문에 위험한 기법이다.
◦
리스코프 치환 원칙을 따르라.
▪
바바라 리스코프는 객체지향 프로그래밍에 관한 그녀의 논문에서 자식 클래스가 상위 클래스의 is a 버전 아니라면 부모 클래스로부터 상속받아서는 안 된다고 주장한다.
▪
앤디 헌트와 데이브 토마스는 다음과 같이 얘기했다. 서브 클래스는 사용자가 그 차이점을 모른 채 기본 클래스의 인터페이스를 통해서 사용할 수 있어야 한다.
•
다시 말하면 상위 클래스의 메소드나 하위 클래스의 메소드는 서로 기대되는 책임에 대한 수행이 같아야한다는 것이다.
▪
프로그램이 이를 잘 준수한다면 개발자들이 구현 세부 사항에 대해서 걱정하지 않고 객체의 일반적인 특성, 즉 추상적인 내용에 중점을 둘 수 있기 때문에 상속을 통해 복잡성을 효과적을 관리할 수 있다ㅓ.
•
하지만 개발자가 자식 클래스를 사용할 때마다 부모 클래스와의 차이점에 대해서 계속해서 긴장을 늦추지 않아야 한다면 이는 곧 캡슐화가 깨지는 것으로 이어질 것이다.
⇒ 이 차이점을 알고 있다는 건, 이미 구현 세부 사항을 봤다는 의미다. 인터페이스에 집중하지 않고 구현 세부 사항을 신경쓰게 된 이상, 캡슐화는 의미가 없어질 것이다.
◦
상속받고 싶을 때만 상속받게 하라.
▪
자식 클래스는 멤버 메소드나 메소드의 구현, 또는 둘 다 상속 받을 수 있다.
•
재정의 가능한 추상 메소드는 자식 클래스가 부모 클래스의 메소드 시그니처를 상속받지만 구현부는 상속받지 않는다.
•
재정의 가능한 메소드는 자식 클래스가 부모 클래스의 메소드 시그니처와 구현을 상속받으며 구현은 재정의할 수 있다는 것을 의미한다.
•
재정의 불가능한 메소드는 자식 클래스가 부모 클래스의 메소드 시그니처와 기본 구현을 상속받지만 메소드의 구현은 재정의할 수 없음을 의미한다.
▪
메소드 시그니처가 필요 없고 구현만 사용하고 싶다면 상속이 필요없다. 포함으로도 충분한 개념 표현이 가능하다.
◦
재정의가 불가능한 메소드를 재정의하지 말라.
▪
자바에서는 개발자가 재정의가 불가능한 메소드를 재정의할 수 있다.
▪
부모 클래스에서 private인 메소드를 자식 클래스에서 같은 이름으로 생성할 수 있다.
▪
자식 클래스의 코드를 읽는 개발자에게는 그런 함수가 혼란을 초래할 수 있다. 왜냐하면 그것이 다형성인 것처럼 보이지만 실제로는 그렇지 않고 이름만 같기 때문이다.
▪
이 원칙을 다르게 말하면 재정의가 불가능한 부모 클래스의 이름을 자식 클래스에서 재사용하지 마라다.
◦
공통으로 사용되는 인터페이스와 데이터, 행위를 상속 단계에서 가능한 가장 높은 곳에 위치시켜라.
▪
인터페이스와 필드, 행위를 더 높은 수준으로 옮길수록 자식 클래스는 그것들을 더 쉽게 사용할 수 있다.
▪
가장 높은 곳이란 뭘까? 이는 추상화를 기준으로 삼아라. 메소드를 더 높이 위치시켰을 때 위치시킨 객체의 추상화에 문제가 생긴다면 그 아래에서 멈춘다.
◦
인스턴스가 하나뿐인 클래스를 의심하라.
▪
생성한 인스턴스가 하나뿐일 때는 객체를 클래스로 잘못 알고 설계했을 수 있다. 클래스 대신 객체를 생성할 수 있는지 고려하라.
▪
자식 클래스를 별개의 클래스가 아닌 데이터로 표현할 수 있다면 그 친구는 상속이 아니라 생성을 해야한다.
▪
싱글톤 패턴은 다분히 의도적인 형태이므로 예외다.
◦
자식 클래스가 하나 뿐인 부모 클래스를 의심하라.
▪
자식 클래스가 하나뿐인 부모 클래스를 보면 너무 과한 설계일 가능성이 있다.
⇒ YAGNY가 생각났다.
▪
미래에 무엇이 필요한지 모르기 때문에 이와 같이 하게되는 것이다. 미래를 그리는 설계는 언젠가 구현될지도 모르는 것을 설계하는 것이 아니라, 가능한 현재의 작업을 분명하고 직관적이며 단순하게 만드는 것이다.
▪
이는 필요한 것 이상으로 상속 구조를 만들어서는 안 된다는 것을 의미한다.
◦
메소드를 재정의했는데, 파생된 메소드 내부에서는 아무것도 하지 않는 클래스들을 의심하라.
▪
Cat 클래스에 Scratch() 메소드가 있다고 해보자. 어떤 고양이는 발톱이 없어서 긁을 수 없다면 그 고양이의 재정의된 메소드는 구현이 비어있을 것이다.
▪
어떻게 해야할까? 이럴땐 Claws 클래스를 생성하고 그 클래스를 Cat 클래스에 포함시키는 것이 낫다.
▪
이 문제의 본질적인 요지는 모든 고양이가 긁을 수 있다고 가정하기 때문에 생긴다. 문제가 발생한 곳보다 문제의 근원을 해결하도록 하자.
◦
깊은 상속 구조를 피하라.
▪
아서 리엘은 그의 책 객체지향설계 휴리스틱에서 상속 계층을 최대 6단계로 제한할 것을 제안했다, 리엘은 매직 넘버 5 ~ 9를 토대로 단계를 제안했다.
▪
저자는 이 주장에 대해서 낙관적인 제안이라고 했다. 우리는 2, 3단계만 내려가도 상속 구조를 쉽게 까먹기 때문에 이 숫자들을 단계의 수보단 자식 클래스의 수로 두는 것이 더 기억하기 쉽다.
◦
광범위한 타입검사보다 다형성을 택하라.
▪
많은 양의 case 문을 보게된다면 상속을 적용하는 게 낫지 않을까 생각하게 된다. 때로는 case 문이 완전히 서로 다른 객체나 행위를 구분하는 데 사용되기도 하기 때문에 주의해서 적용하자.
◦
모든 데이터를 보호가 아닌 비공개로 만들어라.
▪
상속은 캡슐화를 망가뜨린다. 어떤 객체로부터 상속을 받을 때 객체의 protected 속성에 대한 접근 권한을 얻게 된다.
▪
정말로 자식 클래스에서 부모 클래스의 속성에 접근해야 한다면 protected로 선언된 함수를 대신 제공하라.
•
다중 상속
◦
저자의 경험에 비추어봤을 때, 다중 상속은 객체에 일련의 속성을 추가하는 데 사용하는 간단한 클래스인 믹스인을 정의할 때 주로 쓸모가 잇다.
▪
믹스인은 거의 추상적이며 그 자체를 다른 객체의 인스턴스로 사용하기 위한 것은 아니다.
◦
모든 믹스인이 서로 완벽하게 독립적이기만 하면 다중 상속과 관련된 다이아몬드 상속 문제를 만들지는 않는다.
⇒ 하나의 클래스가 서로 다른 두 클래스로부터 상속받을 때, 이름이 같은 함수를 상속받는 문제다.
◦
또한 특성을 청킹함으로써 설계를 이해하는 데 도움을 준다.
◦
자바는 인터페이스의 다중 상속을 허용하되 클래스 상속은 허용하지 않음으로써 믹스인의 가치를 인정하고 있다.
◦
다른 대안을 신중하게 고려하고 시스템의 복잡성과 시스템을 이해하는 데 미치는 영향을 평가한 후에만 다중 상속을 사용해야 한다.
•
상속에 관한 규칙이 왜 이렇게 많을까? 단순하다, 상속이 사용되면 복잡도를 관리하기 어려워지기 때문이다. 복잡성을 잘 관리하고 싶다면 상속을 멀리하는 것이 좋다.
•
다음은 상속을 언제 사용하고 포함을 언제 사용할지를 요약한 내용이다.
◦
다중 클래스가 특정 공통 데이터를 공유하지만 행위를 공유하지 않는다면 해당 클래스들이 포함할 수 있는 공통 객체를 만든다.
◦
다중 클래스가 공통 행위는 공유하지만 데이터를 공유하지 않는다면 공통 메소드를 정의한 공통 클래스를 상속받는다.
◦
다중 클래스가 공통 데이터와 행위를 공유한다면 공통적인 데이터과 메소드를 정의한 공통 클래스를 상속받는다.
◦
인터페이스를 제어하기 위한 상위 클래스가 필요할 때는 상속을 하고 인터페이스를 제어하고 싶다면 포함한다.
멤버 함수와 데이터
•
다음은 클래스의 멤버들을 관리하기에 효과적인 지침이다.
•
클래스에 가능한 적은 수의 메소드를 두라.
•
원하지 않는 멤버 메소드와 연산자가 암묵적으로 생성되지 않도록 하라.
•
클래스에서 호출되는 메소드의 수를 최소화하라.
◦
클래스가 사용한 클래스가 많을수록 오류도 증가하는 경향이 있다. 이런 개념을 팬 아웃이라고 한다.
•
다른 클래스에 대한 간접적인 메소드 호출을 최소화하라.
◦
직접적인 연결은 보통 깨지기 쉽다. 그러나 .이 많이 찍힌 호출은 훨씬 더 위험할 수 있다.
⇒ 디미터 법칙이다.
•
일반적으로 클래스가 다른 클래스와 협력하는 정도를 최소화하라.
◦
다음을 모두 최소화하기 위해 노력하라.
▪
인스턴스로 만드는 객체의 수
▪
인스턴스로 만든 객체에 대한 서로 다른 직접적인 메소드 호출의 수
▪
인스턴스로 만든 다른 객체가 반환하는 객체에 대한 메소드 호출의 수
생성자
•
다음은 생성자에 적용되는 지침이다.
•
가능하다면 모든 멤버 데이터를 모든 생성자에서 초기화하라.
◦
객체는 생성 시점에 온전한 것이 좋다.
•
비공개 생성자를 사용해 싱글턴 속성을 구현하라.
◦
싱글톤 패턴을 사용하고 싶다면 클래스의 모든 생성자를 숨기고 단일 인스턴스에 접근하기 위한 static getInstance() 메소드를 제공하는 방법으로 구현할 수 있다.
•
다른 사실이 증명될 때가지 얕은 복사보다 깊은 복사를 택하라.
◦
복잡한 객체가 복사될 필요가 있을때 결정할 사항이다. 가급적이면 깊은 복사를 선택하는 것이 좋다. 값이 변경될 가능성이 있는 포인트를 늘리는것은 좋지 않다.
4. 클래스를 작성하는 이유
•
여기까지 잘 읽었다면 클래스를 생성해야 하는 유일한 이유는 현실 세계의 객체를 모델링하기 위한 것이라고 생각할 것이다.
•
실제로는 그보다 다양한 이유로 클래스를 만들어야 한다. 다음은 그 이유들이다.
클래스를 만들어야 하는 이유
•
현실 세계의 객체를 모델링한다.
◦
프로그램이 모델링하는 현실 세계의 모든 객체 타입에 대한 클래스를 생성하라.
◦
객체에 필요한 데이터를 클래스에 입력하고 객체의 행위를 모델링하는 서비스 루틴을 구축한다.
•
추상 객체를 모델링한다.
◦
추상 객체, 즉 실체는 없지만 다른 실질적인 객체의 추상화를 제공하는 객체를 모델링하기 위함이다.
▪
Shape 객체가 좋은 예다.
◦
현실 세계에 존재하지 않으므로 구체적인 추상화를 이끌어내기 위해 많은 노력이 필요하다. 현실 세계의 엔티티로부터 추상적인 개념을 만들어내는 과정에는 정답이 없고 설계하는 사람마다 서로 다른 형태로 추상화 할 것이다.
•
복잡성을 줄인다.
◦
클래스를 생성하는 가장 중요한 이유는 프로그램의 복잡성을 줄이는 것이다.
▪
클래스를 생성하여 정보를 은닉하면 그러한 정보에 대해서 생각하지 않아도 된다.
◦
또 다른 이유는 코드의 반복을 줄이거나 유지보수성을 높이기 위해서다.
•
복잡성을 고립시킨다.
◦
복잡한 것들을 특정한 클래스 안에만 존재하도록 하기 위함이다.
◦
오류를 수정해도 해당 클래스만 수정하면 되고 다른 클래스에 영향을 미치지 않을 것이다.
•
구현 세부 사항을 숨긴다.
•
변경의 효과를 제한한다.
◦
변경할 가능성이 있는 부분을 고립시켜서 변경의 효과가 미치는 범위를 제한한다.
◦
변경될 가능성이 높은 부분은 외부 의존성, 복합 데이터형, 비즈니스 로직이 있다.
•
전역 데이터를 숨긴다.
◦
전역 데이터를 사용할 필요가 있다면 구현 세부 사항을 인터페이스 뒤에 숨길 수 있다.
◦
get 메소드를 통해서 전역 데이터를 다루면 전역 데이터를 직접 참조하는 것보다 여러 이점을 제공한다.
▪
프로그램을 변경하지 않고도 데이터의 구조를 변경할 수 있다.
▪
데이터에 대한 접근을 감시할 수 있다.
•
매개변수 전달을 간소화한다.
◦
수많은 데이터를 매개변수로 일일이 전달하는 것보다 클래스를 만드는 편이 더 나을 수 있다.
•
중앙 집중 관리한다.
◦
한 곳에서 작업을 처리하는 것은 좋은 아이디어다.
•
코드 재사용을 돕는다.
◦
잘 분리된 클래스에 코드를 작성하면 하나의 큰 클래스에 코드가 포함되어 있을때보다 쉽게 다른 프로그램에서 재사용 될 수 있다.
◦
어떤 코드가 프로그램 내의 한 곳에서만 호출되고 큰 클래스로도 이해하기 쉽다 하더라도 해당 코드 블록이 다른 곳에서 호출될 가능성이 있다면 클래스로 추출하는 것이 좋다.
•
프로그램 전체를 고려한다.
◦
프로그램의 변경을 염두에 두고 있다면 그러한 부분을 별도의 클래스로 분리하여 고립시키는 것이 좋다.
◦
프로그램의 나머지 부분에 영향을 미치지 않고 클래스를 변경하거나 그 대신에 완전히 새로운 클래스를 구현할 수 있다.
•
연관된 기능을 패키지로 구성한다.
◦
정보를 숨기거나 데이터를 공유하거나 확장 가능한 형태로 설계할 수 없다면 적당한 그룹으로 패키징하면 된다.
◦
클래스는 연관된 기능을 결합하는 한 가지 수단이다.
•
특정한 리팩토링을 수행한다.
◦
하나의 클래스를 두 개의 클래스로 변환하고 위임을 감추고 중간자를 제거하고 확장 클래스를 만드는 방법으로 새로운 클래스를 만들어야할 수 있다.
피해야 할 클래스
•
모든 것을 알고 있는 클래스를 생성하지 말라.
◦
모든 것을 알고 있고 모든 것을 할 수 있는 전지전능한 클래스를 만들지 말라.
◦
어떤 클래스가 get, set 메소드를 사용해 다른 클래스로부터 데이터를 가져와 뭔가를 한다면 그 기능을 그 클래스에게 위임할 방법을 모색하자.
•
관련이 없는 클래스를 제거하라.
◦
클래스가 행위없이 데이터로만 구성된다면 정말 클래스인가에 대해서 고민해보고 없애서 다른 클래스의 속성이 될 수 있을지 고려해본다.
•
동사를 뒤에 붙이는 클래스를 피하라.
◦
데이터없이 행위로만 구성된 클래스는 클래스가 아니다. 다른 클래스의 메소드로 변환할 것을 고려해본다.
5. 프로그래밍 언어와 관련된 이슈
•
프로그래밍 언어마다 클래스에 대한 접근 방법은 판이하다.
•
자식 클래스에서 다형성을 구현하기 위해 메소드 멤버를 어떻게 재정의하는 지를 생각해보자.
◦
자바에서는 모든 메소드가 기본적으로 재정의 가능하며 자식 클래스의 재정의를 막기 위해선 final로 선언해야 한다.
◦
C++에서는 기본적으로 메소드 재정의가 불가능하다. 재정의가 가능하려면 virtual로 선언해야 한다.
•
클래스와 관련해 프로그래밍 언어마다 크게 다른 부분은 다음과 같다.
◦
상속 트리에서 재정의된 상속자와 소멸자의 작동 방식
◦
예외 처리 조건에서 생성자와 소멸자의 작동 방식
◦
기본 생성자(인자가 없는 NoArgsConstructor)의 중요성
◦
소멸자나 종결자가 호출되는 시기
◦
할당과 동치(논리적으로 같은) 연산자와 같이 프로그래밍 언어에서 기본적으로 제공하는 연산자들을 재정의하는 방법
◦
객체가 생성되고 소멸될 때나 객체가 선언되고 범위를 벗어날 때 처리되는 메모리 처리 방식
6. 클래스를 넘어서: 패키지
•
클래스는 현재 모듈화를 수행하기에 가장 최고의 방법이다. 하지만 모듈화는 범위가 큰 주제이며 클래스를 넘어선다.
•
객체의 결합을 관리하는 좋은 툴이 있다면 분명히 추상화와 캡슐화를 더 쉽게 달성할 수 있을 것이다.
•
클래스보다 더 추상적인 개념에서의 책임을 묶는게 패키지다. 이는 더 높은 추상 계층을 제공함으로써 이해를 돕는다.
요점정리
•
클래스의 인터페이스는 일관성 있는 추상화를 제공해야 한다. 이 규칙을 어기면 많은 문제가 발생한다.
•
인터페이스는 시스템 인터페이스나 설계 결정, 구현 세부 사항을 드러내선 안 된다.
•
is a 관계를 모델링하고 있지 않다면 상속보단 포함을 선택하는 것이 좋다.
•
상속은 유용한 도구지만 복잡성을 증가시키며 그로 인해 복잡성 관리가 어려워진다.
•
클래스는 복잡성을 관리하기 위해서 사용할 수 있는 기본적인 도구다. 복잡성을 관리할 수 있도록 설계에 많은 주의를 기울이라.