본문으로 건너뛰기

Object

1. 절차지향

  • 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라고 부른다.(26p)
  • 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터 역할만 수행한다.(26p)

1.1 절차지향의 문제점

  • 절차적 프로그래밍의 세상에서 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어렵다.(26p)
  • 모든 처리가 하나의 클래스에 위치하기 때문에 여러 데이터 클래스 중 하나만 변경되더 라도 변경이 처리 클래스의 변경을 부른다.(26p)
  • 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계입니다.(27)
    • 절차적 프로그래밍은 프로세스가 필요한 모든 데이터에 의존해야 한다는 근본적인 문제점 때문에 변경에 취약할 수밖에 없습니다.

1.2 해결방안

  • 자신의 데이터는 스스로 처리하도록 프로세스를 이동시켜야 합니다.
  • 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라고 부릅니다.(27)
    • 비록 이 관점이 객체지향 구현 관점에서만 바로본 지극히 편현합 관점이지만 객체지향 프로그래밍의 본질을 실용적으로 설명하는 것은 사실입니다.
    • 이 문서를 읽고 나면 객체지향이 단순히 데이터와 프로세스를 하나의 객체 안으로 모으는 것 이상의 것이라는 것을 알 수 있습니다.

2. 객체지향 프로그래밍

  • 비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향의 시계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다.(33)
    • 레베카 워프스브록은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)라고 부른다.
    • 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.
    • 그 대상이 비록 실세계에서는 생명이 없는 수동적인 존재라고 하더라도 객체지향의 시계에 들어오면 능동적이고 자율적인 존재로 바뀌게 된다.
  • 보통 객체지향 프로그래밍을 작성할 때 가장 먼저 고려하는 것은 어떤 클래스가 필요한지 고민한다.(40p)
    • 하지만 진정한 객체지향은 클래스가 아닌 객체에 초점을 맞출 때 얻을 수 있다.
    • 즉 어떤 클래스가 필요한지가 아니라 어떤 객체가 필요한지 고민해야 합니다.

2.1 왜 객체지향인가?

  • 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계입니다.(35)
  • 객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공하여 좋은 설계를 만들 수 있도록 돕습니다.
    • 이로 인해 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여줍니다.

2.2 객체지향 설계

  • 객체지향 프로그램을 작성할 때 가장 먼저 고려하는 것은 어떤 클래스가 필요한지 고민할 것입니다.
  • 대부분의 사람들이 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민합니다.
  • 하지만 진정한 객체지향은 클래스가 아닌 객체에 초점을 맞출 때 얻을 수 있습니다.
  • 이를 위해서는 두가지에 집중해야 합니다.
    • 어떤 클래스가 필요한지 고민하기 전에 어떤 객체들이 필요한지 고민하라
      • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것
      • 클래스의 윤곽을 잡기 위해서는 객체들이 어떤 상태와 행동을 가져야 하는지 고민해야 합니다.
    • 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다.

3. 컴파일 시간 의존성과 실행 시간 의존성(59p)

  • 코드의 의존성과 실행 시점의 의존성이 다를 수 있습니다.
  • 다시 말해 클래스 사이의 의존성과 객체 사이의 의존성이 다를 수 있습니다.
  • 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워집니다.
    • 코드를 이해하기 위해서는 코드뿐 아니라 객체를 생성하고 연결하는 부분을 이해해야 하기 때문입니다.
    • 반면 코드는 더 유연해지고 확장이 가능합니다. 즉 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여줍니다.
      • 설계가 유연해질수록 코드를 이해하고 디버깅하기 어려워지며 반면 유연성을 억제하면 코드를 이해하고 디버깅하기 쉬워지지만 확장 가능성이 낮아진다는 사실을 기억해야 합니다.
    • 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아닙니다. 이것이 객체지향 설계가 어려운 이유입니다.

4. 다형성

  • 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는 성질을 다형성이라고 부른다.(63p)
    • 메시지: 객체의 오퍼레이션이 실행되도록 요청하는 것을 메시지 전송이라고 합니다.
    • 메서드: 메시지에 응답하기 위해 실제로 실행되는 코드 블록을 의미합니다.
  • 다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실에 기반합니다.
  • 다형적 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 합니다.(63)
    • 따라서 인터페이스가 동일해야 합니다.
  • 다형성을 구현하는 방법은 다양합니다.
    • 대표적으로 상속을 이용하면 다형성을 구현할 수 있습니다.

4.1 다형성의 종류

  • 다형성은 크게 유니버셜 다형성과 임시 다형성으로 구분할 수 있습니다.(390)
  • 유니버셜 다형성은 다시 매개변수 다형성과 포함 다형성으로 구분할 수 있습니다.(390)
  • 임시 다형성은 다시 오버로딩 다형성과 강제 다형성으로 구분할 수 있습니다.(390)
  • 매개변수 다형성(391)
    • 제네릭 프로그래밍과 관련이 있습니다.
    • 클래스의 인스턴스 변수다 메서드의 매개변수 타입을 임의로 선언한 후 사용 시점에 구체적인 타입으로 지정하는 방식입니다.
  • 강제 다형성(391)
    • 언어가 지원하는 자동적입 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 적용할 수 있는 방식입니다.
    • 예를 들어 + 이항 연산자를 사용할 때 피연산자가 모두 정수면 정수 덧셈을 수행하고, 하나는 정수고 하나는 문자열인 경우 문자열 연결을 수행하는 방식입니다.
  • 오버로딩 다형성(391)
    • 메서드 오버로딩을 사용하여 유사한 작업을 수행하는 메서드의 이름을 통일하고 다른 매개변수 타입이나 개수를 사용하여 다형성을 구현하는 방식입니다.
    • 따라서 기억해야하는 메서드 이름이 줄일 수 있습니다.
  • 포함 다형성(391)
    • 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행하는 행동이 달라지는 능력을 포함 다형성이라고 부릅니다.
    • 포함 다형성은 서브 타입 다형성이라고도 부릅니다.
    • 포함 다형성은 다형성 중 가장 일반적이고 널리 사용되는 형태입니다.
    • 따라서 특별한 언급없이 다형성이라고 할 때는 포함 다형성을 의미합니다.

5. 상속

  • 상속은 DRY 원칙을 지킬 수 있는 수단이다.
  • 하지만 설계는 트레이드오프 활동이다. 상속은 코드의 재사용을 위해 캡슐화를 희생한다.
  • 부모 클래스의 변경에 의해 자식 클래스가 영향 받는 현상을 취약한 기반 클래스 문제라고 부른다.(323)
    • 상속은 자식 클래스르 점진적으로 추가해서 기능 확장하는데 용이하지만 높은 결합도로 부모 클래스를 점진적으로 개선하는 것은 어렵게 만든다.
    • 최악의 경우 모든 자식 클래스를 수정하고 테스트해야 한다.
    • 상속은 코드 재사용을 위해 캡슐화의 장점을을 희석시킨다.

5.1 상속의 단점

  • 상속은 캡슐화를 위반합니다.(70)
    • 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알아야 합니다.
  • 다른 하나는 설계를 유연하지 못하게 만듭니다.(70)
    • 상속은 부모 클래스와 지식 클래스 사이의 관계를 컴파일 시점에 결정합니다.
    • 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능합니다.
    • 반면 인스턴스 변수로 연결한 경우 실행 시점에 객체의 종류를 변경할 수 있습니다.
    • 예를 들어, Movie에 DiscountPolicy를 변경할 수 있는 changeDiscountPolicy 메서드를 추가하면 실행 시점에 DiscountPolicy를 변경할 수 있습니다.

5.2 상속과 결합도

  • 상속은 부모 클래스와 자식 클래스 사이에 강한 결합을 만들어낸다.
  • 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다.
  • 이것은 자식 클래스 작성자가 부모 클래스의 구현 방법에 대한 정확한 지식을 가져야 한다는 것을 의미한다.
  • 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들어 캡슐화를 약화시킨다.

5.3 취약한 기반 클래스

  • 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 의미한다.
  • super
    • 자식 클래스에서 super의 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 결합도가 강해진다.
    • super 호출을 제거할 수 있는 방법을 찾아보자.

5.4 불필요한 인터페이스 상속 문제

  • 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다
  • 자바 초기에 상속을 잘못 사용한 대표적인 사례 java.util.Stack을 보자
  • Stack은 나중에 추가된 요소가 가장 늦게 추출되는 LIFO 자료 구조인 스택을 구현한 클래스다.
  • Stack은 Vector를 재사용하기 위해 Stack을 Vector의 자식 클래스로 구현했다.
  • 그러나 Vector에는 임의의 위치에서 요소를 조회하고, 추가하고, 삭제할 수 있는 퍼블릭 인터페이스가 존재한다.
  • 따라서 Stack에게 상속된 Vector의 퍼블릭 인터페이스를 이용해 임의의 위치에서 요소를 추가하고 삭제해 Stack의 규칙을 쉽게 위반할 수 있다.
Stack<String> stack = new Stack<String>();
stack.push("1st");
stack.push("2nd");
stack.push("3rd");
stack.add(0, "4th"); // 임의의 위치에 원소를 삽입하는 것이 가능하다.

5.5 메서드 오버라이딩 오작용 문제

  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제
  • 책에서는 HashSet은 상속받은 InstrumentedHashSet을 예로 들었다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;

public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}
@Test
@DisplayName("InstrumentedHashSet 테스트")
void testAddAll() {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));
Assertions.assertThat(s.getAddCount()).isEqualTo(6);
}

5.6 부모 클래스와 자식 클래스의 동시 수정 문제

  • 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 한다.

5.7 업캐스팅

  • 상속을 이용하면 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐지기 때문에 부모 클래스의 인스턴스에 전송할 수 있는 메시지를 자식 클래스의 인스턴스에도 전송할 수 있다.(405)
    • 컴파일러는 명시적인 타입 변환 없이도 자식 클래스가 부모 클래스를 대체할 수 있게 허용합니다.
    • 모든 객체지향 언어는 명시적으로 타입을 변환하지 않고도 부모 클래스의 참조 변수에 자식 클래스의 인스턴스를 할당할 수 있게 허용합니다. 이를 업캐스팅이라고 부릅니다.
    • 업캐스팅으로 인해 미래의 자식 클래스들도 협력에 참여할 수 있게 됩니다.
  • 반대로 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위해서는 명시적인 타입 캐스팅이 필요합니다. 이를 다운캐스팅이라고 부릅니다.(405)

5.8 동적 바인딩

  • 함수를 호출하는 전통적인 언어들은 호출될 함수는 컴파일 시점에 결정된다.(407)
    • 이처럼 컴파일 타임에 호출한 함수를 결정하는 방식을 정적 바인딩, 초기 바인딩, 또는 컴파일타임 바인딩이라고 부른다.
  • 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 실행 시점에 결정된다.(407)
    • foo.bar() 라는 코드를 읽는 것만으로는 실행된느 bar가 어떤 클래스의 어떤 메서드인지를 판단하기 어렵다.
    • foo가 가리키는 객체자 실제로 어떤 클래스의 인스턴스인지 bar 메서드가 해당 클래스의 상속 계층의 어디에 위치하는지를 알아야 한다.
    • 이처럼 실행될 메서드를 런타임에 결정하는 방식을 동적 바인딩 또는 지연 바인딩이라고 부른다.
  • 동적 메서드 탐색 과정
    • 객체지향 시스템은 아래 규칙에 따라 실행할 메서드를 결정한다.(408)
    • 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 확인한다. 존재하면 해당 메서드를 실행하고 탐색을 종료한다.
    • 메서드를 찾지 못했다면 부모 클래스에 적합한 메서드가 존재하는지 확인한다. 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 따라 계속해서 반복된다.
    • 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 찾지 못했다면 해당 메시지를 처리할 수 없다는 예외를 발생시키며 탐색을 종료한다.
    • 정적 타입 언어에서는 코드를 컴파일할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단하여 컴파일 에러를 발생시킵니다.(419)
  • 메서드 탐색은 자식 클래스에서 부모 클래스 방향으로 진행됩니다.(409)
    • 따라서 항상 자식 클래스의 메서드가 부모 클래스의 메서드보다 더 높은 우선순위를 갖습니다.
    • 메시지를 수신했을 때 실제 어떤 메서드를 실행할지 결정하는 것은 런타임에 결정되며, 메서드를 탐색하는 경로는 self 참조를 이용해서 결정합니다.
    • self 참조가 Lecture 클래스의 인스턴스라면 Lecture 클래스에 정의된 메서드를 먼저 탐색을 시작해서 Object에서 종료됩니다.(416)
    • 만약 self 참조가 GradeLecture 클래스의 인스턴스라면 GradeLecture 클래스에 정의된 메서드를 먼저 탐색을 시작해서 Lecture, Object 순으로 탐색을 진행합니다.
    • 동일한 코드라 할지라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변합니다.

5.9 self 대 super

  • 대부분의 객체지향 언어들은 자식 클래스에서 부모 클래스의 인스턴스 변수나 메서드에 접근하기 위해 사용할 수 있는 super 참조라는 내부 변수를 제공합니다.(422)
    • 대부분의 사람들은 super.evaluate()라는 문장이 단순히 부모 클래스의 evaluate 메서드를 호출하는 것이라고 생각합니다.
    • 하지만 super는 부모 클래스의 evaluate 메서드를 호출하는 것이 아니라 더 상위에 위치한 조상 클래스의 메서드일 수도 있습니다.
    • super 전송은 부모 클래스에서부터 메서드 탐색을 시작하게 합니다.
    • super의 용도는 지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요입니다.
  • self 전송은 메세지를 수신하는 객체의 클래스에 따라 탐색할 시작 위치를 동적으로 결정합니다.(424)
    • 하지만 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작됩니다.
    • self 전송은 어떤 클래스에서 메시지가 탐색이 시작될지 알지 못합니다.
    • super의 경우는 항상 해당 클래스의 부모 클래스에서부터 메서드 탐색을 시작합니다.
    • 따라서 self의 경우 런타임에 동적으로 결정되고 super의 경우 컴파일 시점에 미리 결정됩니다.

5.10 상속을 위한 경고

  • 자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스가 강하게 결합된다.(322)
    • super 호출을 제거할 수 있는 방법을 찾아 결합도를 낮춰야 한다.
  • 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨뜨릴 수 있다.(326)
  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.(328)
  • 클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수밖에 없다.(332)

5.11 불필요한 인터페이스 상속 문제

  • 자바 초기에 상속을 잘못 사용한 대표적인 예시로 java.util.Stack 클래스가 있다.(324)
  • Stack 클래스는 Vector 클래스를 상속받아 구현되었다. 따라서 Vector의 퍼블릭 인터페이스를 그래도 상속받는다.
  • Stack은 가장 나중에 추가된 요소가 가장 먼저 제거되는 LIFO(Last In First Out) 구조를 가지지만 Vector는 임의의 위치에 요소를 추가하거나 제거할 수 있는 기능을 제공한다.
  • 따라서 Stack에 상속된 Vector의 퍼블릭 인터페이스를 이용하면 임의의 위치에서 요소를 추가하거나 제거할 수 있다.
  • 이는 Stack의 규칙을 위반하는 것이다.

5.12 메서드 오버라이딩의 오작용 문제

  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받을 수 있습니다.
  • 이펙티브 자바 Item18 상속보다는 컴포지션을 사용하라 참고
    • java.util.HashSet 클래스를 상속받은 InstrumentedHashSet 클래스를 예로 메서드 오버라이딩의 오작용 문제를 설명합니다.

5.13 부모 클래스와 자식 클래스의 동시 수정 문제

  • 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해 부모 클래스와 자식 클래스의 동시 수정이 필요할 수 있습니다.
  • 조슈아 블로치는 이펙티브 자바에서 HashSet의 구현에 강하게 결합된 InstrumentedHashSet 클래스를 소개했습니다.
  • InstrumentedHashSet은 HashSet을 상속받아 요소가 추가될 때마다 카운트를 증가시키는 기능을 추가했습니다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
InstrumentedHashSet<String> languages = new InstrumentedHashSet<>();
languages.addAll(Arrays.asList("Java", "Kotlin", "Scala"));
  • 위 코드의 실행 결과는 3이 될거라 예상하지만 실행 결과는 6이 됩니다.
  • 그 이유는 부모 클래스인 HashSet의 addAll 메서드가 메서드 안에서 add 메서드를 호출하기 때문입니다.
  • 문제를 해결하기 위해 addAll 메서드를 오버라이딩을 제거해야 합니다.
  • 하지만 이 방법도 완벽한 해결책이 아닙니다.
  • 나중에 HashSet 클래스의 addAll 메서드가 add 메시지를 호출하지 않는 방식으로 변경된다면 InstrumentedHashSet 클래스는 더 이상 동작하지 않을 것입니다.
  • 미래 수정을 감안한다면 addAll 메서드를 오버라이딩하고 추가되는 각 요소에 대해 한 번씩 add 메서드를 호출하는 방법이 있습니다.
  • 하지만 이 방법은 addAll 메서드의 구현이 HashSet 클래스의 addAll 메서드 구현가 동일하다는 문제가 존재합니다.
  • 미래 발생할지 모르는 위험 방지를 위해 코드를 중복시킨 것입니다.
  • 클래스가 상속되기 원하다면 상속을 위해 클래스를 설계하고 문서화 해야 하며, 그렇지 않으면 상속을 금지해야 합니다.(328)
    • 내부 구현을 문서화 하면 객체지향의 핵심이 구현을 캡슐화하는 것인데 이를 위반하는게 아닌가 의문이 들 수 있습니다.
    • 맞습니다. 상속은 결국 캡슐화를 위반함으로써 코드 재사용을 가능하게 하는 것입니다.
    • 설계는 트레이드오프 활동입니다.

5.14 상속의 목적

  • 상속을 이용해 자식 클래스를 추가하려 한다면 아래 질문을 스스로에게 던져보세요.(389)
    • 상속을 사용하려는 목적이 단순히 코드를 재사용하기 위한 것인가?
    • 클라이언트 관점에서 인스턴스들을 동리하게 행동하는 그룹으로 묶기 위한 것인가?
    • 만약 첫 번째 질문에 대한 답이 '예'라면 상속을 사용하지 말아야 한다.
  • 객체지향 패러다임 초기에는 상속은 타입 계층과 다형성을 구현할 수 있는 유일한 방법이였습니다.(389)
    • 여기에 상속을 사용하면 코드를 재사용할 수 있다는 과대광고가 더해져 상속을 남용하게 되었습니다.
    • 지금은 상속 이외에도 다형성을 구현할 수 있는 방법이 많아졌습니다.
    • 따라서 상속에 중요성이 많이 낮아졌습니다.
  • 상속을 사용하는 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이여야 합니다.(435)
    • 상속은 코드를 재사용하는 방법을 제공하지만 부모 클래스와 자식 클래스를 강하게 결합시켜 설계의 변경과 진화를 방해합니다.

5.15 언제 상속을 사용해야 하는가?

  • 아래와 같은 질문을 해보고 모두 '예'라고 답할 수 있다면 상속을 사용하라고 조언합니다.(443)
    • 상속 관계가 is-a 관계를 모델링 하는가?: 일반적으로 자식 클래스는 부모 클래스다라고 말해도 이상하지 않다면 상속을 사용할 예비 후보로 생각할 수 있습니다.
    • 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?: 클라이언트는 입장에서 자식 클래스와 부모 클래스의 차이를 몰라야 합니다. 이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 합니다.

6. 합성

  • 347p
  • 상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.
  • 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.
  • 상속에서 부모 클래스와 자식 클래스 사이의 의존성은 컴파일타임에 해결되지만 합성은 두 객체 사이의 의존성은 런타임에 해결된다.
  • 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.

6.1 상속과 비교

  • 합성은 구현에 의존하지 않는다는 점에서 상속과 다르다.
    • 상속은 자식이 부모의 내부 구현에 대해 상세히 알아야하기 때문에 강한 결합이 생긴다.
  • 상속은 내부에 포함된 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다.
    • 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 하는 것이 가능하다. (72)
    • 따라서 객체의 내부 구현이 변경되어도 영향을 최소화할 수 있다.
  • 따라서 코드 재사용을 위해 객체 합성이 클래스 상속보다 더 좋은 방법이다.
    • 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하여 결합도를 느슨하게 한다.
    • 상속은 클래스를 통해서 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨하게 결합된다.(72)
  • 코드 재사용 관점에서 상속을 화이트 박스 재사용이라고 하며 합성을 블랙박스 재사용이라 한다.

6.2 불필요한 인터페이스 상속 문제 해결

  • 앞서 보았던 Stack을 합성을 이용해 다시 구현해 보자.
public class Stack<E> {
private Vector<E> elements = new Vector<>();

public E push(E item) {
elements.addElement(item);
return item;
}


public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}

}
  • 상속을 사용한 기존 Stack 대신 Vector를 포함한다.
  • 상속에서는 불필요한 인터페이스 상속 문제 때문에 임의 위치의 원소를 삭제하거나 삽입하는게 가능했지만 합성을 이용하면 그런 불필요한 인터페이스를 가지지 않는다.

6.3 메서드 오버라이딩 오작용 문제 해결

  • 예제의 InstrumentedHashSet을 상속에서 합성으로 변경하면 메서드 오버라이딩 오작용 문제를 해결할 수 있다.
  • InstrumentedHashSet이 내부에 HashSet을 인스턴스로 가지고 HashSet의 퍼블릭 인터페이스만 사용해서 InstrumentedHashSet을 구현하면 된다.

해결 코드

public class InstrumentedHashSet<E> {
private int addCount = 0;
private Set<E> set;

public InstrumentedHashSet(Set<E> set) {
this.set = set;
}

public boolean add(E e) {
addCount++;
return set.add(e);
}

public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}

public int getAddCount() {
return addCount;
}

}
  • 하지만 InstrumentedHashSet은 HashSet의 모든 퍼블릭 인터페이스를 그대로 제공해야 한다.
  • 따라서 아래와 같은 코드를 작성한다.
public class InstrumentedHashSet<E> implements Set<E> {
private int addCount = 0;
private Set<E> set;

public InstrumentedHashSet(Set<E> set) {
this.set = set;
}

@Override
public boolean add(E e) {
addCount++;
return set.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}

public int getAddCount() {
return addCount;
}

@Override
public Spliterator<E> spliterator() {
return Set.super.spliterator();
}

@Override
public int size() {
return set.size();
}

@Override
public boolean isEmpty() {
return set.isEmpty();
}

@Override
public boolean contains(Object o) {
return set.contains(o);
}

@Override
public Iterator<E> iterator() {
return set.iterator();
}

@Override
public Object[] toArray() {
return set.toArray();
}

@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}

@Override
public boolean remove(Object o) {
return set.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}

@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}

@Override
public void clear() {
set.clear();
}
}
  • Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드 안에서 set 인스턴스에게 동일한 메서드를 그대로 전달한다.
    • 이를 포워딩이라고 부른다.
  • 포워딩은 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경할 때 유용한 기법이다.

6.4 상속과 합성의 차이점

  • 상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.(346)
  • 상속은 부모 클래스와 자식 클래스 사이의 의존성이 컴파일 타임에 해결되지만 합성에서 두 객체 사이의 의존성은 런타임에 해결된다.(346)
    • 이 차이점은 생각보다 중요하다. 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하다. 합성 관계는 실행 시점에 동적으로 변경이 가능해 변경하기 쉽고 유연한 설계를 얻을 수 있다.(347)
  • 상속은 is-a 관계를 표현하는 반면 합성은 has-a 관계를 표현한다.(346)
  • 코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.(347)
    • 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있다.
    • 다시 말해 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있다.
  • 상속을 화이트박스 재사용이라 부르고 합성은 블랙박스 재사용이라고 부른다.(347)
    • 화이트박스 재사용은 부모 클래스의 내부 구조를 알고 있어야 한다. 따라서 부모 클래스의 변경이 자식 클래스에 영향을 미칠 수 있다.
      • 블랙박스 재사용은 부모 클래스의 내부 구조를 알 필요가 없고 인터페이스를 통해 부모 클래스와 협력한다. 따라서 부모 클래스의 변경이 자식 클래스에 영향을 미치지 않는다.

6.5 합성으로 변경하기

  • Vector를 상속받는 Stack을 합성으로 변경하여 불필요한 인터페이스 상속문제를 해결할 수 있습니다.(348)
public class Stack<E> {
private final Vector<E> elements = new Vector<>();

public E push(E item) {
elements.add(item);
return item;
}

public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
  • 이제 Stack 불필요한 Vector의 오퍼레이션들이 포함되지 않습니다.
  • 클라이언트 이제 임의의 위치에 요소를 추가하거나 제거할 수 없습니다.
  • 합성 관계로 변경함으로써 클라이언트가 Stack을 사용할 때 Stack의 규칙을 위반하는 행동을 할 수 없게 되었습니다.

6.6 클래스 폭발

  • 상속 관계는 컴파일타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다. (368)
    • 따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하면 모든 조합 가능한 경우별로 클래스를 추가해야한다.
    • 이것이 바로 핸드폰 과금 시스템의 설계 과정에서 직면했던 클래스 폭발 문제입니다.
  • 컴파일타임 의존성과 런타임 의존성의 거리가 멀수록 설계가 유연해집니다.(368)
    • 상속은 컴파일타임 의존성과 런타임 의존성을 동일하게 만들겠다고 선언하는 것입니다.
    • 따라서 클래스 폭발 문제를 해결하려면 상속을 피하고 합성을 이용해야 합니다.

7. 역할, 책임, 협력

  • 객체지향 패러다임의 관점에서 핵심은 역할, 책임, 협력입니다.(73)
  • 클래스와 상속은 객체들의 책임과 협력이 어느 정도 자리를 잡은 후에 사용할 수 있는 구현 메커니즘입니다.(73)
    • 다분히 구현 측면에 치우져 있기 때문에 객체지향 패러다임의 본질과는 거리가 멀다.(73)

8. 협력

  • 객체지향 시스템은 자율적인 객체들의 공동체다.(75)
  • 협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용이다.(97)
    • 협력이란 어떤 객체가 다른 객체에게 무엇인가를 요청하는 것
  • 메시지 전송은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다.(49p)
    • 메시지를 수신한 객체는 스스로의 결정에 따라 메서드를 실행해 요청에 응답한다.
    • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드라고 부른다.
  • 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화하는 것이다.(76)
    • 캡슐화를 통해 변경에 대한 파급효과를 제한할 수 있습니다.
  • 자율적인 객체는 자신에게 할당된 책임을 수행하는 중에 필요한 정보를 알지 못하거나 외부의 도움이 필요한 경우 적절한 객체에게 메시지를 전송해서 협력을 요청한다.

8.1 협력이 설계를 위한 문맥을 결정한다

  • Move 객체는 어떤 행동을 수행해야 될까? 영화라는 단어만 들었을 때 대부분 사람들은 극장에서 영화를 상영하는 장면을 상상하고 자연스럽게 play라는 행동을 수행할 것이라고 생각한다. (77)
    • 그러나 예매 시스템에서 영화를 상영하기 위한 어떤 코드도 포함돼있지 않다.
    • 그 이유는 영화 예매를 위한 협력에 참여하고 있기 때문이다.
    • 협력이라는 문맥을 고려하지 않고 Movie의 행동을 결정하는 것은 의미가 없다.

7 책임

  • 책임이란 객체에 의해 정의되는 응집도 있는 행위의 집합이다.
  • 즉 객체의 책임은 무엇을 알고 있는가무엇을 할 수 있는가로 구성된다.
  • 무엇을 알고 있는가
    • 사적인 정보에 관해 아는 것
    • 관련된 객체에 관해 아는 것
    • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
  • 무엇을 할 수 있는가
    • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
    • 다른 객체의 행동을 시작시키는 것
    • 다른 객체의 활동을 제어하고 조절하는 것

7.1 책임 할당: Information Expert

  • 자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는데 필요한 정보를 가장 잘 알고있는 전문가에게 그 책임을 할당하는 것이다.(81)
    • 이를 Information Expert(정보 전문가) 패턴이라고 부른다.

8. 역할

  • 객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 부른다.(86)
  • 역할을 구현하는 가장 일반적인 방법은 추상 클래스인터페이스를 사용하는 것이다.
  • 협력 관점에서 추상 클래스와 인터페이스는 구체 클래스들이 따라야하는 책임의 집합을 서술한 것이다.

8.1 역할과 협력

  • 영화 예매 협력에서 예매하라라는 메시지를 처리하기에 적합한 객체로 Screening을 선택했다.(86)
  • 하나의 단계처럼 보이지만 실제로는 두 개의 독립적인 단계가 결합된 것이다.
    • 첫 번째 단계는 영화를 예매할 수 있는 적절한 역할이 무엇인가 찾는다.
    • 두 번째 단계는 역할을 수행할 객체로 Screening을 선택하는 것이다.
  • 왜 역할이라는 개념을 이용해서 설계 과정을 더 번거롭게 만드는 것일까?
    • 아래에서 그 답을 알 수 있다.

8.2 유연하고 재사용 가능한 협력

  • 역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문이다.(87)

8.3 역할의 구현

  • 역할을 구현하는 가장 일반적인 방법은 추상 클래스인터페이스를 사용하는 것이다.(89)
  • 협력 관점에서 추상 클래스와 인터페이스는 구체 클래스들이 따라야하는 책임의 집합을 서술한 것이다.
    • 추상 클래스는 책임의 일부를 구현해 놓은 것이고 인터페이스는 책임의 집합만을 나열해 놓았다는 차이가 있다.
  • 역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상 클래스를 사용하고 없다면 인터페이스를 사용하면 된다.(156)

8.4 객체 대 역할

  • 역할은 객체가 참여할 수 있는 일종의 슬롯이다.(90)
  • 그러나 오직 한 종류의 객체만 협력에 참여한는 상황에서 역할이라는 개념을 고려하는 것이 유용할까?
  • 만약 협력에 적합한 책임을 수행하는 대상이 한 종류라면 간단하게 객체로 간주한다. 만약 여러 종류의 객체들이 참여할수 있다면 역할이라고 부르면 된다.
  • 대부분의 경우 어떤 것이 역할이고 어떤 것이 객체인지가 또렷하게 드러나지 않을 것이다.(91)
    • 특히 설계 초반에는 결정을 내리기 더욱 어렵다.
  • 애매하다면 단순하게 객체로 시작하고 반복적으로 책임과 협력을 정제해가면서 필요한 순간에 객체로부터 역할을 분리하는 것이 가장 좋은 방법이다.

9. 객체지향 설계 예시

  • 지금부터 영화 예매 시스템을 예로 들어 설명을 하겠다.(82)
  • 시스템이 사용자에게 제공할 기능은 영화를 예매하는 것입니다.
  • 예매하라라는 이름의 메시지로 협력을 시작하는 것이 좋을 것 같습니다.
  • 메시지를 선택했으면 메시지를 처리할 적절한 객체를 찾아야 합니다.
  • 기본 전략은 정보 전문가에게 책임을 할당하는 것입니다.
  • 영화를 예매하기 위해서는 상영 시간과 기본 요금을 알아야한다.
  • 이 정보의 전문가는 누구인가? 바로 Screeing입니다.
  • 안타깝게도 Screeing은 예매 가격을 계산하는데 필요한 충분한 정보를 가지고 있지 않습니다.
    • 예매에 대해서는 전문가이지만 가격 자체에 대해서는 전문가가 아닙니다.
    • 따라서 Screening 외부 객체에게 가격 계산을 요청해야 합니다.
  • 가격을 계산하라라는 새로운 메시지가 필요하게 됩니다.
    • 가격을 계산하기 위해서는 가격과 할인 정책이 필요합니다.
    • 따라서 이 모든 정보를 가진 전문가는 Movie입니다.
    • Movie 객체에게 예매 가격을 계산하는 책임을 할당해야 합니다.
    • 가격을 계산하기 위해서는 할인 요금이 필요하지만 Movie는 할인 요금에 대한 정보를 가지고 있지 않습니다.
    • 따라서 Movie는 외부 객체에게 할인 요금을 계산하는 데 필요한 요청을 외부에 전송해야 합니다.
  • 할인 요금을 계산하라라는 새로운 메시지가 필요하게 됩니다.
  • 이런 방식으로 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복적인 과정으로 통해 설계를 완성할 수 있습니다.
  • 이렇게 결정된 메시지가 객체의 퍼블릭 인터페이스가 됩니다.

9.1 책임 주도 설계

  • 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방식을 책임 주도 설계라고 부른다.(83)
  • 이전 예시에서 영화 예매 시스템을 설계하는 과정에서 책임 주도 설계를 적용했습니다.
  • 책임 주도 설계 방법의 과정은 아래와 같습니다.
    • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
    • 시슴템 책임을 더 작은 책임으로 분할한다.
    • 분한될 책임을 수행할 수 있는 적잘한 객체 또는 역할을 찾아 책임을 할당한다.
    • 책임을 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾아 책임을 찾는다.
    • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
  • 데이터 중심 설계에서 책임 중심 설계로 전환하기 위해 아래 두 가지 원칙을 따라야 한다(134)
    • 데이터보다 행동을 먼저 결정하라
    • 협력이라는 문맥 안에서 책임을 결정하라
  • 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.(134)
    • 클라이언트 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다.
    • 데이터는 객체가 책임을 수행하는데 필요한 재료를 제공할 뿐이다.

9.2 데이터 주도 설계와 비교: 행동이 상태를 결정한다.

  • 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정하는 방식으로 설계를 진행하는 방법을 데이터 주도 설계라고 부른다.(85)
  • 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해합니다.
  • 캡슐화를 위반하지 않으려면 구현에 대한 결정을 뒤로 미루면서 객체의 행위를 고려하기 위해서 항상 협력이라는 문맥 안에서 객체를 바라봐야 합니다.
  • 데이터 중심 설계에서 흔히 볼 수 있는 패턴은 아래와 같습니다.(100)
    • 예를 들어 Movie 객체의 종류를 저장하는 인스턴스 변수(movieType)를 두고 그에 따라 행동을 결정하는 방식
    • 인스턴스의 종류에 따라서 배타적으로 사용될 인스턴스 변수를 하나의 클래스 안에 함께 포함시키는 방식

10 메시지

10.1 메시지가 객체를 결정한다

  • 객체에게 책임을 할당하는 데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택하는 것이 중요합니다.(84)
  • 메시지가 객체를 선택해야 하는 이유
    • 최소한의 인터페이스
      • 필요한 메시지가 식별될 때까지 객체의 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에 객체는 꼭 필요한 크기의 퍼블릭 인터페이스만을 가지게 된다.
    • 추상적인 인터페이스
      • 객체의 인터페이스는 무엇을 하는지 표현해야 하지만 어떻게 수행하는지는 노출해서는 안 된다.
      • 메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무엇을 수행할지에 초첨을 맞추는 인터페이스를 얻을 수 있다.

10.2 메시지와 메시지 전송

  • 메시지
    • 메시지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다.(177)
    • 메시지는 오퍼레이션명과 인자로 구성된다. 여기에 메시지 수신자를 포함하면 메시지 전송이 된다.
  • 메시지 전송
    • 한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송이라고 부른다.(177)
    • 메시지 전송은 메시지 수신자, 오퍼레이션 명, 인자의 조합이다.
  • 메시지 전송자: 이 때 메시지를 전송하는 객체를 메시지 전송자라고 부른다.(177)
  • 메시지 수신자: 메시지를 수신하는 객체를 메시지 수신자라고 부른다.(177)
  • 클라이언트-서버 모델에서 메시지 전송자를 클라이언트라고 부르고 메시지 수신자를 서버라고 부른다.(177)

10.3 메시지와 메서드

  • 메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇인가에 달려있다.(178)
  • 이처럼 메시지를 수신했을 때 실제로 수행되는 함수 또는 프로시저를 메서드라고 부른다.(178)
  • 전통적인 방식의 개발자는 어떤 코드가 실행될지를 정확하게 알고 있는 상황에서 함수 호출이나 프로시저 호출 구문을 작성한다.(178)
    • 다시 말해 코드의 의미가 컴파일 시점과 실행 시점에 동일하다.
    • 반면 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야하기 때문에 컴파일 시점과 실행 시점의 의미가 다를 수 있다.
  • 메시지와 메서드의 구분은 메시지 전송자와 수신자가 느슨한게 결합될 수 있도록 해준다.(179)
    • 전송자는 자신이 어떤 메시지를 전송해야 하는지만 알고 있으면 된다.
    • 수신자가 어떤 클래스의 인스턴스인지, 어떤 방식으로 요청을 처리하는지 모르더라도 원할한 협력이 가능하다.
정보

클래스를 결정하고 그 클래스의 책임을 찾아 나서는 대신 메시지를 결정하고 이 메시지를 누구에게 전송할지 찾아보게 되었다. 클래스 기반 설계에서 메시지 기반 설계로의 바꿈은 우리가 해오던 설계 활동의 전환점이다. 메시지 기반의 설계 관점은 클래스 기반의 설계관점보다 훨씬 유연한 애플리케이션을 만들 수 있게 해준다. "이 클래스가가 필요하다는 점은 알겠는데 이 클래스는 무엇을 해야하지"라고 질문하지 않고 "메세지를 전송해야 하는데 누구에게 전송하지"라고 질문하는 것. 설계의 핵심 질문을 이렇게 바꾸는 것이 메시지 기반 설계로 향하는 첫 걸음이다. 객체를 가지고 있기 때문에 메시지를 보내는 것이 아니다. 메시지를 전송하기 때문에 객체를 갖게 된 것이다.

11 캡슐화, 응집도, 결합도

  • 캡슐화
    • 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다.(109)
    • 객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.
  • 응집도와 결합도는 변경과 깊은 관련이 있다.
  • 어떤 설계를 쉽게 변경할 수 있다다는 것은 높은 응집도와 낮은 결합도를 가지고 있다는 뜻이다.

12 캡슐화

  • 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다.(109)
  • 객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.
  • 캡슐화의 정도가 응집도와 결합도에 영향을 미친 다는 사실을 강조하고 싶다.(112)
  • 높은 응집도와 낮은 결합도를 얻기 위해서는 캡슐화가 핵심이다.
    • 따라서 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상 시키기 위해 노력해야한다.(112)
    • 객체는 자신이 어떤 데이터를 가지고 있는지 내부에 캡슐화하고 외부에 공개해서는 안된다.
    • 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 공개하면 캡슐화를 위반한 것이다.

12.1 캡슐화 위반

  • 데이터 중심으로 설계한 Movie 클래스를 보면 오직 메서드를 통해서만 객체 내부 상태에 접근할 수 있습니다.(113)
  • 안타깝게도 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.(113)
    • getFee 메서드는 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.(115)
  • 설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있습니다.(114)
  • 앨런 홀럽은 이처럼 접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략이라고 부른다.(114)
    • 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.
    • 따라서 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출될 수밖에 없는 것이다. 그 결과 캡슐화를 위반하게 된다.

13 응집도

  • 응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.(110)
  • 응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.(111)

13.1 응집도 측정

  • 응집도는 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있습니다.(110)
  • 하나의 변경을 수용하기 위해 모듈 전체가 변경되면 응집도가 높고 모듈의 일부만 변경되면 응집도가 낮은 것입니다.(110)
  • 하나의 변경을 수용하기 위해 하나의 모듈만 변경된다면 응집도가 높고 다수의 모듈이 함께 변경돼야 한다면 응집도가 낮은 것입니다.(110)

13.2 응집도의 높고 낮음

  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다. 변경의 이유를 기준으로 클래스를 분리한다.
  • 클래스 안에서 변경의 이유를 찾는 것이 생각보다 어렵다.(152)
  • 희망적인 소식은 변경의 이유가 하나 이상인 클래스에는 위험 징후를 또렷하게 나타내는 몇 가지 패턴이 존재한다.
    • 인스턴스 변수가 초기화되는 시점을 살펴보는 것이다.
      • 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화 한다.(152)
      • DiscountCondition이 순서 조건을 표현하는 경우 sequence는 초기화 되지만 dayOfWeek, startTime, endTime은 초기화 되지 않는다.(152)
      • 반대로 DiscountCondition이 기간 조건을 표현하는 경우 dayOfWeek, startTime, endTime은 초기화 되지만 sequence는 초기화 되지 않는다.(152)
      • 따라서 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.
    • 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다.
      • 모든 메서드가 모든 속성을 사용한다면 응집도가 높다고 볼 수 있다.(152)
      • DiscountCondition의 isStisfiedBySequence 메서드와 isSatisfiedByPeriod 메서드가 이 경우에 해당한다.(153)
      • isStatisfiedBySequence 메서드는 sequence를 사용하고 isSatisfiedByPeriod 메서드는 dayOfWeek, startTime, endTime을 사용한다.(153)
      • 응집성을 높이기 위해서는 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다.
  • 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화 한다.
  • 모든 메서드가 모든 인스턴스 변수를 사용한다면 응집도가 높다고 볼 수 있다.
  • 153p

13.3 낮은 응집도의 문제점

  • 변경의 사유가 서로 다른 코드를 하나의 모듈 안에 뭉쳐놓아 변경과 상관없는 코드들이 영향을 받게 된다.(116)
  • 하나의 요구사항 변경을 위해 동시에 여러 모듈을 수정해야한다.(116)
    • 응집도가 낮을 경우 다른 모듈에 위치해야할 책임의 일부가 엉뚱한 곳에 위치하기 때문이다.
    • 예를들어 새로운 할인 정책을 추가해야한다고 가정해보자.
    • 이를 위해서 MovieType에 열거형 값을 추가하고 ReservationAgency의 reserve 메서드에 switch 문을 수정해야한다.
    • 또한 할인 정책에 따라 할인 요금을 계산하기 위해 Movie에 필요한 데이터도 추가해야된다.
    • 하나의 요구사항을 반영하기 위해서 Movie, ReservationAgency, MovieType 세 개의 클래스를 수정해야 한다.
      • 이는 응집도가 낮은 설계의 전형적인 예시다.

14 결합도

  • 결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도이다.(110)

14.1 결합도 측정

  • 결합도는 한 모듈의 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.(111)
    • 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함꼐 수정해야 하는지를 나타낸다.
    • 결합도가 높으면 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경이 어려워진다.
  • 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우 두 모듈 사이의 결합도가 높다고 표현한다.(112)
    • 반면 퍼블릭 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우 두 모듈 사이의 결합도가 낮다고 표현한다.

14.1 결합도의 높고 낮음

  • 한 객체가 다른 객체에 대해 알고 있는 정보(지식)의 양으로 결합도가 결정된다.
  • 객체 A가 다른 객체 B의 접근자 메서드를 사용하고 있다면 결합도가 높을 수 있다.
    • 객체는 자율적인 존재여야 한다.
    • A 객체가 수행하고 있는 책임을 데이터를 저장하고 있는 객체 B에게 넘겨주면 결합도를 낮출 수 있다.

14.2 의존성과 결합도

  • 의존성은 두 요소 사이의 관계 유무를 설명합니다.
  • 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현합니다.
    • 결합도가 강하다 또는 결합도가 느슨하다와 같은 표현을 사용합니다.

14.3 추상화에 의존하라

  • 추상화란 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법입니다.(268)
  • 따라서 대상에 대해 알아야하는 지식의 양을 줄여 결합도를 느슨하게 유지할 수 있습니다.(268)
  • 목록에서 아래쪽으로 갈수록 클라이언트가 알아야하는 지식의 양이 적어진다.(268)
    • 구체 클래스 의존성
    • 추상 클래스 의존성
    • 인터페이스 의존성

14.4 명시적 의존성과 숨겨진 의존성

  • 의존성의 대상을 생성자 또는 setter로 전달받는 방식과 생성자 안에서 직접 생성하는 방식의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지에 대한 여부입니다.(270)
  • 이를 명시적 의존성이라고 부릅니다.(270)
  • 반면 Movie 내부에서 AmountDiscountPolicy 인스턴스를 직접 생성하는 방식은 Movie가 DiscountPolicy에 의존한다는 사실은 감춥니다.(270)
  • 이를 숨겨진 의존성이라고 부릅니다.(270)
  • 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 합니다.(271)
  • 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있습니다. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문입니다.(271)
  • 의존성은 명시적으로 표현하는 것이 좋습니다. 퍼블릭 인터페이스를 통해 명시적으로 의존성을 표현하면 컴파일 타임 의존성을 적절한 런타임 의존성으로 교체할 수 있습니다.(271)
    • 그렇다면 설계가 유연하고 재사용 가능해집니다.

14.4 new의 위험성

  • new를 잘못 사용하면 클래스 간의 결합도가 강해집니다.(271)
  • new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 합니다.
    • 이는 추상화에 의존하는 것이 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 강해집니다.
  • new 연산자를 사용하면 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지 알아야 합니다.(271)
    • 즉 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 강해집니다.
    • 구체 클래스를 생성하기 위해 필요한 인자로 사용되는 구체 클래스와의 의존성도 만들어집니다.
    • 예를 들어, Movie 클래스가 AmountDiscountPolicy를 생성하기 위해 AmountDiscountPolicy의 생성자에 필요한 인자를 알아야 합니다.
    • 두 구체 클래스인 SequenceCondition과 PeriodCondition에도 의존하도록 만듭니다.(272)
  • 해결법
    • 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리해야 합니다.(273)
    • 의존성 해결 방법을 이용해 외부에서 인스턴스를 전달받아 사용하고 직접 생성하지 않아야 합니다.(273)
  • new가 항상 위험한 것은 아닙니다.
    • 문제는 객체 생성이 아니라 부적절한 곳에서 객체를 생성하는 것입니다.
    • 유연하고 재사용 가능한 설계를 원한다면 객체에 대한 생성과 사용을 분리해야 합니다.
    • 클래스 안에서 객체의 인스턴스를 직접 생성하는 것이 유용한 경우도 있습니다.
    • 협력하는 기본 객체를 설정하고 싶은 경우가 이러한 경우에 해당합니다.
    • 설계는 트레이드오프 활동입니다.
    • 결합도와 사용성을 저울질해야 합니다.

15. 책임 할당을 위한 GRASP 패턴

  • GRASP(General Responsibility Assignment Software Patterns)의 약자로 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것(137)
  • INFORMATION EXPERT PATTERN

15.1 정보 전문가에게 책임을 할당하라: INFORMATION EXPERT PATTERN

  • 애플리케이션이 제공해야하는 기능을 애플리케이션의 책임으로 생각하고 이 책임을 애플리케이션에 대해 전송되는 메시지로 간주한다.
  • 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.
  • 예를 들어 영화 예매 시스템을 설계할 때 애플리케이션이 제공해야하는 기능은 영화 예매입니다.
  • 이를 책임으로 간주하여 애플리케이션은 영화를 예매할 책임이 있다고 말할 수 있다.
  • 이제 이 책임을 수행하는데 필요한 메시지를 결정해야 한다.
  • 메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.
  • 따라서 첫 번째 질문은 다음과 같다. 메시지를 전송할 객체는 무엇을 원하는가?
  • 따라서 메시지의 이름으로 예매하라가 적절한 같다.
  • 두 번째 질문은 다음과 같다. 메시지를 수신할 적합한 객체는 누구인가?
  • 객체에게 책임을 할당하는 첫 번째 원칙은 수행할 정보를 알고 있는 객체에게 책임을 할당하라는 것이다. GRASP에서 이를 INFORMATION EXPERT 패턴이라고 부른다.
    • 여기서 정보는 데이터와 다르다. 책임을 수행하는 객체가 정보를 알고있다고 해서 그 정보를 저장하고 있을 필요는 없다.
    • 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수 있다.

15.2 낮은 결합도: LOW COUPLING PATTERN

  • 현재의 책임 할당을 검토하거나 여러 설계 대안이 있을 때 낮은 결합도를 유지할 수 있는 설계를 선택하라. (143)

15.3 높은 응집도: HIGH COHESION PATTERN

  • 현재의 책임 할당을 검토하거나 여러 설계 대안이 있을 때 높은 응집도를 유지할 수 있는 설계를 선택하라. (144)
  • 한 요소의 책임들이 얼마나 강력하게 관려되고 집중되어 있는가를 응집도라고 부릅니다.(기초편 4-3)
    • 연관성 높은 책임들만 가지면서 너무 많은 일을 하지 않는 객체에게 책임을 할당하는 것이 좋습니다.(기초편 4-3)

15.4 창조자에게 객체 생성 책임을 할당하라: CREATOR PATTERN

  • 영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다.(144)
  • 이것은 협력에 참여하는 어떤 객체에게는 Reservation 인스턴스를 생성할 책임을 할당해야 한다는 것을 의미한다.
  • 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침
  • 객체 A를 생성할 때 아래의 조건을 최대한 많이 만족시키는 B에게 객체 생성 책임을 할당하라
    • B가 A 객체를 포함하거나 참조한다. (LOW COUPLING 패턴: 이미 결합도가 있는 후보에게 책임을 할당하라)
    • B가 A 객체를 기록한다. (LOW COUPLING 패턴: 이미 결합도가 있는 후보에게 책임을 할당하라)
    • B가 A 객체를 긴밀하게 사용한다. (LOW COUPLING 패턴: 이미 결합도가 있는 후보에게 책임을 할당하라)
    • B가 A 객체를 초기화 하는 데 필요한 데이터를 가지고 있다.(정보 전문가 패턴)
    • 창조자 패턴을 자세히 보면 Low Coupling 패턴과 Information Expert 패턴을 모두 사용하고 있습니다.
  • 이미 결합돼 있는 객체에게 생성 책임을 할당하는 작업은 설계의 전체적인 결합도에 영향을 미치지 않는다.
  • 즉 CREATOR 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 낮은 결합도를 유지할 수 있게 한다.

15.5 다형성 패턴: POLYMORPHISM PATTERN

  • 객체의 암시적 타입에 따라 행동을 분기해야 한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도 문제를 해결할 수 있습니다.(158)
  • 객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하라는 것입니다.(158)
  • GRASP에서 이를 POLYMORPHISM 패턴이라고 부릅니다.(158)
  • 프로그램을 if ~ else 또는 switch 등의 조건 논리를 사용해서 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야 합니다.(158)
    • 객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지 말고 대신 다형성을 사용해 새로운 변화를 다루기 쉽게 확장하라고 권고합니다.

15.6 변경 보호 패턴: PROTECTED VARIATIONS PATTERN

  • 객체, 서브시시텀, 그리고 시스템을 어떻게 설계해야 변화와 불안정성이 다른 요소에 나쁜 영향을 미치지 않도록 방지할 수 있을까?(159)
  • 변화가 예상되는 불안정한 지점들을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하라(159)
  • 우리가 캡슐화해야하는 것은 변경이다. 변경될 가능성이 높은가? 그렇다면 캡슐화하라(159)
  • DiscountConditiond이라는 역할이 Movie로부터 PeriodCondition과 SequenceCondition의 존재를 감춘다.(159)
    • 새로운 DiscountConditiond 타입을 추가하더라도 Movie가 영향받지 않는다는 것을 의미한다.(159)
    • 오직 DiscountConditiond 인터페이스를 실체화하는 클래스를 추가하는 것으로 할인 조건의 종류를 확장할 수 있다.(159)
  • DiscountConditiond 인터페이스를 실체화하는 클래스를 추가하는 것으로 할인 조건의 종류를 확장할 수 있다.(159)

15.7 간접화 패턴: INDIRECTION PATTERN

  • 두 요소 간의 직접적 결합을 피하기 위해 중간 객체 할당하는 패턴입니다.

15.8 순수 가공 패턴: PURE FABRIC PATTERN

  • 모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 문제가 발생할 수 있습니다.(291)
  • 이 경우 도메인 개념을 표현하는 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결할 수 있습니다.(291)
  • 크레이그 리만은 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 순수한 가공물(Pure Fabrication)이라고 부릅니다.(291)
  • 이런 측면에서 객체지향이 실세계의 모방이라는 말은 잘못된 것이다.(292)
  • 객체지향 애플리케이션의 대부분은 실제 도메인에서 발견할 수 없는 순수한 인골물이 더 많은 비중을 차지하는 것이 일반적이다.(292)
  • 먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하라.
    • 만약 도메인 개념이 만족스럽지 못하다면 주저말고 순수한 가공물을 창조하라
    • 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹되지 마라
    • 즉 PURE FABRIC PATTERN은 INFORMATION EXPERT 패턴에 따라 책임 할당 시 낮은 응집도와 높은 결합도를 수반한다면 대안으로 순수 가공물 패턴을 사용할 수 있다.
  • 대부분의 디자인패턴은 PURE FABRIC PATTERN에 해당한다.(293)

15.9 제어 패턴: CONTROLLER PATTERN

  • 시스템 이벤트를 처리하는 첫 번째 객체

16. 퍼블릭 인터페이스와 품질

  • 객체가 협력에 참여하기위해 외부에서 수신할 수 있는 메시지의 묶음(181)
  • 클래스의 퍼블릭 메서드들의 집합이나 메시지의 집합을 가리키는데 사용된다.(181)
  • 객체를 설계할 때 가장 중요한 것은 훌륭한 퍼블릭 인터페이스를 설계하는 것이다.(181)
  • 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙은 아래와 같습니다.
    • 디미터 법칙
    • 묻지 말고 시켜라
    • 의도를 드러내는 인터페이스
    • 명령-쿼리 분리

16.1 디미터 법칙

  • 객체의 내부 구조에 대한 결합으로 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 디미터 법칙이다.(183)
  • 디미터 법칙을 간단히 요약하면 객체 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다.(183)
  • 디미터 법칙은 "낯선 자에게 말하지 말라" 또는 "오직 인접한 이웃하고만 말하라"라고 요약할 수 있다.(183)
  • 자바나 C# 같이 도트.를 이용해 메시지 전송을 표현하는 언어에서는 "오직 하나의 도트만 사용하라"라는 말로 요약할 수 있다.(183)
  • 클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에게만 메시지를 전송하도록 프로그램이 해야한다.(184)
    • this 객체
    • 메서드의 인자
    • this의 속성
    • this의 속성인 컬렉션의 요소
    • 메서드 내에서 생성된 지역 객체
  • 디미터 법칙을 위반하는 대표적인 코드 screeing.getMovie().getDiscounts();
  • 디미터 법칙은 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속산인다.(186)

16.2 묻지 말고 시켜라

  • 메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안 된다.(186)
  • 객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반하는 것이다.(186)
  • 절차지향 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다.(186)
  • 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라(187)

16.3 의도를 드러내는 인터페이스

  • 켄트 벡은 메서드를 명명하는 두 가지 방법을 설명했다.(188)
    • 첫 번째 방법은 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름을 짓는 것이다.
    • 두 번째 방법은 메서드가 "어떻게'가 아니라 "무엇"을 하는지를 드러내는 것이다.
  • 어떻게 수행하는지를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름이다.(189)
  • 이처럼 어떻게 하느냐가 아니라 무엇을 하느냐에따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자라고 부른다.(190)
  • 도메인 주도 설계에서 에릭 에반스는 켄트 벡의 의도를 드러내는 선택자를 인터페이스 레벨로 확장한 의도를 드러내는 인터페이스를 제시 했다.(190)

16.4 원칙의 함정

  • 디미터 법칙과 묻지 말고 시켜라 스타일은 절대적인 법칙은 아니다.(198)
  • 디미터 법칙(198)
    • IntStream.of(1, 15, 20, 3, 9).filter(n -> n > 10).distinct().count();
    • 해당 코드는 디미터 법칙을 위반하는 것이 아니다.
    • of, filter, distinct, count 메서드는 모두 IntStream이라는 동일한 클래스의 인스턴스를 반환한다.
    • IntStream의 내부 구조가 노출됐는가? 그렇지 않다. 단지 IntStream을 다른 IntStream으로 변환했을 뿐이다.
    • 두개 이상의 도트를 사용하는 모든 케이스가 디미터 법칙을 위반하는 것은 아니다.
    • 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 디미터 법칙을 위반하지 않는 것이다.
  • 결합도와 응집도 충돌(200)
    • 묻지 말고 시켜라가 항상 긍정적인 결과로만 귀결되는 것은 아니다.
      • 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스에 안에 어울리지 않는 오퍼레이션이 공존하게 된다.
      • 결과적으로 상관 없는 책임들이 하나의 클래스에 뭉쳐져 응집도가 낮아지게 된다.
      • 예를 들어, PeriodCondition 클래스의 isSatisfiedByPeriod 메서드에서 Screening 객체에게 묻는 메서드로 인해 내부 상태를 가져와 사용해 캡슐화를 위반한 것 처럼 보인다.
        • Screening 객체의 getStartTime 메서드를 호출해 할인 조건을 판단하는 로직을 구현했다. 즉 Screening 객체의 내부 상태를 가져와 사용했다.
      • 따라서 할인 여부를 판단하는 로직을 Screening의 isDiscountable 메서드로 위임하면 묻지 말고 시켜라 스타일이 된다.
      • 이렇게 되면 Screening은 할인 조건을 판단하는 책임을 떠안게 된다.
      • Screening의 본질적인 책임은 영화를 예매하는 것인데 할인 조건을 판단하게 되면 객체의 응집도가 낮아지게 된다.
    • 가끔씩은 묻는 거 외에는 다른 방법이 존재하지 않는 경우도 있다.(201)
      • 컬렉션에 포함된 객체들을 처리하는 유일한 방법은 객체에게 물어보는 것이다.
      • Movie에게 묻지 않고 movies 컬렉션에 포함된 전체 영화의 가격을 계산할수 있는 방법이 있을까?
    • 물으려는 객체가 정말로 데이터인 경우도 있다.(202)
      • 객체는 내부 구조를 숨겨야하지만 자료 구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.
  • 소프트웨어 설계에 법칙이란 존재하지 않는다.(202)
    • 유일한 법칙은 "경우에 따라 다르다"라는 사실을 명심하라

17. 의존성

  • 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재한다고 말한다.(253)
  • 의존성은 방향성을 가지며 항상 단방향이다.(254)
  • 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미합니다.(255)
  • 오브젝트에서 말하는 의존성은 UML에서 말하는 의존 관계와는 다릅니다. 여기서 말하고 있는 의존성은 UML에서 정의하는 모든 관계가 가지는 공통 특성을 의미합니다.
  • 의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있습니다.(16p)
    • 그렇다고 객체 사이의 의존성을 완전히 없애라는 것이 아닙니다.
    • 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것입니다.
    • 최소한의 의존성만 유지하고 불필요한 의존성을 제거하라는 의미입니다.

17.1 런타임 의존성과 컴파일 타임 의존성

  • 먼저 런타임과 컴파일 타임의 차이를 이해해야 합니다.
  • 런타임은 간단하다. 말 그대로 애플리케이션이 실행되는 시점을 말합니다.(257)
  • 컴파일 타임은 작성된 코드를 컴파일 하는 시점을 가리키지만 문맥에 따라서는 코드 그 자체를 가리키기도 합니다.(257)
    • 컴파일 타임 의존성은 시점이 아니라 우리가 작성한 코드의 구조이기 때문에 코드 자체를 의미한다고 볼 수 있습니다.
  • 컴파일 타임에 Movie 클래스는 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 향하는 어떤 의존성도 가지고 있지 않습니다.(258)
  • 오직 추상 클래스인 DiscountPolicy에 의존하고 있습니다.(258)
  • 하지만 런타임 의존성을 살펴보면 상황이 달라집니다. 금액 할인 정책을 적용하기 위해서는 AmountDiscountPolicy의 인스턴스와 협력해야 합니다.(259)
  • 비율 할인 정책을 적용하기 위해서는 PercentDiscountPolicy의 인스턴스와 협력해야 합니다.(259)
  • 코드 작성 시점에는 Movie 클래스는 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스의 존재에 대해 전혀 알지 못하지만 실행 시점에는 두 클래스의 인스턴스와 협력해야 합니다.
  • 만약 Movie 클래스가 AmountDiscountPolicy 클래스에 대해서만 의존한다면 PercentDiscountPolicy 클래스의 인스턴스와 협력할 수 없습니다.(259)
  • 반대로 Movie 클래스가 PercentDiscountPolicy 클래스에 대해서만 의존한다면 AmountDiscountPolicy 클래스의 인스턴스와 협력할 수 없습니다.(259)
  • 그렇다고 해서 Movie 클래스가 두 클래스에 모두 의존하게 만드는 방법은 좋은 방법이 아닙니다.(259)
    • 이것은 Movie의 결합도를 높이고 새로운 할인 정책을 추가할 때마다 Movie 클래스를 수정해야 하기 때문입니다.
    • 이런 문제를 해결하기 위해 두 클래스 중 어떤 것도 알지 못하게 하는 것입니다.
    • 두 클래스를 포괄하는 추상 클래스인 DiscountPolicy에 의존하게 만드는 것입니다.
    • 이 컴파일 타임 의존성을 실행 시에 PercentDiscountPolicy 또는 AmountDiscountPolicy 인스턴스에 대한 의존성으로 대체해야 합니다.
  • 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 됩니다.(260)
  • 따라서 컴파일 타임 구조와 런타임 구조 사이가 멀면 멀수록 설계가 유연해지고 재사용이 가능해집니다.

17.2 의존성 해결

  • 컴파일 타임 의존성은 구체적인 런타임 의존성으로 대체돼야 합니다.(261)
  • 컴파일 타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 대체하는 것을 의존성 해결이라고 부릅니다.(261)
  • 의존성 해결을 위해서는 일반적으로 다음과 같은 세 가지 방법을 사용합니다.(261)
    • 객체를 생성하는 시점에 생성자를 통해 의존성 해결
    • 객체 생성 후 setter 메서드를 통해 의존성 해결
    • 메서드 실행 시 인자를 이용해 의존성 해결
  • 생성자를 통한 의존성 해결
    • Movie 생성자에 DiscountPolicy 타입의 인자를 받는 생성자를 생성합니다.
    • 이렇게 하면 Movie 클래스는 PercentDiscountPolicy 또는 AmountDiscountPolicy 인스턴스를 모두를 선택적으로 전달받을 수 있습니다.(262)
  • setter 메서드를 통한 의존성 해결(262)
    • Movie 클래스에 setDiscountPolicy 메서드를 추가합니다.
    • 이 메서드를 통해 DiscountPolicy 타입의 인스턴스를 전달받아 의존성을 해결할 수 있습니다.
    • 실행 시점에 의존 대상을 변경할수 있는 유연성을 가집니다.
    • 단점은 객체가 생성된 후에 의존성을 설정해야 하기 때문에 객체가 완전히 초기화되지 않은 상태에서 메서드를 호출할 수 있다는 점입니다.
    • 더 좋은 방식은 생성자 방식과 setter 방식을 혼합하는 것입니다. 상태를 안정적으로 유지하면서도 유연성을 제공할 수 있습니다.
  • 메서드 인자를 통한 의존성 해결(262)
    • Movie가 항상 할인 정책을 알 필요까지는 없고 가격을 계산할 때만 일시적으로 알아도 무방하다면 메서드 인자를 통해 의존성을 해결할 수 있습니다.
    • 또는 메서드가 실행될 때마다 의존 대상이 매번 달라져야하는 경우에도 유용합니다.
    • 하지만 클래스의 메서드를 호출하는 대부분의 경우 매번 동일한 객체를 인자로 전달한다면 생성자나 setter 메서드를 통한 의존성을 지속적으로 유지하는 것이 더 좋습니다.

17.3 의존성 주입

  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection)이라고 부릅니다.(293)
  • 의존성 주입은 의존성 해결을 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내서 외부에서 필요한 런타임 의존성을 전달 받는 방법을 의미합니다.(293)

17.4 숨겨진 의존성은 나쁘다: SERVICE LOCATOR PATTERN의 문제점

  • 의존성을 내부로 감추면 관련 문제가 컴파일 타임이 아니라 런타임에 발견되게 됩니다.(298)
    • SERVICE LOCATOR 패턴을 사용하여 내부적으로 의존성을 해결할 수 있습니다.
    • 만일 서비스 로케이터에 의존성을 추가하는 것을 잊고 Movie를 사용한다면 런타임에 NullPointerException이 발생합니다.
    • 사용자는 예외를 발견하고 나서 Movie가 Service Locator에 의존하고 있다는 사실을 알게 되고 Movie를 사용하기전 Service Locator에 의존성을 추가하는 코드를 작성하게 됩니다.
  • 의존성을 숨기면 테스트하기 어렵습니다.(298)
    • SERVICE LOCATOR 패턴을 내부적으로 정적 변수를 사용해 객체를 관리하기 때문에 각 단위 테스트가 서로 고립되지 않습니다.
    • 첫 번째 테스트 케이스에 AmountDiscountPolicy를 등록하고 두 번째 테스트 케이스에 PercentDiscountPolicy를 등록하지 않으면 원하는 결과를 얻을 수 없습니다.
    • 따라서 단위 테스트가 서로 간섭됩니다.
  • 캡슐화는 코드를 읽고 이해하는 행위와 관련있습니다.(298)
    • 가시성만 private으로 선언한다고 해서 캡슐화가 되는 것은 아닙니다.
    • 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화 관점에서 좋은 코드입니다.
    • 클래스 사용법을 위해 내부 구현을 알아야 한다면 캡슐화가 잘 되지 않은 것입니다.
  • 단순히 SERVICE LOCATOR 패턴의 문제점 보다는 가급적 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다.(299)
    • 가급적 의존성을 퍼블릭 인터페이스에 노출하라.

18 SLAP: 단일 추상화 수준 원칙

  • SLAP(Single Level of Abstraction Principle) 단일 추상화 수준 원칙은 메서드 내의 모든 코드가 동일한 추상화 수준에 있어야 한다는 원칙입니다. (오브젝트 - 설계 원칙편)
  • 즉 메서드의 일부가 저수준의 데이터베이스 연결을 처리하고, 다른 부분은 고수준의 비즈니스 코드를 처리하며, 또 다른 부분은 웹 서비스 관련 코드를 처리하는 일이 없어야 합니다.
  • 이런 메서드는 켄트 벡의 조합 메서드 규칙도 위반하게 됩니다.
  • 고수준과 저수준
    • 고수준: 메서드 호출자의 목표를 설명합니다. (What)
    • 저수준: 목표 달성하는 메커니즘이나 알고리즘을 의미합니다. (How)

18.1 조합 메서드 패턴

  • 메서드 내의 모든 작업을 동일한 추상화 수준으로 유지하세요.
  • 이렇게 하면 여러 개의 작은 메서드로 이루어진 프로그램이 만들어지고, 각 메서드는 몇 줄 정도로 짧아지게 됩니다.
  • 이러한 패턴을 조합 메서드 패턴이라고 부릅니다.
  • 단일 추상화 수준 원칙을 따르면 조합 메서드 패턴을 따르게 됩니다.

19. 명령 쿼리 분리 원칙

  • 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴이라고 부릅니다.(202)
  • 루틴은 다시 프로시저함수로 구분됩니다.
    • 프로시저: 부수효과를 발생시킬 수 있지만 값을 반환할 수 없습니다.
    • 함수: 값을 반환할 수 있지만 부수효과를 발생시킬 수 없습니다.
  • Command-Query 분리 원칙은 프로시저와 함수를 부르는 또 다른 이름입니다.
    • Command가 프로시저의 다른 이름입니다.
    • Query가 함수의 다른 이름입니다.

19.1 장점

  • 명령과 쿼리를 구분하면 객체의 부수효과를 제어하기 수월해집니다.
  • 쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라도 상관없습니다.
    • 명령이 개입되지 않는 한 쿼리의 값은 변경되지 않아 결과 예측이 쉬워집니다.
  • 쿼리를 순서와 횟수에 상관없이 호출할 수 있습니다.
  • 메서드의 반환 값만으로 명령과 쿼리를 구분할 수 있습니다.
    • 쿼리의 경우 부수 효과를 발생시키지 않기 때문에 내부 구현을 살펴볼 필요가 없습니다.

19.2 부수 효과 제어

  • 만약 메서드가 명령인 동시에 쿼리인 경우 부수효과를 제어하기 어렵습니다.
  • 예를 들어 Player 클래스의 move 메서드는 플레이어의 위치를 변경하는 명령이면서 동시에 플레이어의 이동의 성공 여부를 Boolean 값으로 반환하는 쿼리입니다.
  • 이런 경우 외관상으로는 쿼리에 해당하지만 실제로는 부수효과를 발생시키는 명령이 됩니다.
  • 이렇게 쿼리와 명령이 혼합된 메서드는 부수효과 예측하기 어렵게 만듭니다.
    • 내부 구현을 확인해 부수효과를 찾아야합니다.
    • 명령과 쿼리를 동시에 사용하면 모든 메서드의 부수 효과를 보기위해 모든 메서드의 내부 구현을 살펴봐야 합니다.
  • 이런 경우 명령과 쿼리로 분리합니다. canMove 메서드와 move 메서드로 분리합니다.
  • canMove 메서드는 플레이어가 이동할 수 있는지 여부를 반환하는 쿼리입니다.
  • move 메서드는 플레이어의 위치를 변경하는 명령입니다.
  • Player 클래스의 클라이언트는 canMove 메서드를 호출해 플레이어가 이동할 수 있는지 여부를 확인한 후 move 메서드를 호출해 플레이어의 위치를 변경할 수 있습니다.
    • 이제 canMove 메서드는 부수효과를 발생시키지 않기 때문에 여러 번 호출해도 상관없습니다.
  • 이동 가능한지 물은 다음 시키는 것은 묻지 말고 시켜라 원칙을 위반하는 것이라고 생각할 수 있습니다.
    • 하지만 move 메서드 안에 canMove 메서드를 호출하는 로직이 들어가 있어 스스로 판단하고 이동하기 때문에 묻지 말고 시켜라 원칙을 위반하지 않습니다.
    • 부수효과를 통제하기 위해 클라이언트가 쿼리를 통해 상태를 확인하고 명령을 통해 상태 변경을 요처하는 패턴을 자주 볼 수 있습니다.
    • 이러한 번거로운 과정은 부수 효과를 제어하기 위한 비용으로 생각해야 합니다.

20 개방 폐쇄 원칙: Open-Closed Principle

  • 소프트웨어 개체는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.(282)
  • 확장에 대해 열려 있다: 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 동작을 추가해서 애플리케이션에 기능을 추가할 수 있다.
  • 수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.

20.1 컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하라

  • 사실 개방 폐쇄 원칙은 런타임 의존성과 컴파일 타임 의존성에 관한 이야기입니다.(283)
  • 중복 할인 정책을 구현하는 OverlappedDiscountPolicy 클래스를 추가하더라도 Movie 클래스는 여전히 DiscountPolicy 클래스에만 의존한다.(284)
  • 따라서 컴파일 타임 의존성은 변하지 않는다. 하지만 런타임에 Movie 인스턴스는 OverlappedDiscountPolicy 인스턴스와 협력할 수 있다.(284)
  • 개방 폐쇄 원칙을 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.(284)

20.2 추상화가 핵심이다.

  • 개방 폐쇄 원칙의 핵심은 추상화에 의존하는 것입니다.(284)
  • 추상화는 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법입니다.(284)
  • 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 암게 되고 문맥에 따라 변하는 부분은 생략된다.
  • 따라서 추상화 부분은 수정에 대해 닫혀있다. 추상화를 통해 생략된 부분은 확장의 여지를 남긴다.
  • 단순히 어떤 개념을 추상화했다고 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니다.(285)
    • 개방 폐쇄 원칙이 가능하려면 모든 요소가 추상화에 의존해야 한다.(285)
    • Movie가 DiscountPolicy라는 추상화에 의존하려면 Movie 내부에서 AmountDiscountPolicy와 같은 구체 클래스 인스턴스를 생성해서는 안 된다.(287)
    • 이렇게 되면 할인 정책 비율을 변경하는 방법은 한가지 코드를 수정하는 방법밖에 없게 된다.(287)
    • 물론 객체 생성을 피할 수 없다. 어딘가에서 반드시 생성해야 한다. 하지만 부적절한 곳에서 하는 것이 문제가 된다.(287)
    • 동일한 클래스 안에서 객체 생성과 사용이 혼합되면 개방 폐쇄 원칙을 위반하게 된다.
    • 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.(288)
    • 이 방법이 타당한 이유는 Movie에게 금액 할인 정책을 적용할지, 비율 할인 정책을 적용할지 알고 있는 것은 그 시점에 Movie와 협력하는 클라이언트다.

21 의존성 역전 원칙: Dependency Inversion Principle

  • 의존성 역전 원칙(오브젝트 - 설계 원칙편 18강)
    • 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
    • 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.
  • 객체 사이의 협력이 존재할 때 본질은 상위 수준의 정책이다.(300)
    • Movie와 AmountDiscountPolicy의 협력의 본질은 영화의 가격을 계산하는 것이다.
    • 어떻게 할인 금액을 계산하는지가 협력의 본질이 아니다.
    • 그러나 이런 상위 수준이 하위 수준 클래스에 의존하면 하위 수준의 변경이 상위 수준에 영향을 받게 된다.
    • AmountDiscountPolicy를 PercentDiscountPolicy로 변경한다고 해서 Movie 클래스가 영향받으면 안 된다.
    • 의존성은 AmountDiscountPolicy에서 Movie로 향해야 한다.
    • 이번에도 해결사는 추상화다.
  • Movie와 AmountDiscountPolicy 모두가 추상화에 의존하게 하면 하위 수준 클래스의 변경이 상위 수준으로 전파되는 것을 막을 수 있다.(301)
    • 유연하고 재사용 가능한 설계를 원한다면 중요한 조언은 추상화에 의존하는 것이다.
    • 구체 클래스는 의존성의 시작점이지 의존성의 끝점이 아니다. 모든 의존성은 추상화로 끝나야 한다.(301)
  • 역전의 의미(302)
    • 전통적인 개발 방법에서는 상위 수준의 모듈이 하위 수준에 의존하는 경우가 많다.
    • 로버트 마틴은 의존성 역전 원칙을 따르면 의존성의 방향이 전통적인 절차형 프로그래밍과는 반대이기 때문에 역전이라고 부른다.

21.1 의존성 역전과 패키지

  • 역전은 의존성의 방향뿐아니라 인터페이스의 소유권에도 적용된다.(302)
  • 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다.(304)
  • 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다.
  • 마틴 파울러는 이를 SEPARATE INTERFACE 패턴이라고 부른다.(304)
  • 인터페이스는 그것을 사용하는 클라이언트 패키지에 위치해야 한다
    • 상위 수준 모듈이 하위 수준 모듈보다 더 안정적이다.
    • 상위 수준 모듈은 본질적인 목적과 관련이 있어 불필요한 이유로 변경되어서는 안 된다.
    • 하위 수준 모듈은 세부사항에 속하므로 자주 변경될 수 있다.
    • 인터페이스(추상화)는 상위 수준 모듈에 속해야 한다.
    • 추상화가 세부사항에 의존하면 안정적이어야 하는 추상화가 불안정해진다.
    • "추상화는 세부사항에 의존해서는 안 된다. 세부사항이 추상화에 의존해야 한다"는 원칙을 패키지 레벨에 적용한 것이다.

21.2 유연한 설계는 유연성이 필요할 때만 옳다

  • 유연한 설계는 런타임 의존성과 컴파일 타임 의존성의 차이를 인식하고 컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하는 것을 목표로 한다.
  • 하지만 유연한 설계가 항상 좋은 것은 아니다.(305)
    • 단순하고 명확한 설계를 가진 코드는 읽기도 쉽고 이해하기도 편하다.
    • 변경하기 쉽고 확장하기 쉬운 코드는 단순함과 명확함의 미덕을 버리게될 가능성이 높다.
    • 유연한 설계라는 말의 이면에는 복잡한 설계가 숨어 있다.
    • 정적인 클래스의구조와 실행 시점의 동적인 객체 구조가 다르기 때문에 개발자들이 이해하기 어려운 복잡한 구조가 만들어진다.
  • 불필요한 유연성은 불필요한 복잡성을 초래한다.(305)
    • 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하라

22. 인터페이스 분리 원칙: Interface Segregation Principle

23. 리스코프 치환 원칙: Liskov Substitution Principle

  • 바바라 리스코프는 올바른 상속 관계의 특징을 정의하기 위해 리스코프 치환 원칙을 발표했습니다.(453)
  • 바바라 리스코프는 상속 관계로 연결된 두 클래스가 서브타이핑 관계를 만족시키기 위해서는 다음의 조건을 만족해야 한다고 주장했습니다.(453)
    • 여기서 요구되는 것은 다음의 치환 소석과 같은 것이다. S형의 각 객체 o1에 대해 T형의 객체 o2가 존재하여, T에 의해 정의된 모든 프로그램 P에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입이다.
    • 한마디로 서브타입은 그건의 기반 타입에 대해 대체 가능해야 한다는 것입니다.
    • 클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다는 것입니다.
    • Stack과 Vector는 리스코프 치환 원칙을 만족시키지 못하는 전형적인 예입니다.
    • 클라이언트가 Vector에 대해 기대하는 행동을 Stack에 대해서는 기대할 수 없기 때문에 행동 호환성을 만족시키지 않기 때문입니다.
    • Stack과 Vector는 서브타이핑이 아니라 서브클래싱 관계입니다.(457)
    • Stack과 Vector가 리스코프 치환 원칙을 위반하는 가장 큰 이유는 상속으로 인해 Stack에는 포함돼서는 안되는 Vector의 퍼블릭 인터페이스가 Stack의 퍼블릭 인터페이스에 포함되기 때문입니다.(457)

23.1 서브타이핑과 서브클래싱

  • 서브타이핑과 서브클래싱은 다릅니다.(452)
    • 서브클래싱: 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 것. 구현 상속 또는 클래스 상속이라고 부르기도 합니다.
    • 서브티이핑: 타입 계층을 구현하기 위해 상속을 사용하는 것. 인터페이스 상속이라 부르기도 합니다.

23.2 리스코프 치환 원칙의 위반 사례

  • 대부분의 사람들은 정사각형은 직사각형이다라는 이야기를 당연하게 생각한다.(454)
    • 사실 정사각형과 직사각형의 상속 관계는 리스코프 치환 원칙을 위반하는 고전적인 사례 중 하나입니다.
    • 정사각형과 직사각형은 어휘적으로 is-a 관계가 성립합니다.
    • is-a 관계를 구현하는 가장 간단한 방법으로 상속을 사용합니다.
    • Rectangle 클래스와 이를 상속받는 Square 클래스가 있다고 가정해봅시다.
    • 여기서 Rectangle 클래스와 협력하는 클라이언트는 직사각형의 너비와 높이가 다르다고 가정합니다.
    • 예를 들어, resize 메서드의 인자로 Rectangle, width, height를 전달한다고 가정해봅시다.
    • 여기서 resize의 메서드 인자로 Rectangle 대신 Square를 전달한다면 문제가 발생합니다.
    • resize 메서드 관점에서 Rectangle과 Square를 사용할 수 없기 때문에 Square는 Rectangle이 아닙니다.
    • Square는 Rectangle의 구현을 재사용하고 있을 뿐 리스코프 치환 원칙을 만족시키지 못합니다.
  • 리스코프 치환 원칙에서 한 가지만 기억해야 한다면 아래를 기억하세요.(458)
    • 대체 가능성을 결정하는 것은 클라이언트입니다.

23.3 리스코프 치환 원칙과 OCP

  • 리스코프 치환 원칙은 개방 폐쇄 원칙을 따르는 데 필수적입니다.(460)
    • 자식 클래스가 클라이언의 고나점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없습니다.
    • 따라서 리스코프 치환 원칙은 OCP를 만족하는 설계를 위한 전제 조건입니다.
    • 일반적으로 리크소프 치환 원칙 위반은 잠재적인 OCP 위반을 의미합니다.

23.4 서브타입과 계약

  • 클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현한 것을 계약에 의한 설계라고 부른다.(461)
  • 계약에 의한 설계는 3가지 요소로 구성된다.(461)
    • 사전 조건(Precondition): 클라이언트가 메서드를 실행하기 전에 만족시켜야 하는 조건
    • 사후 조건(Postcondition): 메서드가 실행된 후 클라이언트에게 보장해야 하는 조건
    • 클래스 불변식(Invariant): 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 클래스의 불변 조건
  • 서브타입이 리스코프 치환 원칙을 만족시키기 위해 클라이언트와 수퍼타입 간에 체결된 계약을 준수해야 한다.(461)
  • 서브타입에 더 강한 사전 조건을 정의할 수 없다.
  • 서브타입에 슈퍼타입과 같거나 더 강한 사조조건을 정의할 수 있다.
  • 서브타입에 더 약한 사후 조건을 정의할 수 없다.