오브젝트 5장 책임 할당하기
- 책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기 쉽지 않다는 것이다.
- 책임 할당 과정은 일종의 트레이드오프 활동이다.
- 동일한 문제를 해결할 수 있는 다양한 책임할당 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다.
- 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다.
01. 책임 주도 설계를 향해 #
- 데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다.
- 데이터보다 행동을 먼저 결정하라
- 협력이라는 문맥 안에서 책임을 결정하라
데이터 보다 행동을 먼저 결정하라 #
- 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.
- 클라이언트 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다.
- 객체는 협력에 참여하기 위해 존재하며 협력 안에서 수행하는 책임이 객체의 존재가치를 증명한다.
- 데이터는 객체가 책임을 수행하는데 필요한 재료를 제공할 뿐이다.
- 너무 이른시기에 데이터에 초점을 맞추면 객체의 캡슐화가 약화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 된다.
- 책임 중심의 설계에서는 “이 객체가 수행해야 하는 책임을 무엇인가"를 결정한 후에 “이 책임을 수행하는 데 필요한 데이터는 무엇인가"를 결정한다.
- 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다.
- 객체지향 설계에서 가장 중요한 것은 적절한 객체에게 적절한 책임을 할당하는 능력이다.
협력이라는 문맥 안에서 책임을 결정하라 #
- 객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다.
- 객체에게 할당된 책임이 협력에 어울리지 않는다면 그 책임은 나쁜 것이다.
- 책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.
- 협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다.
- 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다.
- 메시지가 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것이다.
- 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야한다.
- 메시지 기반의 설계 관점은 클래스 기반의 설계 관점보다 훨씬 유연한 애플리케이션을 만들 수 있게 해준다.
- “메시지를 전송해야 하는데 누구에게 전송해야 하지?”라고 질문하는 것이 첫걸음
- 메시지가 클라이언트의 의도를 표현한다는 사실에 주목하라.
- 클라이언트는 어떤 객체가 메시지를 수신할지 알지 못한다.
- 단지 임의의 객체가 메시지를 수신할 것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다.
- 메시지를 수신하기로 결정된 객체는 메시지를 처리할 ‘책임’을 할당받게 된다.
- 메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대한 어떠한 가정도 할 수 없다.
- 전송자의 관점에서 수신자가 캡슐화되는 것이다.
- 이처럼 처음부터 데이터에 집중하는 데이터 중심의 설계가 캡슈로하에 취약한 반면 협력이라는 문맥 안에서 메시지에 집중하는 책임 중심의 설계는 캡슐화의 원리를 지키기가 훨씬 쉬워진다.
- 책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉬운 이유
책임 주도 설계 #
- 3장의 책임 주도 설계 흐름
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
- 책임 주도 설계의 핵심은 책임을 결정한 후에 책임을 수행하는 객체를 결정하는 것
- 그리고 협력에 참여하는 객체들의 책임이 어느 정도 정리될 때까지는 객체 내부 상태에 관심을 가지지 않는 것이다.
02. 책임 할당을 위한 GRASP 패턴 #
- GRASP(General Responsibility Assignment Software Pattern - 일반적인 책임 할당을 위한 소프트웨어 패턴)
- 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것
도메인 개념에서 출발하기 #
- 설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려 보는 것이 유용하다.
- 도메인 개념들을 책임 할당 대상으로 사용하면 코드에 도메인의 모습을 투영하기 수월하다.
- 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.
- 설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다.
- 단지 우리에게는 출발점이 필요할 뿐이다.
- 중요한 것은 설계를 시작하는 것이니 도메인 개념을 완벽하게 정리하는 것이 아니다.
- 도메인 개념을 정리하는 데 너무 많은 시간을 들이지 말고 빠르게 설계와 구현을 진행하라.
- 올바른 도메인 모델이란 존재하지 않는다.
- 도메인 모델은 도메인을 개념적으로 표현한 것이지만 그 안에 포함된 개념과 관계는 구현의 기반이 되어야 한다.
- 도메인 모델이 구현을 염두에 두고 구조화되는 것이 바람직하며, 반대로 코드의 구조가 도메인을 바라보는 관점을 바꾸기도 한다.
- 올바른 도메인 모델이란 존재하지 않는다. 필요한 것은 도메인을 그대로 투영한 모델이 아니라 구현에 도움이 되는 모델이다.
정보 전문가에게 책임을 할당하라 #
- 책임 주도 설계방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것이다.
- 이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.
- 메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.
- “메시지를 전송할 객체는 무엇을 원하는가?”
- 메시지를 결정하면 메시지에 적합한 객체를 선택해야 한다.
- “메시지를 수신할 적합한 객체는 누구인가?”
- 객체는 상태와 행동을 통합한 캡슐화의 단위이다.
- 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다.
- 객체의 책임과 책임을 수행하는 데 필요한 상태는 동일한 객체 안에 존재해야 한다.
- 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고있는 객체에게 책임을 할당하는 것이다.
- GRASP - INFORMATION EXPERT(정보 전문가) 패턴
- 책임을 수행하는 데 필요한 정보를 가지고 있는 객체에게 할당하라
- 여기서 정보를 ‘알고’있다고 해서 그 정보를 ‘저장’하고 있을 필요는 없음.
- 객체는 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수도 있다.
- 만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야 한다.
- 이 요청이 외부로 전송해야 하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다.
- 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성된다.
높은 응집도와 낮은 결합도 #
- 설계는 트레이드오프 활동이다.
- 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다.
- 따라서 실제로 설계를 진행하다 보면 몇 가지 설계 중 한 가지를 선택해야 하는 경우가 빈번하다.
- 이 경우에 올바른 책임 할당을 위해 INFORMATION EXPERT 패턴 이외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다.
- 높은 응집도(HIGH COHESION) 낮은 결합도(LOW COUPLING)은 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리이다.
창조자에게 객체 생성 책임을 할당하라 #
- 영화 예매 예시의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다.
- 이것은 협력에 참여하는 어떤 객체에게는 Reservation 인스턴스를 생성할 책임을 할당해야 한다는 것이다.
- CREATOR 패턴
- 객체 A를 생성해야 할 때 어떤 객체에게 객체 생성 책임을 할당해야 하는가?
- 아래 조건을 최대한 많이 만족하는 B가 객체 생성 책임을 할당하라.
- B가 A객체를 포함하거나 참조한다.
- B가 A객체를 기록한다.
- B가 A객체를 긴밀하게 사용한다.
- B가 A객체를 초기화하는 데 필요한 데이터를 가지고 있다.(정보 전문가)
- 이미 결합되어 있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 않는다.
- CREATOR패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 한다.
03. 구현을 통한 검증 #
DiscountCondition 개선하기 #
-
변경에 취약한 클래스를 포함하고 있다.
- 변경에 취약한 클래스란 코드를 수정해야 하는 이유가 하나 이상 가지는 클래스이다.
-
응집도가 낮다는 것은 서로 연관성이 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐져 있다는 것을 의미한다.
- 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.
-
각기 다른 변경은 코드에 영향을 미치는 시점이 서로 다를 수 있다.
- DiscountCondition은 서로 다른 이유로, 서로 다른 시점에 변경될 확률이 높다.
-
변경의 이유가 하나 이상인 클래스에는 위험 징후를 또렷하게 드러내는 몇 가지 패턴이 드러난다.
- 첫 번째 방법은 인스턴스 변수가 초기화 되는 시점이다.
- 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다.
- 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은채로 남겨진다.
- 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.
- 두 번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다.
- 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다.
- 반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 응집도가 낮다.
- 첫 번째 방법은 인스턴스 변수가 초기화 되는 시점이다.
변경과 유연성 #
- 설계를 주도하는 것은 변경이다.
- 변경에 대비할 수 있는 방법은 두가지이다.
- 하나는 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계하는 것.
- 다른 하나는 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것이다.
- 대부분의 경우 전자가 좋지만, 유사한 변경이 반복적으로 발생하고 있다면 복잡성이 상승하더라도 유연성을 추가하는 두 번째 방법이 더 좋다.
04. 책임 주도 설계의 대안 #
- 책임과 객체 사이에서 방황할 때 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다.
- 아무것도 없는 상태에서 책임과 협력에 관해 고민하기보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키자.
- 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안된다.
- 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야 한다.
메서드 응집도 #
- 긴 메서드는 다양한 측면에서 코드의 유지보수에 부정적인 영향을 미친다.
- 어떤 일을 수행하는지 한 눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.
- 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.
- 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
- 로직의 일부만 재사용하는 것이 불가능하다.
- 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.
- 긴 메서드는 응집도가 낮기 때문에 이해하기 어렵고 재사용하기도 어려우며 변경하기도 어렵다.
- 응집도가 낮은 메서드는 로직의 흐름을 이해하기 위해 주석이 필요한 경우가 대부분이다.
- 메서드가 명령문들의 그룹으로 구성되고 각 그룹에 주석을 달아야 할 필요가 있다면 그 메서드의 응집도는 낮은 것이다.
- 주석을 추가하는 대신 메서드를 작게 분해해서 각 메서드의 응집도를 높여라.
- 클래스의 응집도와 마찬가지로 메서드의 응집도를 높이는 이유도 변경과 관련이 깊다.
- 응집도 높은 메서드는 변경되는 이유가 단 하나여야 한다.
- 클래스가 작고, 목적이 명확한 메서드들로 구성돼 있다면 변경을 처리하기 위해 어떤 메서드를 수정해야 하는지를 쉽게 판단할 수 있다.
- 메서드의 크기가 작고 목적이 분명하기 때문에 재사용하기도 쉽다.
- 작은 메서드들로 조합된 메서드는 마치 주석들을 나열한 것처럼 보이기 때문에 코드를 이해하기도 쉽다.
- 클래스의 길이가 길어지더라도 일반적으로 명확성의 가치가 그보다 중요하다.
- 코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.
- 동시에 너무 많은 세부사항을 기억하도록 강요하는 코드는 이해하기 어렵다.
- 큰 메서드를 작은 메서드들로 나누면 한 번에 기억해야 하는 정보를 줄일 수 있다.
- 세부적인 정보가 필요하다면 그때 각 메서드의 세부 구현을 확인하면 된다.
객체를 자율적으로 만들자 #
- 어떤 메서드를 어떤 클래스로 이동시켜야 할까?
- 자신이 소유하고 있는 데이터를 자기 스스로처리하도록 만드는 것이 자율적인 객체를 만드는 지름길이다.
- 따라서 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키자.