Object
1. 절차지향
- 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라고 부른다.(26p)
- 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터 역할만 수행한다.(26p)
1.1 절차지향의 문제점
- 절차적 프로그래밍의 세상에서 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어렵다.(26p)
- 모든 처리가 하나의 클래스에 위치하기 때문에 여러 데이터 클래스 중 하나만 변경되더 라도 변경이 처리 클래스의 변경을 부른다.(26p)
- 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계입니다.(27)
- 절차적 프로그래밍은 프로세스가 필요한 모든 데이터에 의존해야 한다는 근본적인 문제점 때문에 변경에 취약할 수밖에 없습니다.
1.2 해결방안
- 자신의 데이터는 스스로 처리하도록 프로세스를 이동시켜야 합니다. (27)
- 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라고 부릅니다.(27)
- 비록 이 관점이 객체지향 구현 관점에서만 바라본 지극히 편현합 관점이지만 객체지향 프로그래밍의 본질을 실용적으로 설명하는 것은 사실입니다.
- 이 문서를 읽고 나면 객체지향이 단순히 데이터와 프로세스를 하나의 객체 안으로 모으는 것 이상의 것이라는 것을 알 수 있습니다.
- 사실 객체지향의 핵심은 적절한 객체에 적절한 책임을 할당하는 것입니다. (29)
2. 객체지향 프로그래밍
- 보통 객체지향 프로그래밍을 작성할 때 가장 먼저 고려하는 것은 어떤 클래스가 필요한지 고민한다.(40p)
- 하지만 진정한 객체지향은 클래스가 아닌 객체에 초점을 맞출 때 얻을 수 있다.
- 즉 어떤 클래스가 필요한지가 아니라 어떤 객체가 필요한지 고민해야 합니다.
2.1 의인화
- 비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다.(33)
- Theater, Bag, TicketOffice 이들은 실세계에서 자율적인 존재가 아닙니다. 그럼에도 우리는 이들을 관람객이나 판매원과 같은 생물처럼 다뤘 다. 무생물 역시 스스로 행동하고 자기 자신을 책임지는 자율적인 존재로 취급한 것입니다.
- 레베카 워프스브록은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)라고 부른다.
- 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.
- 그 대상이 비록 실세계에서는 생명이 없는 수동적인 존재라고 하더라도 객체지향의 시계에 들어오면 능동적이고 자율적인 존재로 바뀌게 된다.
- 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킵니다. (34)
- 그 대상이 비록 실세계에서 생명이 없는 수동적인 존재라고 하더라도 객체지향의 세계로 넘어오는 순간 그들은 생명과 지능을 가진 싱싱한 존재로 다시 태어난다.
2.2 왜 객체지향인가?
- 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계입니다.(35)
- 객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공하여 좋은 설계를 만들 수 있도록 돕습니다.(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)
- 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙은 아래와 같습니다.
- 디미터 법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리