CH5. 책임 할당하기

의진·2024년 7월 12일

[Book] 오브젝트

목록 보기
2/4

💬 객체지향의 핵심은 적절한 객체에게 적절한 책임을 할당하는 것이다.

  • 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.
  • 데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공할 뿐이다.
    💡 즉, "이 객체가 수행해야 하는 책임은 무엇인가"를 결정한 후에 "이 책임을 수행하는 데 필요한 데이터는 무엇인가"를 결정하자!
    ❗️이때, 책임은 객체의 입장이 아닌 객체가 참여하는 협력(메시지)에 적합하게 결정하자!

하지만 객체에게 어떤 책임을 할당할지를 결정하는 것은 쉽지 않다! 왜냐하면 책임 할당 과정은 일종의 트레이드오프 활동이기 때문이다.
즉, 동일한 문제를 해결할 수 있는 다양한 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다.


💬 책임 주도 설계

1️⃣ 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
2️⃣ 시스템 책임을 더 작은 책임으로 분할
3️⃣ 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임 할당
4️⃣ 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우(스스로 처리할 수 없는 경우) 이를 책임질 적절한 객체 또는 역할 탐색
5️⃣ 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력

🙋🏻‍♀️ 책임을 수행할 수 있는 적절한 객체를 어떻게 찾죠? (책임 할당 패턴)

✔️ 정보 전문가 패턴
: 책임을 수행하는 데 필요한 정보를 가지고 있는 객체에게 할당하라 (책임 할당의 기본적인 원리)
❕이때, 책임을 수행하는 객체가 정보를 '알고'있다고 해서 그 정보를 '저장'하고 있을 필요 ❌

✔️ 낮은 결합도 패턴
: 설계의 전체적인 결합도가 낮게 유지되도록 책임을 할당하라 (의존성↓ 변화에 영향↓ 재사용성↑)

✔️ 높은 응집도 패턴
: 높은 응집도를 유지할 수 있게 책임을 할당하라 (복잡성을 관리할 수 있는 수준으로 유지)

✔️ 창조자 패턴
: 아래의 조건을 최대한 많이 만족하는 B에게 A를 생성할 책임을 할당하라

  • B가 A객체를 포함하거나 참조한다
  • B가 A객체를 기록한다
  • B가 A객체를 긴밀하게 사용한다
  • B가 A객체를 초기화하는 데 필요한 데이터를 가지고 있다

🚨 자주 하는 실수 (if ~ else)

public class Movie {
	private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;
    
    // ...

	private Money calculateDiscountAmount() {
    	switch(movieType) {
          case AMOUNT_DISCOUNT:
              return calculateAmountDiscountAmount();
          case PERSENT_DISCOUNT:
              return calculatePercentDiscountAmount();
          case NONE_DISCOUNT:
              return calculateNoneDiscountAmount();
        }

        throw new IllegalStateException();
    }
}

이 메서드를 가지고 있는 클래스(DicountCondition)은 여러 이유로 변경될 수 있다 (캡슐화❌)
1️⃣ 새로운 할인이 추가되는 경우
2️⃣ 금액 할인의 로직이 변경되는 경우
3️⃣ 퍼센트 할인의 로직이 변경되는 경우

낮은 응집도를 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.

💡 코드를 통해 변경의 이유를 파악할 수 있는 Tip

  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다.
    ❗️변경의 이유를 기준으로 클래스를 분리하라
  • 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다.
    클래스의 속성이 서로 다른 시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거다.
    ❗️초기화 되는 속성의 그룹을 기준으로 클래스를 분리하라
  • 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다.
    반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 응집도가 낮다는 증거다.
    ❗️그룹을 기준으로 클래스를 분리하라

이제 문제를 해결해보자!

1️⃣ 타입 분리하기

public abstract class Movie {
	// ...
    
    abstract protected Money calculateDiscountAmount();
    
    protected Money getFee() {
    	return fee;
    }
}

public AmountDiscountMovie extends Movie {
	private double percent;
    
    @Override
    public Money calculateDiscountAmount() {
    	return discountAmount;
    }
}

public PercentDiscountMovie extends Movie {
	private double percent;
    
    @Override
    public Money calculateDiscountAmount() {
    	return getFee().times(percent);
    }
}

public NoneDiscountMovie extends Movie {
    @Override
    public Money calculateDiscountAmount() {
    	return Money.ZERO;
    }
}

❗️ 할인 정책 구현을 위해 상속을 이용하고 있기 때문에 실행 중에 할인 정책을 변경하기 어렵다!
❗️ 변경하기 위해서는 새로운 인스턴스를 생성한 후 필요한 정보를 복사해야 한다
→ 변경 전후의 인스턴스가 개념적으로는 동일한 객체를 가리키지만 물리적으로 서로 다른 객체이기 때문에 식별자의 관점에서 혼란스러울 수 있다


2️⃣ 상속 대신 합성을 사용하자

public class Movie {
	private DiscountPolicy discountPolicy;
    
    // ...
}

public interface DiscountPolicy {
	Money calculateDiscountAmount();
}
  • 이제 금액할인 정책이 적용된 영화를 비율 할인 정책으로 바꾸는 일은 Movie에 연결된 DiscountPolicy의 인스턴스를 교체하는 단순한 작업으로 바뀐다!

✔️ 다형성 패턴
: 객체의 타입에 따라 변하는 로직이 있을 때, 타입을 명시적으로 정의하고 각 타입에 다형적으로 행동하는 책임을 할당하라!
프로그램을 if ~ else 또는 switch ~ case 등의 조건 논리를 사용해서 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야 한다! 이것은 프로그램을 수정하기 어렵고 변경에 취약하게 만든다!

  • 역할을 사용하면 객체의 구체적인 타입을 추상화할 수 있다!
  • 역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상 클래스
    구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용

✔️ 변경 보호 패턴
: 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화하라


💬 책임 주도 설계애 익숙하지 않다면, 리팩터링을 사용하자!

: 책임 관점에서 사고하기 위해서는 충분한 경험과 학습이 필요하다. 따라서 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하고, 코드를 얻고 난 후 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키자!

✔️ 리팩터링
: 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 변경하는 것

1️⃣ 메서드를 작게 분해해서 각 메서드의 응집도를 높여라

  • 다른 메서드에서 사용될 확률이 높아진다 (재사용성↑)
  • 고수준 메서드를 볼 때 일련의 주석을 읽는 것 같은 느낌이 들게 할 수 있다 (가독성↑)
  • 오버라이딩하는 것도 훨씬 쉽다

❗️ 작은 메서드는 실제로 이름을 잘 지었을 때만 그 진가가 드러나므로, 이름을 지을 때 주의해야 한다

2️⃣ 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키자

  • 메서드를 이동할 때 캡슐화, 응집도, 결합도의 측면에서 이동시킨 메서드의 적절성을 판단해야 한다.
  • Tip) 메서드를 다른 클래스로 이동시킬 때는 인자에 정의된 클래스 중 하나로 이동하는 경우가 일반적이다
profile
📖 오브젝트

0개의 댓글