서적/Object

5장 책임 할당하기

Mo_bi!e 2026. 2. 8. 14:43

들어가며

  • 앞장에서 책임에 맞춰 설계 할 때 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할 것인지 결정하기가 쉽지않다.
  • 책임할당 과정은 트레이드 오프 활동이다.
  • 같은 문제에 대한 다양한 책임 할당 방법이 존재한다 그 최선은 상황과 문맥에 따라 달리 판단된다

1. 책임주도 설계를 향해

  • 데이터 중심 설계에서 책임 주도 설계로 전환은 2가지 원칙을 따라야한다
    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
      • 다형성을 이용해 새로운 변화를 다루기 쉽게 확장하기

6. 변경으로부터 보호하기

3. 구현을 통한 검증

"예매하라" 메시지

  • Screening 의 의도를 표현하고, Movie 내부 구현에 대한 지식이 없어도 메시지로 결정했다

"계산하라" 메시지

  • DiscountCondition 에 요청한다
  • DiscountCondition 개선
    • 가장 큰 문제는 변경에 취약한 클래스(수정해야 하는 이유가 하나이상)를 포함하고있다.
      1. 새로운 할인 조건 추가 시 -> 조건문 추가 + DiscountCondition 속성 추가
      2. 순번 조건 판단하는 로직 변경 시 -> isSatisfiedBySequence 메서드 내부 구현 수정필요
      3. 기간 조건을 판단하는 로직 변경 시 -> isSatisfiedByPeriod 메서드 내부 구현 수정필요
  • 하나 이상의 변경을 가지므로 응집도가 낮다 -> 연관성 없는 기능이나 데이터가 뭉쳐져 있음
    • 변경의 이유에 따라 클래스 분리해야한다

클래스 응집도 판단하기 (중요)

  • DiscountCondition 내 isSatisfiedBySequence, isSatisfiedByPeriod 는 서로 다른 이유로 서로 다른 시점에 변경될 확률이 높음

    • 그러므로 설계 개선 시 변경의 이유가 하나 이상인 클래스 찾는것으로 시작하면 좋다
    1. 방법 1 : 인스턴스 변수가 초기화 되는 시점 살펴보기
      • 일부만 초기화하고 일부는 초기화되지 않으면 응집도가 낮음
      • 따라서 함께 초기화 되는 속성을 기준으로 코드 분리해야한다
    2. 방법 2 : 메서드들이 인스턴스 변수를 사용하는 방식 살펴보기
      • 메서드들의 속성에 따라 그룹이 나뉘면 응집도가 낮음
      • 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리한다
    3. 방법 3 : 클래스가 하나이상의 이유로 변경된다면 응집도가 낮은 것 (추가)
      • 변경의 이유를 기준으로 클래스 분리
  • DiscountCondition 의 분리 : SequenceCondition, PeriodCondition 따로 두기

    • 하지만 문제가 있음
      • 문제 1 : Movie 클래스가 SequenceCondition, PeriodCondition 클래스 양쪽에 결합하게 됨
      • 문제 2 : 새로운 할인 조건을 추가하기가 더 어렵다
      • 그 결과 응집도는 높아졌지만, 변경과 캡슐화 관점에서 설계의 품질이 나빠짐

다형성과 변경 보호

다형성을 통한 분리 (Polymorphism 패턴)

  • Movie 입장에서는 할인여부 판단이 누구인지 중요하지 않음
    • 2개 다 동일한 책임임 -> Movie가 역할에만 결합되도록 의존성 제한
    • 객체의 타입에 따라 변하는 행동이 있다면, 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하라

변경으로 부터 보호하기 (Protected Variations 패턴)

  • DiscountCondition 으로 캡슐화가 된 경우 Movie가 영향받지 않는다.
    • 이 경우 변화가 에상되는 불안정한 지점들을 식별하고, 안정된 인터페이스를 형성하도록 책임을 할당할 것

정리

  • 하나의 클래스가 여러 타입의 행동을 구현하고 있는 것 같다면 클래스를 분리하고, Polymorphism 패턴에 따라 책임을 분산
  • 예측가능한 변경으로 인해 여러 클래스가 불안정해 진다면 Protected Variations 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화 하라

변경과 유연성

  • 설계를 주도하는 것은 변경인데 변경에 대비하는 2가지 방법

    1. 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계
    2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만들기
    • 이 중 가급적 두번째 방법이 더 좋다
  • 만약에 영화 설정 정책 중 실행 중 변경할수있다는 요구사항이 있는 경우

    • Movie 에서 '상속' 방식으로 기존에 있었음 -> 개념적으로는 동일한 객체이지만, 물리적으로 서로 다른 객체이다
    • 매번 이 방식은 번거롭고 오류 발생하기 쉽다
  • 할인 정책의 변경을 쉽게 수용할 수 있게 코드를 유연하게 만드는것이 더 좋다 -> 방법은 '합성'

    • Movie 의 상속 안에 있던 할인정책을 독립적인 DiscountPolicy 로 분리 후 Movie에 합성시키면된다
    • 이러면 Moive 생성자에 DiscountPolicy 인스턴스만 교체하면된다

코드의 구조가 도메인의 구조에 대한 새로운 통찰력을 제공한다

  • 코드의 구조가 바뀌면 도메인에 대한 관점도 바뀌게 된다.
    • 도메인 모델은 단순히 도메인 개념과 관계 뿐만 아니라 구현과 밀접한 관계가 필요하다

4. 책임 주도 설계의 대안

메서드 분리

  • 책임 주도 설계는 노력과 시간 필요하다. 하지만 최대한 빠르게 목적한 기능을 수행하는 코드 작성이 필요하다

    • 일단 실행되는 코드를 얻고나서, 코드상에 드러나는 책임을 올바른 위치로 이동시키기가 필요하다
  • 주의 : 코드 수정 후 겉으로 드러나는 동작이 바뀌면 안된다

    • 즉 캡슐화 향상, 응집도 높이고, 결합도 낮추면서 -> 동작은 그대로 유지
    • 결국 '리팩터링'
      • 리팩터링 : 이해하기 쉽고 수정하기 쉬운 SW로 개선하기 위해, 겉으로 보이는 동작은 바꾸지 않은 채 내부구조를 변경하는 것
  • 기존 ReservationAgency.reserve() : Monster method 라고 부른다

    • 응집도 낮은 메서드로 분해가 필요하다
    1. 메서드가 어떤 일을 하는지 한눈에 알아 볼 수있다 (목적)
    2. 전체적인 흐름을 이해하기 쉽다 (흐름)
    3. 각각의 메서드는 하나의 이유로 변경된다 (변경)
  • 하지만 여전히 ReservationAgency 의 응집도는 낮다

클래스 분리

  • 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는것이 지름길이다
    • 메스드안에서 어떤 클래스의 접근자 메서드를 이용하는지 파악하기
  • 이후 Polymorphism, Protected Variations 패턴 고려하기

결론

  • 책임주도 설계가 익숙하지 않다면, 데이터 중심으로 구현한 후 이를 리팩터링 해도 유사한 결과를 얻을 수있다.