contra

<오브젝트> - 코드로 이해하는 객체지향 설계 독서 정리

2020-09-14

<오브젝트> 독서 정리

클래스의 내부와 외부를 구분해야 하는 이유

  1. 경계의 명확성이 객체의 자율성 보장
  2. 프로그래머에게 구현의 자유를 제공

객체는 상태와 행동을 함께 가진다. 객체는 스스로 판단하고 행동하는 자율적인 존재이다.

객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶어서 문제영역의 아이디어를 표현한다. 이것이 캡슐화이다. 캡슐화에 덧붙여서, 객체가 스스로 판단하고 행동할 수 있도록 외부의 간섭을 제한할 필요가 있다. 외부에서는 객체가 어떤 상태에 있는지 어떤 생각을 하는지 알아서는 안되며, 결정에 직접적으로 개입해서는 안된다. 객체에게 원하는 것을 요청하고, 스스로 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.

  1. 외부에서 접근 가능한 인터페이스 부분
  2. 내부에서만 접근 가능한 구현 부분

변경될 가능성이 있는 부분은 private 으로 두고, 내부 구현은 무시하고 외부 인터페이스 만으로도 클래스를 사용할 수 있도록 하며, 내부 클래스의 변경을 실수로라도 할 수 있는 가능성 방지.

돈을 나타내는 숫자를 표현할 때 Long 데이터 타입을 사용할 수도 있지만, Money 클래스를 만들어서 명시적으로 사용하면 1. 의미 표현 명시적이어짐 2. 돈과 관련된 로직이 하나로 모임

메시지와 메서드

어떤 객체가 다른 객체의 메서드를 호출한다고 표현하지만, 실제로는 다른 객체에게 메시지를 보낸 것이다. 발송하는 쪽에서는 받는 객체가 그 메시지를 처리할 수 있을지 없을지도 모른다. 메시지를 받은 객체에서는 자신만의 방법(method) 대로 메시지를 처리한다. Java같은 언어에서는 안되지만, Ruby, SmallTalk에서는 다른 메서드를 통해서도 메시지에 응답이 가능하다.

부모 클래스에 기본적인 알고리즘 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴: 템플릿 메소드 패턴 (예: abstract class)

클래스 사이의 의존성 ≠ 객체 사이의 의존성 : 유연하고 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계의 비법! But, 코드는 이해하기 어려워짐. 트레이드 오프 해야함.

차이에 의한 프로그래밍: 상속을 이용하는 방식을 일컫는 말

추상화

구현이 아니라 인터페이스에 초점을 맞추는 것.

장점: 추상화 계층만 놓고보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.

설계가 좀 더 유연해진다. 상위 정책에 포함되는 하위 정책들을 얼마든지 만들어내도 문제가 발생하지 않는다.

유연한 설계

만약 할인 정책이 지금까지는 Movie의 getFee 메소드에서 DiscountPolicy를 불러와서 결정하고 있었는데, 할인이 없는 경우라면? Movie에서 if(discount === null) 이런식으로 하는 순간 일관적인 협력이 무너진다. 할인 정책의 계산 책임이 movie로 넘어가기 때문이다. 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택하라.

기존 코드를 수정하지 않고 추가하는 방식으로 코딩하자. NonePolicy 같은 클래스를 만들어서 걔를 활용하자.

결론: 유연성이 필요한 곳에 추상화를 사용하라.

상속과 합성

상속의 가장 큰 문제: 캡슐화를 위반한다. 부모를 잘 알아야 하기 때문.부모와 강하게 결합되기 때문에, 부모를 고칠 때 문제가 발생할 수도 있다. 또한, 런타임에 객체를 갈아끼울 수도 없다.

객체 합성

인스턴스를 포함시켜서 사용하는 것. 인터페이스에 정의된 메시지를 통해서만 코드를 재사용한다.

상속과 합성을 섞어쓸 수 밖에 없는 경우도 있다. 다형성을 위해 인터페이스를 재사용하는 경우 상속 써야 한다.

3장

객체지향의 핵심은 역할, 책임, 협력. 객체지향의 본질은 협력하는 객체들의 공동체를 창조하는 것.

  • 협력: 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용
  • 책임: 객체가 협력에 참여하기 위해 수행하는 로직
  • 역할: 객체들이 협력에서 수행하는 책임이 모여 객체가 수행하는 역할

객체의 자율성을 보장하는 방법: 내부 구현을 캡슐화한다. 만약 어느 객체가 다른 객체의 자율성을 존중하지 않는다면, 두 객체 중 하나만 고쳐도 다른 하나까지 영향받을 수 있게된다.

객체지향에서 가장 중요한 것은 책임이다.

협력이 객체의 행동을 결정하고, 행동이 상태를 결정한다. 그리고 그 행동이 객체의 책임이 된다.

메시지가 객체를 결정한다

장점: 객체가 최소한의 인터페이스를 갖는다, 추상적인 인터페이스만 유지할 수 있다.

행동이 상태를 결정한다

객체가 맡을 역할에 따른 행동을 먼저 설계해야지 ,객체의 상태 먼저 설계하면 안된다. 그러면 객체의 내부 구현이 퍼블릭 인터페이스로 공개된다. 그러면 내부 구현을 수정했을때 인터페이스도 수정해야 하는 일이 생긴다.

연극에서의 배우와 객체는 비슷하다. 객체는 특정한 협력 안에서 하나의 역할을 맡지만, 다른 역할을 맡을 수도 있다. 그리고 동일한 배역이라면 필요에 따라 다른 객체들이 대신 연기해줄 수도 있다.

4장

객체지향은 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.

데이터 중심 설계와 책임 중심 설계의 장단점을 비교해보자. 우선 데이터 중심 설계의 특징:

데이터 중심 설계

  1. 객체가 어떤 데이터를 가질지 먼저 생각한다
  2. 캡슐화를 위해 해당 데이터의 get, set 메서드를 구현한다

어떤 설계가 좋은 설계인가를 나누는 척도

  1. 캡슐화 → 외부에서 알 필요가 없는 부분은 숨기는 것. 추상화의 한 종류. 불안정한 구현 세부사항을 인터페이스 뒤로 캡슐화하는 것.
  2. 응집도 → 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협업하는지
  3. 결합도 → 다른 모듈에 대해서 얼마나 많은 지식을 갖고있는지

응집도와 결합도는 변경과 관련이 깊다. 어떤 설계를 쉽게 변경할 수 있다면 높은 응집도와 낮은 결합도를 갖고 있을 것이다.(리디북스의 카테고리에 적용하면 어떨까?)

응집도와 결합도는 캡슐화로 달성할 수 있다. 캡슐화를 지키면 응집도는 올라가고 결합도는 낮아진다.

데이터 중심 설계의 문제점

캡슐화 위반

Getter, setter가 정말 캡슐화에 도움이 될까? 전혀 그렇지 않다. 내부에 특정 인스턴스 변수가 존재한다는 사실이 퍼블릭 인터페이스로 드러난다. 이렇게 된 근본적인 원인은 객체가 수행할 책임이 아니라, 내부에 저장할 데이터에 초점을 맞췄기 때문이다.

접근자와 수정자에 과도하게 의존하는 설계: 추측에 의한 설계. 객체가 다양하게 사용될 것이라는 이상한 전제 하에 만들었으니, 필요도 없는 인터페이스만 잔뜩 생긴다.

높은 결합도

데이터 객체를 사용해서 제어 로직을 다루는 제어 객체가 거대한 의존성 덩어리가 된다. 데이터 자료형이라도 바뀌는 순간 모든걸 다 바꿔야 한다

낮은 응집도

  • 변경 이유가 다른 코드를 하나의 모듈에 뭉쳐놓았기 때문에 하나만 바꿔도 모듈 전체가 영향받을 수 있다
  • 하나의 요구사항 반영을 위해 여러개의 파일을 수정해야 한다.

단일책임원칙: 하나의 클래스는 단 한 가지의 변경 이유를 가져야 한다

객체를 자율적으로 만드는 방법

캡슐화 지키기

내부 구현을 노출하지 않고, 자기 내부 상태를 노출하지 않고 퍼블릭 인터페이스만 노출한다.

예를 들어서, 사각형을 나타내는 Rectengle 클래스에서, setHeight, setWidth 이딴거 하나하나 노출해서 크기를 조정하려고 하지 마라. 그냥 enlarge같은 함수를 만들어서 그걸로 크기를 조정하게 만들어라. 그리고 그 함수를 객체 스스로 갖게 하라. 책임을 객체 안에다가 두어라.

  1. 객체에 필요한 데이터 리스팅
  2. 그 데이터에 어떤 operator가 필요한지 리스팅

캡슐화: 변하는 모든것이든 다 감추는 것

객체 각각에서 데이터와 역할을 갖고, 스스로 책임지는 객체가 됐다고 하자. 하지만 그것만으로는 부족하다. 스스로 책임지는 operation의 메소드를 다른 클래스에서 갖다쓰는 순간, 캡슐화 위반으로 이어질 수 있기 때문이다.

  1. 메소드 시그니처가 달라졌을 때, 사용처도 변경 필요
  2. 만약에 switch case로 분기하거나 if else 로 분기할 때, 새로운 케이스가 생기면 if else 를 추가해야 하므로 역시 사용처 변경 필요
  3. 반환값 달라졌을 때 사용처 변경 필요

데이터 중심 설계가 변경에 취약한 이유

  1. 데이터 중심 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다
  2. 데이터 중심 설게에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 강요한다

데이터 중심 설게는 객체의 행동보다는 상태에 초점을 맞춘다

데이터 중심 설게에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 강요한다

5장

객체 입장에서 책임이 조금 어색해 보이더라도, 협력에 적합하다면 그 책임은 좋은 것이다.

메시지를 보내는 쪽에 맞춘다. 메시지를 결정하고 그 메시지를 받을 객체를 결정한다.

책임 할당을 정의하는 GRASP 패턴

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그린다. 도메인 안에는 무수히 많은 개념들이 존재하며, 이 도메인 개념들을 책임 할당의 대상으로 사용한다.

  1. 어플리케이션이 해야 할 일을 메시지화한다.
  2. 그 메시지를 수신할 객체를 고른다
  3. 정보전문가 패턴을 이용해서 고른다. 이 패턴은, 어떤 객체가 그 책임을 수행하는데에 전문가를 선택한다는 것이다. 그래야 그 객체가 자율적인 존재가 된다.
  4. 그렇지만 해당 객체가 그 정보를 저장하고 있을 필요는 없다. 다른 객체에 있다는 사실만 알아도 된다.

책임을 할당할 수 있는 다양한 대안들이 존재한다면, 높은 응집도와 낮은 결합도를 가진 쪽으로 선택한다.

  • Low coupling

  • High cohension

  • Creator 패턴 → A객체를 생성해야 할 때, 다음 조건을 최대한 많이 만족하는 B객체가 생성하

    • B가 A를 포함하거나 참조한다
    • B가 A를 기록한다
    • B가 A를 긴밀하게 사용한다
    • B가 A를 초기화하는데 필요한 데이터를 갖고 있다(이 경우 B는 A에 대해 정보 전문가

    다른 객체의 함수를 부를 때(메시지를 전송할 때)는 보내는 쪽에 맞춰서 메시지를 작성한다. 내부 구현을 노출시키지 않는다.

    변경에 취약한 클래스는 작성하면 안된다. 코드를 수정해야 하는 이유를 하나 이상 가지면 안된다. ← 응집도 낮음. 해결방법 → 변경의 이유에 따라 클래스를 분리해야 한다.

    질문: 응집도가 낮으면 왜 문제가 될까?

    변경의 이유 > 1인 클래스 찾기

    1. 인스턴스 변수가 초기화되는 시점을 찾기

      1. 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다.
    2. 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보기

    POLYMORPHISM패턴 → 객체의 암시적인 타입에 따라 행동을 분기해야 할 때. 그 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눈다.

    사용하는 측에서 동일한 인터페이스를 가진 객체라면 새로운 무언가가 얼마나 생기든 영향받지 않는다. 이것이 protected variations 패턴.

    변경이 될 가능성이 높은가? 그러면 캡슐화하라.

    하나의 클래스가 여러 타입의 행동을 구현하고 있나? → 다형성 패턴

    예측 가능한 변경으로 인해 여러 클래스들이 불안정해지나? → protected variations 패턴

    변경과 유연성

    책임 주도 설계의 대안

    책임 주도 설계는 어렵다. 숙련된 프로그래머라도 적절한 책임과 객체를 선택하는 일에 어려움을 느낀다.

    그럴 때는 우선 최대한 빠르게 목적한 기능을 수행하는 코드를 작성한다. 일단 실행되는 코드를 작성한 후, 명확하게 드러나는 책임을 올바른 위치로 이동시킨다. 대신, 겉으로 드러나는 동작에는 변경이 없어야 한다. 이것을 리팩토링이라 부른다.

    6장

    협력: 어떤 객체가 다른 객체에게 무언가를 요청하는 것

    클라이언트: 메시지 전송자

    서버: 메시지 수신자

    디미터 법칙

    • 오직 하나의 . 을 이용하라. 인접하지 않은 객체에게는 말하지 마라
    • This객체
    • 메서드의 매개변수
    • This의 속성
    • This의 속성인 컬렉션의 요소
    • 메서드 내에서 생성된 지역 객체

    묻지 말고 시켜라

    절차적인 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다.

    설계 원칙을 맹목적으로 따르지 마라

    1. 어떤 객체가 다른 객체의 상태를 가져와서 로직을 구성한다고 해보자. 다른 객체의 상태에 직접 접근하므로 그 객체의 캡슐화를 깨는 것 처럼 보인다. 그래서 해당 로직을 그 객체로 옮겼다. 이게 때에 따라서는 옳지 못할 수도 있다. 옮기긴 했지만, 그 객체의 책임이 아닐 수도 있기 때문이다.
    2. 가끔은 묻는것 말고 다른 대안이 없을 수도 있다. Foreach로 루프를 돌 때, 그 객체에게 묻는 것 말고 대안이 있는가?
    3. 설계는 트레이드오프다. 때에 따라 다르다.

    CQRS

    • 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
    • 객체의 정보를 변경하는 쿼리는 상태를 변경할 수 없다.

    참조 투명성

    수학과 프로그래밍의 차이: 부수효과의 존재 유무.

    • X = X + 1 는 대입 연산자
    • 같은 input에 따라 값이 매번 달라질 수 있는 함수

7장 객체 분해

전역변수를 이용한 하향식 시스템의 끔찍함.

시스템의 어떤 전역 변수가 있고. 프로그램은 일정한 절차에 의해서 기술되어 있으며. 그 절차는 Top level → Leaf node로 점점 세부적인 역할을 수행하도록 분해되어있는 시스템은,

  1. 전역 변수에 데이터를 추가하거나
  2. 처음에 기대했던 탑-다운 구조에 새로운 기능이 추가되면

전역변수를 사용하고 있는 모든 사용처를 뒤져야 하고, 모든 구조를 싹 다 바꿔야 한다. 그리고 그 중에서 하나라도 놓치는 순간 버그가 바로 생긴다. 이것은 꼼꼼함의 영역이 아니라 운의 영역이다.

해결방법: 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다. 데이터와 함께 변경되는 부분을 하나의 구현 단위로 묶고, 외부에서는 제공되는 함수만 이용해 데이터에 접근해야 한다. 즉, 잘 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 한다.

8장: 의존성 관리하기

객체지향은 객체들끼리 협력해야 한다. 작고 응집력 있는 객체들이 서로 의사소통 해야 하기 때문에, 그 의사소통 하려면 그런 객체가 존재한다는 사실을 알아야 한다. 이게 의존성이 된다. 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. 우리는 의존성을 관리하고, 객체가 변화를 받아들일 수 ㅇ있게 의존성을 정리해야 한다.

어떤 클래스가 추상화되지 않은 구체 클래스 여러개에 의존하는게 안좋은 이유: 전체적인 결합도를 높일뿐만 아니라 새로운 정책을 추가하기 어렵게 만들기 때문이다.

어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다. 컴파일타임의 구조와 런타임의 구조가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.

클래스가 사용될 특정한 문맥에 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기 수월해진다. → 컨텍스트 독립성.

컴파일타임 의존성 → 런타임 의존성 전환: 의존성 해결

  • 객체를 생성하는 시점에 생성자를 통해
  • 객체 생성 후 setter를 통해
  • 메서드 실행 시 인자를 통해

바람직한 의존성과 그렇지 않은 의존성의 차이 → 재사용성. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면, 그 의존성은 바람직하지 못한 것.

의존성은 명시적으로 표현돼야 한다. 퍼블릭 인터페이스를 통해 의존성을 드러내라.

휼륭한 설계 → 올바른 객체가 올바른 책임을 수행하게 하는 것.

가끔은 생성해도 무방하다

예를 들어, A 클래스가 B 구체 클래스와 자주 협업하고 C 구체 클래스와는 별로 협업하지 않을 때, P가 B,C를 추상화하는 추상 클래스인 경우.

생성자를 2개 만들어서 B클래스를 생성해도 된다.

설계는 트레이드오프 활동이다. 구체 클래스에 의존하게 되더라도, 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 설계할 수 있다. 하지만, Factory 패턴으로 모든 결합도가 모이는 새로운 클래스를 사용해서 이 문제를 해결할 수도 있다.

예외 케이스가 생길 때: if 문으로 해결하지 말고, 그 예외 케이스에 맞는 협업 객체를 만들어서 그걸로 협업하게 해라.

조합 가능한 행동

유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what) 을 하는지를 표현하는 클래스들로 이루어져 있다. 따라서, 클래스의 인스턴스를 생성하는 코드를 보는 것 만으로 객체가 무슨 일을 하는지를 쉽게 파악할 수 있다. 다시 말해 객체의 행동을 선언적으로 정의할 수 있는 것이다.

객체의 구성을 변경해 (절차적인 코드를 작성하기 보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 바꿀 수 있어야 한다.