오브젝트: 코드로 이해하는 객체지향 설계를 읽고


오브젝트: 코드로 이해하는 객체지향 설계를 읽고 정리한 글입니다.


객체지향 설계

  • 설계란 코드를 배치하는 것이다.

  • 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계

  • 요구사항은 항상 변하기 마련이다.

객체지향 프로그래밍

  • 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴 이라고 한다.

  • 자식 클래스가 부모 클래스를 대신 하는 것이 업캐스팅

  • 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력

  • 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것은 오해다.
    인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

  • 상속의 가장 큰 문제점은 캡슐화를 위반한다는 것.

  • 상속의 두번째 단점은 설계가 유연하지 않다는 것

역할, 책임, 협력

  • 코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.

  • 객체지향 패러다임의 관점에서 핵심은 역할, 책임, 협력이다.

  • 객체지향 설계에서 가장 중요한 것은 책임이다. 객체에게 얼마나 적절한 책임을 할당하느냐가 설계의 전체적인 품질을 결정한다.

  • 역할을 구현하는 가장 일반적인 방법은 추상 클래스와 인터페이스를 사용하는 것

  • 협력의 관점에서 추상 클래스와 인터페이스는 구체 클래스들이 따라야 하는 책임의 집합을 서술한 것이다.
    추상 클래스는 책임의 일부를 구현해 놓은 것이고 인터페이스는 일체의 구현 없이 책임의 집합만을 나열해 놓았다는 차이가 있지만 협력의 관점에서는 둘 모두 역할을 정의할 수 있는 구현 방법이라는 공통점을 공유한다.

메시지와 인터페이스

  • 강조하고 싶은 것은 소프트웨어 설계에 법칙이란 존재하지 않는다 라는 것이다.
    원칙을 맹신하지 마라. 원칙이 적절한 상황과 부적절한 상황을 판단할 수 있는 안목을 길러라.
    설계는 트레이드오프의 산물이다. 소프트웨어 설계에 존재하는 몇 안되는 법칙 중 하나는 경우에 따라 다르다 라는 사실을 명심해라.

  • 프로시저는 부수효과를 발생시킬 수 있지만 반환할 수 없다. 함수는 값을 반환할 수 있지만 부수효과는 발생시킬 수 없다.

객체 분해

  • 하향식은 이미 완전히 이해된 사실을 서술하기에 적합한 방법이다. 그러나 하향식은 새로운 것을 개발하고, 설계하고, 발견하는 데는 적합한 방법이 아니다.
    이것은 수학과 아주 유사하다. 수학 교과서는 계산의 과정을 논리적인 순서로 서술한다. 공인되고 증명된 이론이 뒤이은 이론을 증명하기 위해 사용된다.

    그러나 이론은 그런 방식이나 순서로 개발되거나 발견된 것이 아니다. 시스템이나 프로그램 개발자가 이미 완료한 결과에 대한 명확한 아이디어를 가지고 있다면 머리속에 있는 것을 종이에 서술하기 위해 하향식을 사용할 수 있다.
    이것은 사람들이 하향식 설계나 개발을 할 수 있고, 그렇게 함으로써 성공할 수 있다고 믿게 만드는 이유다. 하향식 단계가 시작될 때는 문제는 이미 해결됐고, 오직 해결돼야만 하는 세부사항만이 존재할 뿐이다.

  • 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점

의존성 관리하기

  • 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술

  • 의존성에 의한 영향이 적은 경우에도 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 설계 습관이다.

유연한 설계

  • 표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것이다. 표현적 분해는 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다.

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

상속과 코드 재사용

  • 역할, 책임 협력에 먼저 집중하라. 3가지의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수도 있다는 사실을 명심하라.

  • 코드 중복을 제거하기 위해 상속을 도입할때 따르는 두가지 원칙이 있다.

    • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라. 메서드 추출을 통해 두 메서드를 동일한 형태로 보이도록 만들 수 있다.

    • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것보다 자식 클래스의 추상적인 메서드를 부모 클래스로 올리는 것이 재사용성과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.

  • 상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없다. 상속은 어떤 방식으로든 부모 클래스와 자식 클래스를 결합시킨다. 메서드 구현에 대한 결합은 추상 메서드를 추가함으로써 어느정도 완화할 수 있지만 인스턴스 변수에 대한 잠재적인 결합을 제거 할 수 있는 방법은 없다.

합성과 유연한 설계

  • 상속이 코드 재사용이라는 측면에서 강력한 도구인 것은 사실이지만 강력한 만큼 잘못 사용할 경우에 돌아오는 피해 역시 크다는 사실을 뼈저리게 경험한 것이다.

    상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만든다. 정말로 필요한 경우에만 상속을 사용하라. 상속은 코드 재상용과 관련된 대부분의 경우에 우아한 해결방법이 아니다.

    객체지향에 능숙한 개발자들은 상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은 방법이 있다는 사실을 알고 있다. 바로 합성이다.

  • 상속은 합성과 재사용의 대상이 다르다. 상속은 부모 클래스 안에 구현된 코드자체를 재사용하지만 합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용한다.
    따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있다. 다시 말해서 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있는 것이다.

  • 몽키 패치란 현재 실행중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것을 가리킨다. 코드를 수정할 권한이 없거나 소스코드가 존재하지 않든다고 가정하더라도 몽키패치가 지원되는 환경이라면 특정 객체에 특정 메서드를 추가흔 것이 가능하다.

  • 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법은 상속이다. 하지만 상속은 코드 재사용을 위한 우아한 해결책은 아니다. 상속은 부모 클래스의 세부적인 구현에 자식 클래스를 강하게 결합시키기 때문에 코드의 진화를 방해한다.

  • 합성이 상속과 같은 문제점을 초래하지 않는 이유는 클래스의 구체적인 구현이 아니라 객체의 추상적인 인터페이스에 의존하기 대문이다.

  • 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.

다형성

  • 상속의 목적은 코드 재사용이 아니다. 상속은 타입 계층을 구조화 하기 위해 사용해야 한다.

  • 다형성은 여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법

  • 데이터 관점의 상속이 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 개념이라면 행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다.

  • 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것을 업캐스팅이라고 부른다. 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다.

    이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행 시점에 결정하기 때문에 가능하다. 이를 동적 바인딩이라고 부른다.

서브클래싱과 서브타이핑

  • 상속의 첫번째 용도는 타입 계층을 구현하는 것

  • 상속의 두번재 용도는 코드 재사용

  • 재사용을 위해 상속을 사용할 경우 부모 클래스와 자식 클래스가 강하게 결합되어 변경하기 어려운 코드가 될 확률이 높다.

  • 상속관계가 IS-A 관계를 모델링하는가? 자식 클래스는 부모클래스다라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있다.

    클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방하고 둘의 차이점을 클라이언트 입장에서는 몰라야 한다. 이를 자식 클래스와 부모클래스 사이의 행동 호환성이라고 부른다.

  • 서브타입에 더 약한 사후조건을 정의할 수 없다.

디자인 패턴과 프레임워크

  • 알고리즘을 캡슐화 하기 위해 합성 관계가 아닌 상속 관계를 사용하는 것을 TEMPLATE METHOD 패턴이라고 한다.

  • 알고리즘 교체와 같은 요구사항이 없다면 상대적으로 STRATEGY 패턴보다 TEMPALTE MEHTOD패턴이 복잡도를 낮출 수 있다는 점에서 장점이다.

  • DECORATOR 패턴은 객체의 행동을 동적으로 추가할 수 있게 해주는 패턴으로서 기본적으로 객체의 행동을 결합하기 위해 객체 합성을 사용한다. 이 패턴은 선택적인 행동의 개수와 순서에 대한 변경을 캡슐화할 수 있다.

  • 디자인 패턴의 목적은 특정한 변경을 캡슐화함으로써 유연하고 일관성 있는 협력을 설계할 수 있는 경험을 공유하는 것이다.

    디자인 패턴에서 중요한 것은 디자인 패턴의 구현 방법이나 구조가 아니다. 어떤 디자인 패턴이 어떤 변경을 캡슐화하는지를 이해하는 것이 중요하다.
    그리고 각 디자인 패턴이 변경을 캡슐화하기 위해 어떤 방법을 사용하는지를 이해하는 것이 더 중요하다.

  • COMPOSITE 패턴은 개별 객체와 복합 객체라는 객체의 수와 관련된 변경을 캡슐화하는 것이 목적이다.

  • 패턴 입문자가 빠지기 쉬운 함정은 패턴을 적용하는 컨텍스트의 적절성은 무시한 채 패턴의 구조에만 초점을 맞추는 것이다.

  • 패턴을 가장 효과적으로 적용하는 방법은 패턴을 지향하거나 패턴을 목표로 리팩터링을 하는 것이라고 이야기 한다.
    그는 패턴이 적용된 최종결과를 이해하는 것보다는 패턴을 목표로 리팩토링 하는 이유를 이해하는 것이 훨씬 가치있고 훌륭한 설계 자체를 공부하는 것보다 훨씬 중요하다고 한다.

타입계층의 구현

클래스와 타입간의 차이를 이해하는 것은 중요한 일이다.

객체의 클래스는 객체의 구현을 정의한다. 클래스는 객체의 내부상태와 오퍼레이션 구현 방법을 정의하는 것이고, 객체의 타입은 인터페이스만을 정의하는 것으로 객체가 반응할 수 있는 오퍼레이션의 집합`을 정의한다.

하나의 객체가 여러 타입을 가질 수 있고 서로다른 클래스의 객체들이 동일한 타입을 가질 수 있다.
즉, 객체의 구현은 다를지라도 인터페이스는 같을 수 있다는 의미이다. 클래스와 타입간에는 밀접한 관련이 있다. 클래스도 객체가 만족 할 수 있는 오퍼레이션을 정의하고 있으므로 타입을 정의하는 것이기도 하다. 그래서 객체가 클래스의 인터페이스라고 말할 때 객체는 클래스가 정의하고 있는 인터페이스를 지원한 다는 뜻을 내포한다.

도메인 모델

  • 도메인이란 사용자가 프로그램을 사용하는 대상 영역을 가리킨다. 모델이란 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태이다.

  • 자가 프로그램을 사용하는 대상 영역에 대한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태이다.

  • 어떤 인스턴스가 다른 인스턴스의 타입을 표현하는 방법을 TYPE OBJECT 패턴이라고 부른다.