들어가며
- 앞장에서 책임에 맞춰 설계 할 때 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할 것인지 결정하기가 쉽지않다.
- 책임할당 과정은 트레이드 오프 활동이다.
- 같은 문제에 대한 다양한 책임 할당 방법이 존재한다 그 최선은 상황과 문맥에 따라 달리 판단된다
1. 책임주도 설계를 향해
- 데이터 중심 설계에서 책임 주도 설계로 전환은 2가지 원칙을 따라야한다
- 데이터 보다 행동을 먼저 결정하라
- 협력이라는 문맥 안에서 결정하라
1. 데이터 보다 행동을 먼저 결정하라
- 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.
- 클라이언트 관점에서 객체가 수행하는 행동이란 곧 객체의 책임이다.
- 질문의 순서를 바꾸는 것이 중요하다
- 이 객체게 수행해야하는 책임은 무엇인가 -> 이 책임을 수행해야하는 데이터가 무엇인가
- 즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다
2. 협력이라는 문맥 안에서 결정하라
- 객체에 할당괸 책임의 품질은 협력에 적합한 정도로 결정된다
- 책임이 조금 어색해 보이더라도 협력에 적합하다면 그 책임은 좋은 것이다.
- 책임은 객체 입장이 아니라, 객체가 참여하는 협력에 적합해야한다
- 협력에 적합한 책임이란 메시지 수신자가 아닌 전송자에게 적합한 책임을 의미한다
- 즉 메시지를 결정한 후에 객체를 선택해야한다
- 책임이 조금 어색해 보이더라도 협력에 적합하다면 그 책임은 좋은 것이다.
- 메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대한 어떤 가정도 할 수없다
- 전송자 관점에서는 수신자가 깔끔하게 캡슐화된다
핵심
- 책임을 결정한 후에 책임을 수행할 객체를 결정하는 것이다
2. 책임 할당을 위한 GRASP 패턴
- Craig Laman 은 GRASP로 객체에게 책임을 할당할 때 지침으로 삼을 수있는 원칙들의 집합을 패턴으로 정리한것
1. 도메인 개념에서 출발하기
- 설계를 시작하기 전 도메인에 대한 개략적인 모습을 그려보는것이 유용하다
- 어떤 책임을 할당해야 할 때 가장 먼저 고민해야할 유력한 후보는 도메인 개념이다
- 개념들의 의미와 관계가 완벽할 필요없다 단지 출발점이 필요할 뿐이다.
- 중요한것은 설계를 시작하는 것이 도메인 개념들을 완벽하게 정리하는것이 아님
- 도메인 모델과 구현은 무관한것이 아니다
- 도메인 모델이 구현에 염두에 두고 구조화 되는것이 바람직하고, 반대로 코드 구조가 도메인 바라보는 관점 바꾸기도 한다
- 올바른 도메인 모델은 존재하지 않는다. 필요한것은 구현에 도움되는 모델이다
2. 정보전문가에게 책임을 할당하라
- 메시지는 수신할 객체가 아닌, 전송할 객체의 의도를 반영해서 결정한다
- 흐름
- 메세지를 전송할 객체는 무엇을 원하는가? -> 메시지 결정 -> 메시지를 수신할 적합한 객체는 누구인가? -> 자율적인 존재인 '정보전문가' 에게 할당
- 정보전문가? (자율적인 존재)
- 책임을 수행할 정보를 알고있는 객체
- 이 패턴을 따르면 정보와 행동을 최대한 가까운 곳에 위치 시킨다
- 일반적인 직관을 표현한 것이다.
- 이 경우 정보와 데이터는 다르다 데이터를 반드시 저장할필요없다
- 저장 할 필요없고, 정보를 제공할 수있는 다른객체를 알고있거나 필요한 정보를 계산해서 제공가능
- 스스로 처리할 수 없는 작업이 있으면 외부에 도움을 요청해야하고, 이것이 새로운 메시지가 된다
- :Screening -> :Movie -> :DiscountCondition
3. 높은 응집도와 낮은 결합도
- 설계는 트레이드 오프 활동이다
- 책임을 할당 할 때 다양한 대안이 존재한다면, 응집도와 결합도 측면에서 더 나은 선택이 좋다.
- Low Coupling 패턴
- 어떻게 의존성을 낮추고 변화에 영향을 줄이며 재사용성을 증가시킬 수 있을까?
- Moive 와 DiscountCondition 이 이미 결합중인데 Screening 이 DiscountCondition 와 협력할 경우 새로운 결합도가 생긴다
- 결국 Moive 와 DiscountCondition 결합이 더 나은 설계 대안
- High Cohesion 패턴
- 어떻게 복잡성을 관리할 수있는 수준으로 유지할 수있는가?
- 이 2가지는 책임과 협력의 품질을 검토하는데 중요한 기준이다.
4. 창조자에게 객체 생성 책임을 할당
- 협력의 최종 결과물은 Reservation 인스턴스 생성이다.
- CREATOR 패턴은 객체를 생성할 책임을 어떤 객체에게 할당할지 지침을 제공한다
- CREATOR 패턴
- 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡긴다
- 이미 결합돼 있는 객체게 생성책임 할당은 전체적인 결합도에 영향을 미치지 않는다. -> 즉 낮은 결합도 유지가 가능하다
5. 다형성을 통한 분리 (아래 Polymorphism 패턴)
- 조건에 따른 변화는 if-else, switch-case 등의 조건 논리로 해야하는데, 이것은 프로그램을 수정하기 어렵고 변경에 취약하게 만든다
- Polymorphism 패턴은 객체의 타입을 검사해서, 타입에 따라 대안을 수행하는 조건 논리가 X
- 다형성을 이용해 새로운 변화를 다루기 쉽게 확장하기
- Polymorphism 패턴은 객체의 타입을 검사해서, 타입에 따라 대안을 수행하는 조건 논리가 X
6. 변경으로부터 보호하기
3. 구현을 통한 검증
"예매하라" 메시지
- Screening 의 의도를 표현하고, Movie 내부 구현에 대한 지식이 없어도 메시지로 결정했다
"계산하라" 메시지
- DiscountCondition 에 요청한다
- DiscountCondition 개선
- 가장 큰 문제는 변경에 취약한 클래스(수정해야 하는 이유가 하나이상)를 포함하고있다.
- 새로운 할인 조건 추가 시 -> 조건문 추가 + DiscountCondition 속성 추가
- 순번 조건 판단하는 로직 변경 시 -> isSatisfiedBySequence 메서드 내부 구현 수정필요
- 기간 조건을 판단하는 로직 변경 시 -> isSatisfiedByPeriod 메서드 내부 구현 수정필요
- 가장 큰 문제는 변경에 취약한 클래스(수정해야 하는 이유가 하나이상)를 포함하고있다.
- 하나 이상의 변경을 가지므로 응집도가 낮다 -> 연관성 없는 기능이나 데이터가 뭉쳐져 있음
- 변경의 이유에 따라 클래스 분리해야한다
클래스 응집도 판단하기 (중요)
DiscountCondition 내 isSatisfiedBySequence, isSatisfiedByPeriod 는 서로 다른 이유로 서로 다른 시점에 변경될 확률이 높음
- 그러므로 설계 개선 시 변경의 이유가 하나 이상인 클래스 찾는것으로 시작하면 좋다
- 방법 1 : 인스턴스 변수가 초기화 되는 시점 살펴보기
- 일부만 초기화하고 일부는 초기화되지 않으면 응집도가 낮음
- 따라서 함께 초기화 되는 속성을 기준으로 코드 분리해야한다
- 방법 2 : 메서드들이 인스턴스 변수를 사용하는 방식 살펴보기
- 메서드들의 속성에 따라 그룹이 나뉘면 응집도가 낮음
- 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리한다
- 방법 3 : 클래스가 하나이상의 이유로 변경된다면 응집도가 낮은 것 (추가)
- 변경의 이유를 기준으로 클래스 분리
DiscountCondition 의 분리 : SequenceCondition, PeriodCondition 따로 두기
- 하지만 문제가 있음
- 문제 1 : Movie 클래스가 SequenceCondition, PeriodCondition 클래스 양쪽에 결합하게 됨
- 문제 2 : 새로운 할인 조건을 추가하기가 더 어렵다
- 그 결과 응집도는 높아졌지만, 변경과 캡슐화 관점에서 설계의 품질이 나빠짐
- 하지만 문제가 있음
다형성과 변경 보호
다형성을 통한 분리 (Polymorphism 패턴)
- Movie 입장에서는 할인여부 판단이 누구인지 중요하지 않음
- 2개 다 동일한 책임임 -> Movie가 역할에만 결합되도록 의존성 제한
- 객체의 타입에 따라 변하는 행동이 있다면, 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하라
변경으로 부터 보호하기 (Protected Variations 패턴)
- DiscountCondition 으로 캡슐화가 된 경우 Movie가 영향받지 않는다.
- 이 경우 변화가 에상되는 불안정한 지점들을 식별하고, 안정된 인터페이스를 형성하도록 책임을 할당할 것
정리
- 하나의 클래스가 여러 타입의 행동을 구현하고 있는 것 같다면 클래스를 분리하고, Polymorphism 패턴에 따라 책임을 분산
- 예측가능한 변경으로 인해 여러 클래스가 불안정해 진다면 Protected Variations 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화 하라
변경과 유연성
설계를 주도하는 것은 변경인데 변경에 대비하는 2가지 방법
- 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계
- 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만들기
- 이 중 가급적 두번째 방법이 더 좋다
만약에 영화 설정 정책 중 실행 중 변경할수있다는 요구사항이 있는 경우
- Movie 에서 '상속' 방식으로 기존에 있었음 -> 개념적으로는 동일한 객체이지만, 물리적으로 서로 다른 객체이다
- 매번 이 방식은 번거롭고 오류 발생하기 쉽다
할인 정책의 변경을 쉽게 수용할 수 있게 코드를 유연하게 만드는것이 더 좋다 -> 방법은 '합성'
- Movie 의 상속 안에 있던 할인정책을 독립적인 DiscountPolicy 로 분리 후 Movie에 합성시키면된다
- 이러면 Moive 생성자에 DiscountPolicy 인스턴스만 교체하면된다
코드의 구조가 도메인의 구조에 대한 새로운 통찰력을 제공한다
- 코드의 구조가 바뀌면 도메인에 대한 관점도 바뀌게 된다.
- 도메인 모델은 단순히 도메인 개념과 관계 뿐만 아니라 구현과 밀접한 관계가 필요하다
4. 책임 주도 설계의 대안
메서드 분리
책임 주도 설계는 노력과 시간 필요하다. 하지만 최대한 빠르게 목적한 기능을 수행하는 코드 작성이 필요하다
- 일단 실행되는 코드를 얻고나서, 코드상에 드러나는 책임을 올바른 위치로 이동시키기가 필요하다
주의 : 코드 수정 후 겉으로 드러나는 동작이 바뀌면 안된다
- 즉 캡슐화 향상, 응집도 높이고, 결합도 낮추면서 -> 동작은 그대로 유지
- 결국 '리팩터링'
- 리팩터링 : 이해하기 쉽고 수정하기 쉬운 SW로 개선하기 위해, 겉으로 보이는 동작은 바꾸지 않은 채 내부구조를 변경하는 것
기존 ReservationAgency.reserve() : Monster method 라고 부른다
- 응집도 낮은 메서드로 분해가 필요하다
- 메서드가 어떤 일을 하는지 한눈에 알아 볼 수있다 (목적)
- 전체적인 흐름을 이해하기 쉽다 (흐름)
- 각각의 메서드는 하나의 이유로 변경된다 (변경)
하지만 여전히 ReservationAgency 의 응집도는 낮다
클래스 분리
- 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는것이 지름길이다
- 메스드안에서 어떤 클래스의 접근자 메서드를 이용하는지 파악하기
- 이후 Polymorphism, Protected Variations 패턴 고려하기
결론
- 책임주도 설계가 익숙하지 않다면, 데이터 중심으로 구현한 후 이를 리팩터링 해도 유사한 결과를 얻을 수있다.
'서적 > Object' 카테고리의 다른 글
| 9장 유연한 설계 (6) | 2026.03.04 |
|---|---|
| 6장 메시지와 인터페이스 / 8장 의존성 관리하기 (8) | 2026.02.24 |
| 4장 설계품질과 트레이드 오프 (3) | 2026.02.03 |
| 2장 객체지향 프로그래밍 / 3장 역할, 책임, 협력 (4) | 2026.01.29 |
| 1장 객체, 설계 (1) | 2026.01.21 |