////
Search
Duplicate
🚖

6. 클래스 다루기

클래스는 연관성이 높고 잘 정의된 기능을 공유하는 필드와 메소드의 모음으로 비교적 최근 자리잡은 개념이다.
이 장은 고급 클래스를 만드는 데 필요한 핵심적인 조언을 소개한다.

1. 클래스의 토대: 추상 데이터형(Abstract Data Type)

추상 데이터형이란 데이터데이터를 처리하는 연산의 집합이다.
연산은 프로그램의 나머지 영역들에게 데이터가 무엇인지를 설명해주는 역할과 그 데이터를 변경할 수 있게 해주는 역할을 한다.
객체지향 프로그래밍을 이해하기 위해서는 ADT를 반드시 이해해야 한다. 이를 이해하지 않는다면 클래스명만 클래스인 코드를 작성하게 될 것이다.
그런 클래스는 실제로는 연관성이 높지 않은 필드와 메소드를 편의를 위해 보관하는 도구 상자와 다를 게 없다.
전통적으로 프로그래밍 책에서는 추상 데이터형을 소개할 때, 수학적으로 설명한다.
추상 데이터형은 연산자들이 모여있는 수학적 모델로 생각할 수 있다와 같이 표현하는 경향이 있다.
이는 저수준의 구현 엔티티가 아닌 실제 세계에 존재하는 엔티티를 조작할 수 있게 해준다.
링크드 리스트에 노드를 삽입하는 것과 같이 구체적인 수준이 아닌 표에 행을 추가하는 등 추상화시켜서 문제를 다룰 수 있게 한다는 뜻이다.

ADT를 사용할 때 좋은 점

구현 세부 사항을 감출 수 있다.
구현을 숨기게 되면 해당 구현에 대해서 나머지 영역들은 몰라도 된다. 구현이 바뀌게 된다면 해당 범위만 수정해주면 된다.
변경이 전체에 영향을 미치지 않는다.
인터페이스가 더 많은 정보를 제공하도록 만들 수 있다.
유사한 모든 연산을 하나의 ADT에 모아놓으면 그 자체로 맥락을 제공해준다.
성능을 향상시키기 쉽다.
구현 부분만 성능을 향상시키면 된다.
프로그램이 명백하게 정확해진다.
실제 세계의 엔티티를 조작하기 때문에 조금 더 명확하고 이해하기 쉽게 코드를 짤 수 있다.
프로그램의 가독성이 높아진다.
전체 프로그램에 데이터를 넘길 필요가 없다.
속성에 대한 책임을 그에 대한 처리 책임을 가지고 있는 객체가 도맡아 처리하면 된다. 여기저기 해당 속성이 돌아다닐 필요가 없다.
저수준 구현 구조체 대힌 현실 세계의 개체를 다룰 수 있다.

ADT 사용 시 원칙

전형적인 저수준 데이터형을 저수준 데이터형이 아닌 ADT로 만들거나 사용하라.
결국 뭔가를 표현하기 위해 저수준 데이터형을 사용하므로 그것들을 ADT로 만들었을때, 그것들의 책임을 오롯이 그 객체에게 전담할 수 있다. 다른 처리 메소드를 만들 필요가 없다.
파일과 같은 일반적인 객체를 ADT로 취급하라.
파일도 추상화된 영역이다. 구체적으로는 디스크의 어떠한 영역이다.
간단한 객체도 ADT로 취급하라.
ADT가 저장된 매체와 독립적으로 ADT를 참조하라.
요즘은 데이터베이스를 많이 쓰니 데이터베이스와 독립적으로 비즈니스 로직을 참조하라라고 이해해도 될 것 같다.
비객체지향 프로그래밍 환경에서 ADT로 여러 개의 데이터 인스턴스를 다루기
과거의 잔재
ADT와 클래스
추상 데이터형은 클래스의 기본 개념의 바탕을 이룬다.
클래스는 일반적으로 상속과 다형성이라는 개념도 지원하기 때문에 추상 데이터형에 상속과 다형성을 더한 것을 클래스로 생각하자.

2. 좋은 인터페이스

좋은 클래스를 만들기 위한 가장 좋은 방법은 좋은 인터페이스를 만드는 것에서 시작한다.
⇒ 인터페이스를 꼭 만들 필요는 없다고 생각하기 때문에 좋은 클래스 초안의 특징으로 생각해도 좋을 것 같다.

좋은 추상화

추상화는 복잡한 연산을 단순한 형태로 보여주는 능력이다. 인터페이스는 인터페이스 내부에 숨겨져 있는 구현 세부 사항에 대한 추상화를 제공한다.
인터페이스는 서로 같은 도메인에 책임을 가지고 있는 메소드들을 제공해야만 한다.
직원을 구현하는 클래스가 있다고 하자. 우리의 도메인의 책임은 직원 정보를 관리하거나 사용하는 책임을 가지고 있다.
필드로는 이름, 주소, 전화번호 등이 존재할 것이다.
메소드로는 각 정보들을 조회하고 초기화하는 기능들이 존재할 것이다.
이처럼 메소드들 간의 밀접한 관계를 맺고 있고 유사한 책임을 달성하기 위한 목적을 지향하고 있어야 좋은 클래스가 될 수 있다.
클래스 추상화에 대한 평가는 public 메소드의 집합을 기초로 한다. 이는 곧 인터페이스를 의미한다.

좋은 추상화 수준을 가지는 인터페이스를 만드는 데 도움을 주는 원칙들

인터페이스가 일관된 추상화 수준을 갖도록 한다.
클래스는 추상 데이터형의 구현체라고 생각하면 이해하기 쉽다.
각 클래스는 오직 하나의 ADT만 상속해야 한다. 다중 상속은 허용되지 않는다. 책임은 하나여야만 한다.
만약 여러 ADT가 하나의 클래스에 있다면 클래스를 분리할 때가 온 것이다.
클래스가 구현하고 있는 추상화가 무엇인지 이해해야 한다.
클래스가 가지고 있는 책임이 뭔지 명확하게 정의해야 한다는 원칙같다.
자신이 이 클래스에게 어떤 책임을 부여하려 하는지, 명확하게 알고 있는 상태에서 유사한 추상화들 중 하나를 선택해야 한다.
서로 반대되는 기능을 갖는 서비스 쌍을 제공하라.
대부분의 연산은 유사하거나 정반대의 연산을 가지고 있다. 불을 켜는 연산이 있으면 끄는 연산도 있듯이 말이다.
굳이 만들 필요는 없지만 이런 사례들은 참고할만큼 충분히 많으므로 기능을 만들 때 한번 고려하는 것이 좋다.
관련이 없는 정보를 다른 클래스로 옮겨라.
가능하면 인터페이스를 의미론적이기보다는 프로그래밍적으로 만들어라.
인터페이스는 프로그래밍적인 부분과 의미론적인 부분으로 구성된다.
프로그래밍적인 부분은 인터페이스에서 컴파일러로 강제할 수 있는 속성으로 구성된다.
의미론적인 부분은 인터페이스가 어떻게 사용될 것인지에 대한 가정으로 구성된다. 이 부분은 컴파일러로 강제할 수 없다.
이는 methodAmethodB보다 먼저 호출되어야 한다와 같은 고려사항을 포함한다.
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 관계를 모델링하고 있지 않다면 상속보단 포함을 선택하는 것이 좋다.
상속은 유용한 도구지만 복잡성을 증가시키며 그로 인해 복잡성 관리가 어려워진다.
클래스는 복잡성을 관리하기 위해서 사용할 수 있는 기본적인 도구다. 복잡성을 관리할 수 있도록 설계에 많은 주의를 기울이라.