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);
}