오브젝트 : 코드로 이해하는 객체지향 설계 (조영호 저) 를 읽고 정리합니다.
GRASP 패턴으로 알아보는 책임 할당 기준
어떤 객체에게 어떤 책임을 할당해야할까? 🧐
1️⃣ 책임 주도 설계 리마인드
- 객체의 책임과 협력에 초점 맞추기
- 데이터보다 행동을 먼저 결정할 것
: 협력에 참여하기 위해, 외부에 제공하는 행동(= 협력 속에서 수행할 책임)을 먼저 결정해야 한다.
- 협력이라는 문맥 안에서 책임을 결정할 것
: 책임은 객체가 아닌 객체가 참여하는 협력에 적합해야 한다.
: 메시지를 전송하는 클라이언트의 의도에 적합하도록, 메시지를 결정한 후 객체를 선택해야 한다.
- 책임 주도 설계의 흐름
- 책임을 결정한 후, 책임을 수행할 객체를 결정하기
- 시스템이 사용자에게 제공해야 하는 기능, 전체 시스템 책임 파악
- 시스템 책임을 더 작은 책임으로 분할
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임 할당
- 객체가 책임을 수행하는 중 다른 객체 도움이 필요한 경우, 해당 도움을 수행할 적절한 객체 또는 역할 찾기
- 찾은 객체 또는 역할에게 책임 할당 = 두 객체의 협력 관계 구축 완료
2️⃣ GRASP 패턴?
일반적인 객체 책임 할당을 위한 소프트웨어 패턴.
👣 도메인 개념에서 출발하기
- 설계를 시작할 때, 도메인에 대한 개략적인 모습을 그려보면 좋다.
- 도메인에 존재하는 개념들을 책임 할당의 대상으로 사용하자.
- 이때, 도메인을 완벽하게 정리하는 것에 집착하지 말자!
- 단순히 출발점의 목적으로 활용할 것
- 책임을 할당받을 객체들의 종류와 관계에 대한 정보를 수집하는 선으로 족하다.
- 올바른 도메인 모델?
- 그런건 없다..! 단지 구현에 도움이 되는 실용적인 모델이 필요할 뿐이다.
🦾 정보 전문가에게 책임 할당하기
- 책임 수행에 필요한 메시지를 결정하자.
- 메시지를 전송할 객체의 의도를 반영하여 아래 질문을 생각하며 메시지를 결정해야 한다.
- 메시지를 전송할 객체는 뭘 원할까?
- 메시지를 수신할 객체는 누가 적당할까?
- 메시지 수신 객체는 캡슐화의 단위에서 결정해야 한다.
- INFORMATION EXPERT (정보 전문가) 패턴
- 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하자.
정보 전문가 패턴
- 객체가 자신이 소유한 정보와 관련된 작업을 수행하는 직관
- 이때, 정보 전문가는 데이터를 반드시 저장할 필요가 없다는 점에 주의
- 정보를 제공할 수 있는 다른 객체를 알거나, 필요한 정보를 직접 계산해서 제공할 수 있기 때문
- 따라서 상태와 행동을 함께 가지는 단위(=객체)를 책임 할당 관점에서 표현한다.
⛑ 높은 응집도, 낮은 결합도
- 위 과정대로 했는데, 동일한 기능을 구현할 수 있는 여러 설계 선택지가 있다면?
- 응집도를 높이고 결합도를 낮게 만들 수 있는 설계를 선택하자.
- 높은 응집도 패턴 (HIGH COHESION)
- 어떤 협력 관계를 만들어야 각 객체의 변경 책임을 분리할 수 있을까?
- 낮은 결합도 패턴 (LOW COUPLING)
- 어떤 협력 관계를 만들어야 설계 전체적으로 결합도를 추가하지 않을 수 있을까?
⚒ 창조자에게 객체 생성 책임 할당하기
- 창조자 패턴 (CREATOR)
- 객체 A를 생성할 때, 아래 조건을 최대한 많이 만족하는 B에게 책임을 할당하라
- B가 A 객체를 포함하거나 참조한다
- B가 A 객체를 기록한다
- B가 A 객체를 긴밀하게 사용한다
- B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다 (= B는 A에 대한 정보 전문가 )
구현을 통해 설계 검증하기
설계가 협력과 책임을 잘 반영했는지 검증하기 위해서 코드를 작성하고 실행해봐야 한다.
1️⃣ 클래스 응집도 판단하기
메세지가 객체를 선택해야 한다!
- 객체가 협력하는 객체의 내부 구현에 대해 지식이 없을 때, 각 객체의 깔끔한 캡슐화가 가능하다.
- 객체간 연결고리가 메시지 뿐이기 때문에, 메시지가 변경되지 않는 한 내부 구현은 영향을 받지 않기 때문
- 변경에 취약한 클래스가 존재하는가?
- 변경에 취약한 클래스 : 코드를 수정해야 하는 이유가 하나 이상인 클래스
- 높은 응집도를 위해 변경의 이유에 따라 클래스를 분리해야 한다.
- 따라서 설계 개선 작업은 변경의 이유가 여러개인 클래스가 있는지 확인하면서 시작하면 좋다.
- 변경의 이유를 파악하는 방법
- (1) 인스턴스 변수 초기화 시점 파악
: 클래스 속성이 서로 다른 시점에 초기화되거나 Or 일부만 초기화된다 = 응집도가 낮다
: 함께 초기화되는 속성을 기준으로 코드를 분리하자!
- (2) 메서드들이 인스턴스 변수를 사용하는 방식 파악
: 메서드가 객체의 모든 속성을 사용할 때 클래스 응집도가 높다.
: 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리하자22!
2️⃣ 설계 개선하기
⛑ 타입 분리하기
- 독립적인 타입은 서로 다른 클래스로 분리하자
- 자신의 인스턴스 변수를 모두 함께 초기화 가능
- 클래스에 있는 모든 메서드는 동일한 인스턴스 변수 그룹을 사용
타입 분리로 인해 발생할 수 있는 문제들 🔨
- 특정 클래스 응집도는 높아져도, 변경과 캡슐화 관점에서 보면 전체 설계 품질은 나빠지는 케이스
- 전체 설계의 결합도가 높아질 수 있다
- 새로운 기능을 추가하기 위해 여러 클래스에 접근해야 한다
⛑ 다형성을 통해 분리하기
- 역할을 이용해 객체 타입을 추상화하자
- 동일한 책임을 수행한다 = 동일한 역할을 수행한다. 따라서 역할에 대해 결합하게 의존성을 제한하자
- 역할을 대체할 클래스들 사이의 관계에 따라 추상화 방법을 골라보자 (Java)
- (1) 클래스들 사이서 구현을 공유해야 함 = 추상 클래스
- (2) 구현 공유할 필요 없이 책임만 정의하면 됨 = 인터페이스
- POLYMORPHISM (다형성) 패턴
- 객체 타입에 따라 변하는 행동이 있다면? 타입을 분리하고, 변하는 행동을 각 타입의 책임으로 할당하자
- if-else가 아니라, 객체의 타입에 따라 변화를 컨트롤하는 방식
⛑ 변경으로부터 보호하기
- PROTECTED VARIATIONS (변경 보호) 패턴
- 변경을 캡슐화하도록 책임을 할당하는 방식
- 변화가 예상되는 불안정한 지점들을 클래스로 인터페이스를 이용해 변경을 캡슐화한다
캡슐화를 이용해 설계 결합도와 응집도 향상하기
(1) 하나의 클래스가 여러 타입의 행동을 구현하는 것 같다 = 다형성 패턴으로 책임 분산
(2) 예측 가능한 변경으로 인해 클래스들이 불안정해진다 = 변경 보호 패턴으로 인터페이스를 이용한 캡슐화
3️⃣ 변경과 유연성
클래스가 작고 한 가지 일만 수행할 때, 책임이 적절하게 분배되어 있을 때, 다른 클래스와 느슨하게 결합되어 있을 때 변경에 유연한 코드를 만들 수 있다.
- 개발자가 변경에 대비하는 두 가지 방법
- (1) 코드를 수정하기 쉽도록 최대한 단순하게 설계하기
- (2) 코드를 수정하지 않고도 변경을 수용할 수 있게 유연하게 설계하기
- 유연성은 요소들 사이의 의존성에 의해 결정된다
책임 주도 설계의 대안
적절한 책임을 찾는게 어렵다면, 일단 코드부터 짜고 이후 리팩토링을 통해 구조를 개선해보자.
- 리팩토링 (Refactoring)
- 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 개선(변경)하는 것
1️⃣ 메서드 응집도
- 긴 메서드 = 몬스터 메서드 (monster method)
- 문제
- 코드의 전체적인 컨텍스트 파악이 오래 걸린다
- 코드 변경할 때 수정해야 할 부분 찾기가 어렵다
- 메서드 내부 일부 로직을 수정했을 때, 다른 로직이 영향을 받을 가능성이 크다
- 로직의 일부를 재사용하는 것이 불가능하다
- 코드 재사용을 위해 복붙하게 되어, 코드 중복이 발생하기 쉽다
- 요약
- 응집도가 낮아서 보기도 어렵고 쓰기도 어렵고 재활용도 어렵고 어쨌든 모든게 어려워진다!
- 주석이 주렁주렁 필요하다? 높은 확률로 메서드 응집도가 낮은 경우!
- 메서드를 작게 분해해서 응집도를 높이자. 작은 메서드는 일종의 주석과 같다
- 메서드를 작고 응집도 높은 메서드로 분리할 때, 각 메서드를 적절한 클래스로 이동 (책임 할당)하기가 더 쉬워진다.
2️⃣ 자율적인 객체 만들기
- 메서드가 사용하는 데이터를 가지고 있는 클래스로 메서드를 이동시키자
- 자율적인 객체 = 데이터를 처리하기 위해 타 객체에게 의존할 필요가 없는 객체 = 내가 가진 데이터를 스스로 처리하는 객체
- 메서드를 이동할 때는 캡슐화, 응집도, 결합도의 측면에서 해당 이동이 적절한지 판단한다.