- 이번 챕터는 “객체는 작아야 한다" 한문장으로 요약할 수 있다.
- 작은 객체란? : 유지보수가 가능한 객체
- 항상 유지보수성을 생각하자
2-1 가능하면 적게 캡슐화하자
자바가 바라보는 객체지향 관점
- 많은 객체를 캡슐화 한다면 클래스에 문제가 있다고 볼 수 있다.
- 따라서 리팩토링이 필요하다.
- 동일한 값을 캡슐화하는 클래스의 객체는 동일하다.
- 자바 언어에서 보면 올바른 설명은 아니지만 이것은 자바 언어가 안고있는 설계적 결함이다?
- 자바 뿐만 아니라 C++에서도 객체의 식별자와 상태는 서로 분리되어있다.
- 위의 예제에서는 두 객체 x와 y의 상태는 동일하지만 식별자는 서로 다르다.
- ==연산자를 비교해서 듣게되면 false얻게되고
- equals의 기본구현 역시 두 객체가 서로 동일하지 않다고 나온다.
- 이런 특성은 C++로부터 물려받은 Java 언어의 설계적 결함이라고 한다.
- 여기서 필자가 이해하고있는 OOP란 ?
- “객체"는 고수준의 행동을 낳기 위해 함께 동작하는 객체들의 집합체로 보고있다.
- 책은 페이지, 표지, ISBN의 집합체이며 책장은 책과 제목의 집합체로 볼 수 있다.
- 다른 객체를 캡슐화하지 않는 객체란 존재하지도 않으며 존재할 수도 없다는 사실을 보여주고 있다.
- 부품이 없는 객체는 의미가 없다.
- 하지만 Java는 내부에 부품을 전혀 포함하지 않는 객체를 생성할 뿐만 아니라 부품을 포함하지 않는 복사본과 비교했을 때 서로 다른 객체라고 판단하는 상황도 발생할 수 있다.
- Java 언어에서는 상식적인 일이다. Java를 비롯한 대부분의 OOP 언어에서 객체는 단지 메서드가 추가된 데이터의 집합일 뿐이다.
- 객체는 데이터를 저장할 수 있는 껍질과 유사하다. 데이터의 저장 여부와 상관없이 복사본을 비교하는 경우에도 하나의 껍질은 다른 껍질과는 다르다.
- 위의 예제가 자바가 객체를 바라보는 방식이다.
- 어떤 데이터도 포함하지 않고 또한 서로 다른 껍질이기 때문에 다르다고 판단한다.
- 이것은 완전히 잘못된 방식이라고 이야기하고있다. 왜?
- 상태없는 객체는 존재해서는 안되고, 상태는 개체의 식별자여야 한다.
- 객체의 식별자는 기본적으로 세계 안에서 객체가 위치하는 좌표라고 할 수 있다.
- 지구가 유일한 좌표공간이라는 가정 하게 나의 식별자는 이름과 생년월일이다. 두 정보만 있으면 나를 찾을 수 있으며 제조사, 모델, 제조년도 라는 정보만 있다면 나의 자동차를 찾을 수도 있다.
- 여기서 말하는 요점은 4개 이상의 좌표는 직관에 위배된다는 점이다.
- 세계 안의 객체를 바라보는 우리의 사고방식으로 4개 이상의 요소로 구성된 좌표를 이해하는 것은 너무나도 어렵다.
- 책에서 나오는 반례 : 자신의 이웃이 동일한 제조사, 모델, 제조년도의 자동차를 가지고 있더라도 이것은 여전히 두 차는 다른차가 아닌가??
- 동의하고 있지만 “실세계"의 자동차가 “객체지향 세계"의 자동차보다 더 복잡하기 때문이다.
- 물론 객체가 실제 자동차를 참조한다면? 객체를 유일하게 식별하기 위해 더 많은 좌표와 속성이 필요할 것이다.
- 하지만 이 속성들은 객체를 이용해 다시 그룹화가 될 수 있기 때문에 결과적으로 전체 속성들은 객체들로 구성된 트리를 구성하게 된다.
- 자동차 객체는 타입과 차량 인식번호를 캡슐화하고 타입은 다시 제조사, 모델, 제조년도를 캡슐화 할 수 있다.
- 결과적으로 자동차 객체는 3개의 작은 객체를 포함하게 된다.
- 객체를 작게 분할하여 나누자. 4개가 최대이다.
- 자바의 결함을 해결하기 위해 ==연산자를 사용하지 말고 equals 메서드를 오버라이드하자.
예제 화면

캡슐화 하기에 적합한 객체의 개수는 4개다.
결론
2-2 최소한 뭔가는 캡슐화하자
- 2-1의 내용에 추가하여 어떤 것도 캡슐화 하지 않는 객체가 존재할 수 있다.
- 아래의 코드는 어떤것도 캡슐화 하지 않기 때문에 이 클래스의 모든 객체들은 동일하다는 사실을 알 수 있다.
- 하지만 이것도 잘못된 설계 방식이다. 너무 많이 캡슐화 하는 것도 좋지 않지만 반대로 아무것도 캡슐화 하지 않는것도 좋지 못하다.
- 프로퍼티가 없는 클래스는 객체지향 프로그래밍에서 악명이 높은 “정적 메서드"와 유사하다. 이런 클래스는 아무런 상태와 식별자를 가지지 않고 오직 행동만을 포함하게 된다.
- 정적 메서드가 존재하지 않고 인스턴스 생성과 실행을 엄격하게 분리하는 순수한 OOP에서는 기술적으로 프로퍼티가 없는 클래스를 만들 수 없기 때문이다.
어떤부분이 문제일까?
- 실행으로부터 인스턴스 생성을 고립시켜야 한다. 즉 오직 생성자에서만 new 연산자를 허용해야한다는 것을 말한다.
- 오직 생성자 안에서만 new 연산자를 허용한다고 가정하자.
- 위의 예제의 read() 메서드는 유틸리티 클래스의 System의 정적 메서드를 사용하고 있다.
- 순수한 OOP에는 정적 메서드가 존재하지 않기 때문에 당연히 정적 메서드를 호출하는 일 역시 불가능하다.
- 대신 어떤 클래스의 인스턴스를 생성한 후 이 인스턴스를 통해 시스템 클럭을 얻어야 한다.
- 객체가 “무"와 아주 비슷한 어떤 것이 아니라면 무언가를 캡슐화 해야한다.
- 여기서 “무"란 세계 안에서 좌표가 없는 존재를 의미하고 있다.
- 이런 특징을 가진 엔티티만이 아무것도 캡슐화하지 않을 수 있는데, 오직 하나만 존재하고 생존이나 자신의 좌표를 표현하기 위해 다른 엔티티를 필요로 하지 않기 때문이다.
- 반대로 어떤 일을 수행하는 객체는 다른 객체들과 공존하면서 이를 사용하게 된다.
- 이 객체는 자기 자신을 식별할 수 있도록 다른 객체들을 캡슐화해야 한다.
- 다소 추상적이고 철학적으로 들릴 수 있지만? 여기에는 실용적인 추론이 개입할 여지가 없다. 우리는 분명히 어떤 것도 캡슐화하지 않는 객체를 생성할 수 있으며 다양한 예들을 나열할 수도 있다.
- 하지만 철학적인 관점에서 이야기할 때 이런 객체는 잘못됐으며 이것이 실용적인 측면에서도 잘못된 이유이다.
- 캡슐화된 상태는 세계 안에서 객체의 위치를 지정하는 고유한 식별자이다.
- 객체가 어떤것도 캡슐화하지 않는다면 객체의 좌표는 무엇일까?
- 바로 객체 자신이 세계 전체가 되어야 한다. !
- 여기서 말하고자 하는 year의 올바른 클래스
2-3 항상 인터페이스를 사용하자
- 객체는 살아있는 유기체라고 할 수 있다.
- 객체는 다른 유기체들과 의사소통하면서 그들의 작업을 지원하고 다른 유기체들 역시 이 객체에게 도움을 제공한다. 객체들의 세계는 매우 사회적이면서 유대감이 높은 환경이다.
- 핵심은 객체들이 서로 필요로 하기 때문에 결합된다는 뜻이다.
- 설계를 시작하는 단계에서는 각각의 객체가 어떤 일을 수행해야 하고 다른 객체에게 어떤 서비스를 제공하는 지를 정확하게 알고 있는 편이 낫기 때문에 결합이 유용하다.
- 하지만 애플리케이션이 성장하기 시작하고 객체들의 수가 수십 개를 넘어가면서부터 객체 사이의 강한 결합도가 심각한 문제로 이어질 수 있다.
- 결합도 문제는 유지보수성에 영향을 미치고 이 책의 목적은 유지보수성이 좋게 코드를 작성하는 것이 목표이기 때문에 이런식으로 설계하는 것을 권장하지 않는 것 같다.
- 애플리케이션 전체를 유지보수 가능하도록 만들기 위해서는 최선을 다해 객체를 분리해야한다.
- 기술적인 관점에서 객체 분리란?
- 상호작용하는 다른 객체를 수정하지 않고도 해당 객체를 수정할 수 있도록 만들어야 한다.
- 인터페이스 써라..!!!
- Cash는 인터페이스이다. 다시 말해 우리의 객체가 다른 객체와 의사소통하기 위해 따라야 하는 “계약"이라고 할 수 있다.
- 금액이 필요하다면 실제 구현 대신 계약에 의존하면 된다.
- Employee 클래스는 Cash 인터페이스의 구현 방법에는 아무런 관심이 없다. multiply() 메서드가 어떻게 동작하는지도 관심이 없다. 간단히 말해 동작 방식을 알지 못한다.
- 글자대로 Cash 인터페이스를 사용하면 Employee 클래스와 DefaultCash 클래스가 느슨하게 분리할 수 있다는 의미를 뜻한다.
- 이제 DefaultCash의 내부 구현을 변경하거나 심지어 Cash 인터페이스의 구현체를 다른 것으로 교체하더라도 Employee에는 아무런 영향을 주지 않는다.
- 또다른 규칙을 따르면 더욱 좋다.
- 클래스 안의 모든 퍼블릭 메서드가 인터페이스를 구현하도록 만들어야 한다.
- 올바르게 설계된 클래스라면 최소한 하나의 인터페이스라도 구현하지 않는 퍼블릭 메서드를 포함해서는 안된다.
- cents() 메서드는 어떤 것도 오버라이드 하지 않기 때문에 문제가 있다. 이 설계는 클래스의 사용자로 하여금 이 클래스에 강하게 결합되도록 조장하게 된다.
- 다른 클래스의 객체가 직접적으로 Cash.cents()를 사용할 수 밖에 없기 때문이다. 즉 새로운 메서드를 이용해서 구현을 대체할 수 없다.
- 조금 더 철학적인 관점에서 클래스가 존재하는 이유는?
- 다른 누군가가 클래스의 서비스를 필요로 하기 때문이다.
- 서비스는 계약이자 인터페이스기 때문에 클래스가 제공하는 서비스는 어딘가에 문서화가 되어야 한다.
- 서비스 제공자들은 서로 경쟁한다.
- 동일한 인터페이스를 구현하는 여러 클래스들이 존재한다는 것을 의미한다.
- 그리고 각각의 경쟁자는 서로 다른 경쟁자를 쉽게 대체할 수 있어야한다.
- 이것이 바로 느슨한 결합도이다.
2-4 메서드 이름을 신중하게 선택하자
빌더의 이름은 명사로, 조정자의 이름은 동사로 짓자
빌더란?
- 뭔가를 만들고 새로운 객체를 반환하는 메서드를 가리킨다. 빌더는 항상 뭔가를 반환한다.
- 빌더의 반환타입은 절대 void가 될 수 없으며 이름은 항상 명사여야 한다.
- parsedCell() 메서드의 이름은 단순한 명사 형태가 아니라 형용사인 parsed가 명사인 cell을 꾸며주고 있다.
- 이 이름은 앞에서 설명한 원칙을 위반하지 않는다. 단지 형용사를 덧붙여 의미를 좀 더 풍부하게 설명하고 있을 뿐이다.
- 객체로 추상화한 실세계 엔티티를 수정하는 메서드를 조정자(mainpulator)라고 부른다.
- 조정자의 반환 타입은 항상 void이고 이름은 항상 동사이다.
- quicklyPrint() 메서드의 이름은 부사의 꾸밈을 받는 동사로 지어졌다.
- 여기에서 중심이 되는 요소는 동사인 print이다. 그에 비해 부사인 quickly는 단지 메서드의 문맥과 목적에 관한 풍부한 정보를 제공하기 위해 print를 설명해주고 있다.
- 빌더와 조정자에게 우리만의 이름을 붙일 경우에도 여기에서 제시한 원칙을 지키는 것을 권장한다.
- 빌더는 어떤것을 만들고, 조정자는 뭔가를 조작한다.
- 개념적으로 빌더와 조정자 사이에는 어떤 메서드도 존재해서는 안된다
- 즉 뭔가를 조작한 후 반환하거나
- 뭔가를 만드는 동시에 조작하는 메서드가 있어서는 안된다는 것이다. 원칙을 위반하는 예