오브젝트를 읽고

Hyun·2023년 3월 5일
1

공부한거 정리

목록 보기
6/20
post-thumbnail

오브젝트 (조영호 님) 를 읽고


정리한 Chapter

  • 객체, 설계

  • 객체지향 프로그래밍

  • 역할, 책임, 협력

  • 설계 품질과 트레이드 오프

  • 책임 할당하기

  • 메시지와 인터페이스

  • 객체 분해

  • 의존성 관리하기

  • 유연한 설계

  • 상속과 코드 재사용

  • 합성과 유연한 설계

  • 다형성

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

  • 일관성 있는 협력

  • 디자인 패턴과 프레임워크


객체, 설계


나쁜 설계의 문제점

https://github.com/ghkdgus29/object-code/tree/main/chapter1/bad

나쁜 설계 구현 (bad 패키지)


  • 모듈의 3가지 목적
    • 제대로 실행되어야 한다.
    • 변경이 용이해야 한다.
    • 이해하기 쉬워야 한다.

bad 패키지는 빨간색 부분을 만족시키지 못한다.


  • 이해가 어렵다.

    • 동작이 우리의 예상을 크게 벗어난다.

    • 여러 세부적인 내용을 기억해야 한다.

      • get 매서드를 체이닝하기 위해서 각 객체들이 어떤 내부필드를 갖는 지 알아야 한다.
Ticket ticket = ticketSeller.getTicketOffice().getTicket();

  • 변경이 어렵다.
    • Theater 가 너무 많은 세부정보를 알고 있다.
    • 그래서 세부정보 중 한가지라도 바뀌면, Theater 의 코드도 변경해야 한다.

  • bad 패키지 클래스 다이어그램

의존성 (dependency)

  • A 객체B 객체의 세부정보를 알고있을 때,

  • B 객체 세부정보 변경에 의해 A 객체 코드도 변경해야 한다면,

  • A 객체B 객체의존성이 있다고 표현

  • bad 패키지Theater 클래스는 Audience, Bag, TicketOffice, Ticket 에 의존하고 있다.


bad 패키지 개선방법

  • Theater 의 너무 많은 의존관계들을 끊어낸다.

    • 결합도 ⬇
  • 각 객체들이 스스로 문제를 해결할 수 있는 자율적인 존재로 만든다.


좋은 설계로 바꾸기

https://github.com/ghkdgus29/object-code/tree/main/chapter1/good

좋은 설계 구현 (good 패키지)

  • 캡슐화
    • 객체 내부의 세부적인 사항을 감추는 것
      • 객체와 객체 사이 결합도를 낮춘다.
      • 이를 통해 변경하기 쉬운 객체를 만들 수 있다.

  • 인터페이스 의존
    • 객체를 인터페이스와 구현으로 나누고,
    • 인터페이스만을 외부에 공개한다.

public class Theater {
	
    private TicketSeller ticketSeller;

    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}
  • TheaterTicketSeller 의 자세한 구현은 알지 못한다.
  • TicketSellerenter(Audience audience) 라는 인터페이스만을 Theater 에 제공한다.
  • TicketSeller 의 내부는 캡슐화되었다.

  • 정리
    • 객체 내부의 상태를 캡슐화
    • 객체 간에 오직 메시지를 통해서만 상호작용하도록 설계

응집도 (cohesion)

  • 밀접하게 연관된 작업만을 수행하고

  • 연관성 없는 작업은 다른 객체에게 위임하는 객체를

  • 응집도가 높다고 말한다.

  • 자신의 데이터를 스스로 처리하는 자율적인 객체 ➔ 응집도 ⬆ , 결합도 ⬇


절차지향 vs 객체지향

  • 절차적 프로그래밍 (Procedural Programming)

    • 프로세스와 데이터를 별도의 모듈에 위치시키는 방식

    • bad 패키지 에서

      • Theater 는 작업을 처리하는 프로세스
      • 그 외 클래스 들 작업에 필요한 데이터
    • 프로세스가 모든 데이터에 의존한다.

      • 데이터의 변경으로 인한 영향을 크게 받는다.
      • 책임이 Theater 에 집중


  • 객체지향 프로그래밍 (Object-Oriented Programming)

    • 프로세스와 데이터를 동일한 모듈에 위치시키는 방식

    • good 패키지 에서

      • 객체들은 자신의 문제를 스스로 처리
      • 각 객체는 자신을 스스로 책임진다.
    • 의존성이 적절히 관리된다.

      • TheaterTicketSeller
      • TicketSellerAudience
      • 하나의 변경이 여러 클래스로 전파되지 않는다.


훌륭한 객체지향설계

  • 객체에 적절한 책임을 할당해야 한다.

  • 쉽게 변경할 수 있는 설계가 중요하다.

    • 의존성은 변경을 어렵게 한다.
    • 캡슐화를 통해 몰라도 되는 세부사항을 내부로 감춰, 불필요한 의존성을 제거
      • 객체간 결합도 ⬇
      • 객체의 자율성 ⬆
      • 응집도 높은 객체 공동체를 만든다.

객체지향 프로그래밍


https://github.com/ghkdgus29/object-code/tree/main/chapter2/good

  • Chapter 2 예제 구현

객체지향

  • 어떤 클래스가 필요한 지 고민하기 전에 어떤 객체들이 필요한지 고민한다.

  • 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다.


자율적인 객체

  • 객체는 상태와 행동을 가지는 복합적인 존재

    • 상태와 행동을 객체 내부로 함께 묶는 것을 캡슐화라 한다.
  • 객체는 스스로 판단하고 행동하는 자율적인 존재

  • 인터페이스와 구현의 분리

    • 퍼블릭 인터페이스

      • 외부에서 접근 가능 (public)
      • 변경 가능성 ⬇
    • 구현

      • 외부에서 접근 불가 (private, protected)
      • 내부에서만 접근 가능
      • 변경 가능성 ⬆
    • 객체의 상태는 숨기고 행동만 외부에 공개해야 한다.


협력

  • 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용

  • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지 전송

    • 메시지를 수신한 객체는 자신만의 방법 (메서드) 메시지를 처리한다.

컴파일 시간 의존성 vs 실행 시간 의존성

  • 코드상으로는 Movie 가 추상 클래스인 DiscountPolicy 에 의존

    컴파일 시간 의존성


  • 실행시점에는 Movie 인스턴스를 생성할 때 넣어준 구체적인 클래스(인자) 에 의존
        Movie avatar = new Movie("아바타",
                Duration.ofMinutes(120),
                Money.wons(10000),
                new AmountDiscountPolicy(Money.wons(800),  // DiscountPolicy 가 아닌 AmountDiscountPolicy 에 의존
                        new SequenceCondition(1),
                        new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59))));
  • 고정할인 적용

        Movie titanic = new Movie("타이타닉",
                Duration.ofMinutes(180),
                Money.wons(11000),
                new PercentDiscountPolicy(0.1,		// DiscountPolicy 가 아닌 PercentDiscountPolicy 에 의존
                        new PeriodCondition(DayOfWeek.TUESDAY, LocalTime.of(14, 0), LocalTime.of(16, 59)),
                        new SequenceCondition(2)));
  • 비율할인 적용

  • 코드의 의존성과 객체 사이 의존성이 동일하지 않을 수 있다.
    • 객체지향의 특징
    • 코드를 이해하기 어렵다.
    • 설계가 유연해지고 확장 가능성이 높다.
      • 새로운 할인정책이 DiscountPolicy 를 상속한다면,
      • 새로운 할인정책을 추가한다 해도 Movie 의 코드 수정은 발생하지 않는다.

상속과 인터페이스

인터페이스 : 객체가 이해할 수 있는 메시지 목록

  • 부모 클래스의 모든 인터페이스를 자식 클래스가 물려 받는다.

    • 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지 수신 가능

    • 메시지 전송 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. (업캐스팅)

    • 즉, 메시지 전송 객체는 메시지 수신 객체가 어떤 인스턴스인지 관심이 없다.

    • 내가 보내는 메시지를 이해할 수 있는지만 관심이 있다.


다형성

메시지 전송 객체는 동일한 메시지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지 수신 객체의 클래스가 무엇이냐에 따라 달라지는 것

  • 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있기 때문에 발생

  • 다형적인 협력에 참여하는 객체들은 인터페이스가 동일해야 한다.

    • 상속을 통해 이를 구현한다.
  • 객체지향은 메시지와 메서드를 실행시점에 바인딩(Dynamic binding) 하기 때문에 다형성을 구현할 수 있다.


상속의 분류

  • 구현 상속 (subclassing)

    • 단순한 코드 재사용
    • 변경에 취약하므로 추천하지 않는다.
  • 인터페이스 상속 (subtyping)

    • 부모 자식 간의 인터페이스를 공유
    • 다형적인 협력이 가능해지므로 추천하는 사용법

추상화

복잡한 자료, 모듈로 부터 핵심적인 개념, 기능을 간추려 내는 것

  • 상위 클래스는 하위 클래스보다 추상적

  • 장점

    • 설계사항을 간략하고 직관적으로 서술할 수 있다.
    • 새로운 기능의 확장이 쉬워진다.
      • 설계가 유연해진다.

코드 재사용

  • 코드 재사용을 위해 상속보다는 합성을 사용해라.

합성 : 다른 객체 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용

  • 코드 재사용을 위한 상속의 단점

    • 캡슐화 위반

      • 자식 클래스는, 부모 클래스의 내부 구조를 잘 알아야 한다.

      • 부모 클래스의 구현이 자식 클래스에게 노출된다.

      • 부모 클래스의 변경은 자식 클래스의 변경을 낳는다.

    • 설계의 유연성 ⬇

      • 상속은 컴파일 시점에 부모 클래스와 자식 클래스가 강하게 결합한다.

      • 따라서 실행 시점에는 객체 종류를 변경할 수 없다.


  • 코드 재사용을 위한 합성

    • 인터페이스를 통해 약하게 결합한다.

    • 캡슐화

      • 인터페이스를 통해서만 재사용하기 때문에 재사용 코드의 내부 구조를 잘 알 필요 없다.
    • 설계의 유연성 ⬆


역할, 책임, 협력

협력

  • 객체들이 애플리케이션 기능을 구현하기 위해 수행하는 상호작용

  • 오로지 메시지 전송만을 사용해 객체간 협력할 수 있다.

  • 협력이 객체의 행동을 결정한다.

    • 행동은 객체의 상태를 결정한다.
    • 따라서 협력이 객체의 행동, 상태를 모두 결정한다.

책임

  • 협력에 참여하기 위해 객체가 수행하는 행동

  • 객체의 책임은 2가지로 나뉜다.

    • 무엇을 알아야하는가 ➔ 상태
    • 무엇을 해야하는가 ➔ 행동
  • 책임을 수행하는 데 필요한 정보를 가장 잘 알고있는 정보전문가에게 책임을 할당해야 한다.


책임 주도 설계

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악

  • 시스템 책임을 더 작은 책임으로 분할

  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당

  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우

    • 이를 책임질 적절한 객체 또는 역할을 찾는다.
    • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

책임 할당 시 2가지 고려사항

  • 메시지를 먼저 정하고, 처리할 객체를 나중에 선택한다.

    • 이를 통해 적절한 크기의 퍼블릭 인터페이스를 갖게 된다.
    • 이를 통해 추상적인 인터페이스를 갖게 된다.
      • 객체가 무엇을 하는지는 노출하되, 어떻게 하는지는 노출하지 않는다.
  • 행동이 상태를 결정한다.

    • 객체는 행동이 제일 중요

역할

  • 특정한 협력안에서의 책임의 집합

  • 연극으로 치면 로미오는 역할이고, 로미오를 연기하는 배우들은 구체적인 객체와 같다.

  • 즉, 역할은 구체적인 객체들을 포괄하는 추상화

    • 추상클래스와 인터페이스 사용

설계 품질과 트레이드 오프


https://github.com/ghkdgus29/object-code/tree/main/chapter4/bad

데이터 중심 설계

  • 데이터 중심의 설계는 변경에 취약
    • 너무 이른 시기에 데이터에 관해 결정한다.
    • 협력이라는 문맥을 고려하지 않고, 객체를 고립시킨 채 오퍼레이션을 결정한다.

객체지향설계 2가지 방법

  • 데이터 (상태) 중심 객체 분할

    • 객체는 자신의 데이터를 조작하기 위한 오퍼레이션을 정의

    • 객체의 상태에 집중

    • 퍼블릭 인터페이스에 데이터와 관련된 세부사항이 노출되기 쉽다.

      • 캡슐화 위반
      • 데이터는 불안정해 자주 변경되므로, 인터페이스에 의존하는 모든 외부 객체들도 변경의 영향을 받는다.
      • ex) getter, setter
  • 책임 중심 객체 분할

    • 다른 객체가 요청하는 오퍼레이션을 처리하기 위한 데이터 보관

    • 객체의 행동에 집중

    • 책임을 드러내는 안정적인 퍼블릭 인터페이스

      • 책임을 수행하는 데 필요한 상태를 캡슐화
      • 구현 변경에 대한 파장이 외부로 퍼져 나가지 않는다.

캡슐화

  • 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화 (추상화)

  • 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 숨기는 것

  • 변경의 영향을 통제한다.


응집도와 결합도

  • 응집도

    • 모듈 내의 요소들이 연관되어 있는 정도

    • 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 응집도 ⬆

    • 모듈 내의 요소들이 서로 다른 목적을 추구한다면 응집도 ⬇

  • 결합도

    • 어떤 모듈이 다른 모듈에 대해 얼마나 의존하는지에 대한 척도

    • 어떤 모듈이 다른 모듈에 대해 너무 자세히 알고 있다면 두 모듈의 결합도 ⬆

    • 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면, 두 모듈은 결합도 ⬇

    • 어떤 모듈의 내부구현 변경이 다른 모듈에 영향을 미치는 경우에도 결합도 ⬆

      • 인터페이스 변경시에만 영향을 미치면 결합도 ⬇

좋은 설계란

  • 높은 응집도와 낮은 결합도

  • 캡슐화를 지키면 모듈의 높은 응집도와 낮은 결합도는 따라온다.

  • 따라서 캡슐화에 집중하라.


단일 책임 원칙 (SRP)

  • Single Responsibility Principle

  • 클래스는 단 한가지의 변경 이유만을 가져야 한다.

  • 서로 다른 이유로 변경되는 코드가 하나의 클래스안에 공존해선 안된다. (낮은 응집도)


캡슐화의 진정한 의미

  • 단순히 객체 내부의 데이터를 감추는 것은 데이터 캡슐화 로 캡슐화의 한 종류이다.

  • 진정한 캡슐화란 변할 수 있는 건 어떤 것이라도 감추는 것을 의미한다.

    • 메서드 시그니처도 내부 구현을 노출시킬 수 있다.
public class DiscountCondition {
    
    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public DiscountConditionType getType() {
        return type;
    }

    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        // ...
    }
    
    public boolean isDiscountable(int sequence) {
        // ...
    }
}
  • getType()DiscountConditionType 을 내부에 포함하고 있음을 노출
  • isDiscountable()DayOfWeek, LocalTime, sequence 정보 를 내부에 포함하고 있음을 노출

public class Movie {
    
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;

    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;

    public MovieType getMovieType() {
        return movieType;
    }
    
    public Money calculateAmountDiscountedFee() {
        // ...
    }
    
    public Money calculatePercentDiscountedFee() {
        // ...
    }
    
    public Money calculateNoneDiscountedFee() {
        // ...
    }
}
  • 할인 정책의 종류를 인터페이스를 통해 외부에 노출시키고 있다.
  • 만약 할인정책이 수정된다면, 해당 메서드에 의존하는 모든 객체들은 영향을 받는다.

책임 할당하기

https://github.com/ghkdgus29/object-code/tree/main/chapter5

책임 주도 설계


데이터 중심 설계 vs 책임 중심 설계

  • 앞서 살펴본 데이터 중심 설계는 협력을 고려하지 않고, 고립된 객체 상태에 초점을 맞추기에 다양한 문제점이 발생한다.

    • 캡슐화를 위반하기 쉽다.
    • 요소들 사이 결합도가 높다.
    • 코드 변경이 어려워진다.
  • 책임 중심 설계를 통해 이러한 문제점들을 해결할 수 있다.


책임 중심 설계의 2가지 원칙

  • 데이터보다 행동을 먼저 결정하라

    • 데이터는 행동을 수행하는데 필요한 재료를 제공할 뿐이다.
  • 협력이라는 문맥안에서 책임을 결정하라

    • 행동이 곧 객체의 책임이다.

책임 결정 방법

  • 객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다.

    • 책임은 객체의 입장이 아니라, 객체가 참여하는 협력에 적합해야 한다.
  • 클라이언트가 보낼 메시지를 결정한 후, 메시지를 수신할 객체를 선택한다.

    • 객체가 메시지를 선택 X

    • 메시지가 객체를 선택 O

    • 메시지가 존재하기 때문에 메시지를 처리할 객체가 필요한 것이다.

    • 클라이언트는 메시지 수신자에 대한 어떠한 가정도 할 수 없기 때문에 수신자의 내부구현이 깔끔하게 캡슐화된다.


책임 할당 방식

  1. 설계 시작전 도메인 개념들을 간단하게 그려보기

  1. 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 간주

    • 이 책임을 처리할 메시지 결정

    • 메시지를 전송할 객체는 무엇을 원하는지 고려


  1. 메시지를 처리할 적합한 객체를 선택

    • 메시지를 수신할 적합한 객체는 누구인지 고려

    • 책임을 수행하는데 필요한 정보를 알고 있을만한 정보전문가에게 메시지를 처리할 책임을 할당한다.


  1. 선택된 객체가 메시지를 처리하는 (책임을 수행하는) 작업 흐름을 생각해본다.
    • 스스로 처리할 수 없는 작업이 있다면, 외부에 도움을 요청해야 한다.
      • 외부에 전송하는 새로운 메시지가 된다.
      • 이 메시지는 새로운 객체의 책임이 된다.

이와 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성된다.


낮은 응집도를 가진 클래스의 패턴

  • 응집도가 낮다는 것은 변경의 이유가 하나 이상이라는 걸 말한다.

  • 인스턴스 변수가 초기화되는 시점이 다르다.

    • 일부만 초기화되고, 일부는 초기화되지 않는다.
  • 모든 메서드가 객체의 모든 속성을 사용하지 않는다.

    • 메서드별로 일부 속성 그룹만 사용한다.

다형성 패턴

  • 객체의 타입에 따라 변하는 행동이 있다면

  • 타입을 명시적인 클래스로 분리하고

  • 변화하는 행동을 각 타입의 책임으로 할당하는 패턴

public interface DiscountCondition {

    boolean isSatisfiedBy(Screening screening);
}

타입을 명시적인 클래스로 분리


public class PeriodCondition implements DiscountCondition{ ... }
  
public class SequenceCondition implements DiscountCondition{ ... }

변화하는 행동을 각 타입의 책임으로 할당

이 경우 새로운 DiscountCondition 타입을 추가하더라도, Movie가 영향을 받지 않는다.


메시지와 인터페이스

https://github.com/ghkdgus29/object-code/tree/main/chapter6

명령-쿼리를 분리하지 않아 버그가 발생하는 코드


  • 객체지향 애플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지다.

  • 객체가 수신하는 메시지가 객체의 퍼블릭 인터페이스가 된다.


메시지와 메서드

  • 메시지를 수신했을 때, 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇이냐에 달려있다.

    • 이때 실제로 실행되는 함수를 메서드라 한다.
  • 전통적인 메서드 호출과 메시지 전송은 다르다.

  • 메시지 전송은 송수신자가 느슨하게 결합된다.

    • 전송자는 자신이 전송할 메시지만 알면된다.

    • 수신자는 누가 전송했는지 알 필요없이 메시지를 받았음만 알면 된다.

    • 송수신자 간의 결합도를 낮춘다.


퍼블릭 인터페이스와 오퍼레이션

  • 객체가 의사소통을 위해 외부에 공개하는 메시지 집합 = 퍼블릭 인터페이스

  • 퍼블릭 인터페이스에 포함된 메시지 = 오퍼레이션

  • 메시지를 수신했을 때 실제 실행되는 코드 = 메서드


  • DiscountConditonisSatisfiedBy(screening) = 오퍼레이션
  • SequenceCondition, PeriodConditionisSatisfiedBy(screening) = 실제 구현을 포함하는 메서드

  • 인터페이스의 각 요소는 오퍼레이션
  • 오퍼레이션은 추상화
  • 메서드는 오퍼레이션의 구현
  • 따라서 다형성이란 동일한 오퍼레이션 호출에 대해 서로 다른 메서드들이 실행되는 것

시그니처

  • 메서드의 이름과 파라미터 목록을 합쳐 시그니처라고 부른다.

  • 오퍼레이션은 실행코드 없이 시그니처만을 정의한 것


퍼블릭 인터페이스의 품질을 높이는 방법

  • 디미터 법칙

  • 묻지 말고 시켜라

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

  • 명령 - 쿼리 분리


디미터 법칙

  • 객체의 내부구조에 강하게 결합되지 않도록 협력 경로를 제한하라

  • "오직 인접한 이웃하고만 말하라."

  • "오직 하나의 도트만 사용하라."

  • 클래스 내부의 메서드는 아래 해당하는 인스턴스에만 메시지를 전송해야 한다.

    • 클래스의 속성

    • 메서드의 매개변수

    • 메서드 내에서 생성된 지역 객체

  • 메시지 수신자의 내부구조가 전송자에게 노출 X

    • 클라이언트와 서버사이 낮은 결합도를 유지

묻지 말고 시켜라

  • 훌륭한 메시지는 객체의 내부구조를 묻는 메시지가 아니라, 원하는걸 시키는 메시지이다.

  • 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체하라.

  • 이를 통해 정보전문가에게 책임을 할당하게 된다.


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

  • 작업을 어떻게 수행하는지 드러내는 메서드는 좋지 않다.

    • 협력하는 객체의 종류를 노출한다.

    • 메서드 명으로 내부 구현을 설명하기 때문에 캡슐화를 위반

public class PeriodCondition {
	public boolean isSatisfiedByPeriod(Screening screening) { ... } 
}

public class SequenceCondition {
	public boolean isSatisfiedBySequence(Screening screening) { ... } 
}
  • 할인 여부 조건이 변경된다면, 클라이언트가 호출하는 메서드를 변경해야 한다.
    • 변경에 취약하다.

  • 작업이 무엇을 수행하는지 드러내는 메서드가 좋다.

    • 이해하기 쉽다.

    • 설계의 유연성을 향상시킨다.

public interface DiscountCondition {

    boolean isSatisfiedBy(Screening screening);
}

  
  
public class PeriodCondition implements DiscountCondition{

    @Override
    public boolean isSatisfiedBy(Screening screening) { ... }
}

public class SequenceCondition implements DiscountCondition{

    @Override
    public boolean isSatisfiedBy(Screening screening) { ... }
}
  • 두 메서드는 동일한 메시지를 서로 다른 방법으로 처리한다.
  • 두 객체를 동일한 타입 계층으로 묶으면, 두 메서드는 서로 대체 가능하다.

  • 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다.

명령 - 쿼리 분리 원칙

  • 루틴 : 기능 모듈, 아래의 개념들을 포함한다.

    • 프로시저 : 내부 상태 변경 O (부수 효과 O), 반환값 X

    • 함수 : 내부 상태 변경 X (부수 효과 X), 반환값 O

  • 명령

    • 객체의 상태를 수정하는 오퍼레이션

    • 프로시저와 유사

    • 반환값 X

    • 부수 효과에 주의해야 한다.

  • 쿼리

    • 객체와 관련된 정보를 반환하는 오퍼레이션

    • 함수와 유사

    • 반환값 O

    • 부수 효과가 없으므로 다른 부분에 영향을 주지 않는다.

    • 참조 투명성을 만족한다.

      • 참조 투명성: 동일한 입력값에 대해 항상 동일한 출력값을 만족하는 것
  • 오퍼레이션은 명령인 동시에 쿼리여서는 안된다.

  • 명령과 쿼리가 뒤섞이면 실행 결과를 예측하기 어렵다.

    • 버그를 양산한다.

객체 분해

https://github.com/ghkdgus29/object-code/tree/main/chapter7

  • procedure : 프로시저 추상화
  • module : 모듈 추상화
  • abstractdata : 추상 데이터 타입
  • clasz : 클래스 , 객체지향

추상화

  • 불필요한 정보를 제거하고 현재 문제 해결에 필요한 핵심만 남기는 작업

  • 일반적인 추상화 방법은 한 번에 다뤄야하는 문제의 크기를 줄이는 것

  • 큰 문제를 해결가능한 작은 문제로 나누는 것 = 분해 (decomposition)


추상화 메커니즘

  • 프로시저 추상화

    • 소프트웨어가 무엇을 해야 하는지 추상화
    • 기능 분해
  • 데이터 추상화

    • 소프트웨어가 무엇을 알아야 하는지 추상화
    • 다시 2가지로 나뉜다.
      • 데이터를 중심으로 타입 추상화
      • 데이터를 중심으로 프로시저 추상화 = (객체지향)

즉 객체지향이란 프로시저 추상화와 데이터 추상화를 통합한 객체들을 만들어 시스템을 분해하는 것


전통적인 추상화 방식 - 기능 분해

  • 시스템을 프로시저 단위로 분해

  • 시스템은 하나의 커다란 메인 함수

    • 작은 단계의 하위 기능 함수로 분해

전통적인 추상화 방식 - 기능 분해의 문제점

  • 현대적인 시스템은 동등한 수준의 다양한 기능으로 구성된다.

    • 실제 시스템에 정상 (top) 이란 존재하지 않는다.
  • 비즈니스 로직과 사용자 인터페이스가 설계 초기부터 밀접하게 결합된다.

    • 둘은 변경빈도가 다르다.
    • 기능 분해의 경우, 사용자 인터페이스 변경이 비즈니스 로직에 영향을 준다.
  • 성급하게 실행 순서를 결정한다.

    • 시스템이 what 보다 how 에 집중하게 된다.
  • 하위 함수는 상위 함수가 강요하는 문맥에 종속적이다.

    • 하위 함수의 재사용성이 떨어진다.
    • 결합도가 높아 사소한 변경에도 크게 요동친다.
  • 어떤 데이터를 어떤 함수가 사용하고 있는지 알기 어렵다.

    • 데이터 변경으로 어떤 함수가 영향을 받을지 예상하기 어렵다.
  • 변경에 취약한 설계


추상화 방식 - 모듈

  • 변경되는 부분은 은닉시키고 안정적인 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것

  • 복잡한 부분은 내부로 숨기고 외부엔 모듈을 추상화하는 간단한 인터페이스만 제공

  • 기능이 아니라 변경의 정도에 따라 시스템을 분해

    • 모듈 내부 응집도 ⬆, 결합도 ⬇
  • 장점

    • 모듈 내부 변수가 변경되더라도 모듈 내부에만 영향을 미친다.

    • 비즈니스 로직과 사용자 인터페이스를 분리한다.

    • 네임스페이스 오염을 방지한다.

      • 변수와 함수를 모듈 내부에 포함시키기 때문에 다른 모듈에서도 동일한 이름을 사용가능
  • 단점

    • 인스턴스 개념을 제공하지 않는다.

추상화 방식 - 추상 데이터 타입

  • 타입 : 변수에 저장할 수 있는 내용물의 종류

  • 언어가 기본적으로 제공하는 내장 타입으로는 프로그램을 표현하는데 한계가 있다.

  • 데이터를 상태와 행위를 가지는 독립적인 객체로 표현

    • 좀 더 개념적으로 사람들의 사고방식에 가깝다.
  • 특징

    • 타입을 정의할 수 있다.

    • 타입 인스턴스의 오퍼레이션을 정의할 수 있다.

    • 타입이 같은 여러개의 인스턴스들을 생성할 수 있다.

  • 한계점

    • 데이터만 독립적인 객체로 분리했을 뿐, 핵심 로직은 추상 데이터 타입 외부에 존재

    • 타입 내에서 객체 분류 기준 필드가 필요하다.

      • 객체 분류 기준을 사용하여 오퍼레이션 내에서 분기해야 한다.
  • 추상 데이터 타입과 클래스는 다르다.

    • 추상 데이터 타입은 상속과 다형성을 지원하지 않는다.

    • 객체기반 프로그래밍이라고도 부른다.


추상화 방식 - 클래스

  • 상속과 다형성을 지원한다.

    • 객체 지향 프로그래밍의 특징
  • 클래스는 절차를 추상화한다.

추상 데이터 타입은 오퍼레이션을 기준으로 실제 타입을 분류한다.


객체지향은 타입을 기준으로 오퍼레이션을 묶는다.


  • 타입 변수를 이용한 조건문을 사용하지 않는다.
    • 다형성으로 대체한다.

  • 추상 데이터 타입 ➔ 클래스
  • Employee 를 상속하면, 얼마든지 새로운 직원 유형을 구현할 수 있다.
  • ClientEmployee 에만 의존한다.
    • 구체적인 SalariedEmployeeHourlyEmployee 에 의존 X
    • Client 의 코드 변경 X

이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가하는 특성을 개방 - 폐쇄 원칙이라 한다. (Open - Closed Principle)


의존성 관리하기


https://github.com/ghkdgus29/object-code/tree/main/chapter8/good

컨텍스트 확장 NoneDiscountPolicy, OverlappedDiscountPolicy


  • 협력을 위해 의존성은 필수적이다.

  • 그러나 과도한 의존성은 설계의 유연성을 떨어뜨린다.

  • 따라서 의존성을 적절하게 관리해야 한다.


의존성

  • 어떤 객체가 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 한다면

    • 두 객체 사이에 의존성이 존재한다고 말한다.
  • 의존성은 항상 단방향이다.

의존하는 요소의존되는 요소


  • 의존되는 요소의 변경은 의존하는 요소도 변경될 수 있음을 의미

의존성 전이

  • PeriodCondition 은 잠재적으로 Movie 에 의존

  • 의존이 연쇄적으로 전파될 수 있다.

  • 중간의 객체가 내부구현을 효과적으로 캡슐화했다면 변경은 전파되지 않는다.


컨텍스트 독립성

  • 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다.

    • 구체적인 클래스를 안다면 특정한 문맥에 강하게 결합된다.

    • 구체적으로 알게되면 다른 문맥에서의 재사용이 어려워진다.

  • 클래스가 자신과 협력할 객체의 추상적인 인터페이스에 대해서만 안다면

    • 다른 문맥에서의 재사용이 수월

    • 이를 컨텍스트 독립성이라 함

  • 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.

    • 이를 통해 설계는 더 유연해진다.

의존성 해결하기

  • 컴파일 타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다.

    • 이를 의존성 해결이라 한다.
  • 의존성 해결 방법 3가지

    • 생성자

    • setter

    • 메서드 인자

      • 일시적으로 의존하는 경우 사용

추상화

  • 바람직한 의존성 == 느슨한 결합도

    • 이를 위해 객체는 추상화에 의존해야 한다.
  • 구체 클래스 ➔ 추상 클래스 ➔ 인터페이스 에 의존할수록 결합도는 낮아진다.


숨겨진 의존성 vs 명시적인 의존성

  • 숨겨진 의존성
public class Movie {
	
  private DiscountPolicy discountPolicy;
  
  public Movie(String title, Duration runningTime, Money fee) {
  	...
  	this.discountPolicy = new AmountDiscountPolicy(...);
  }
}
  • MovieDiscountPolicy, AmountDiscountPolicy 에 의존한다.
  • 메서드 내부에서 인스턴스를 직접 생성하여, 의존성이 인터페이스에 표현되지 않는다.
  • 이를 숨겨진 의존성이라 한다.

  • 명시적인 의존성
public class Movie {
	
  private DiscountPolicy discountPolicy;
  
  public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }
}
  • 생성자를 사용해 모든 의존성을 해결
  • 메서드 내부에서 인스턴스를 직접 생성하지 않아, 의존성이 인터페이스에 모두 표현된다.
  • 이를 명시적인 의존성이라 한다.

정리

  • 사용과 생성의 책임을 분리한다.

    • 생성의 책임을 클라이언트로 옮긴다.
  • 의존성을 생성자에 명시적으로 드러낸다.

  • 구체 클래스가 아닌 추상 클래스에 의존하게 한다.

이를 통해 설계를 유연하게 만들 수 있다.


new Movie("아바타",
  Duration.ofMinutes(120),
  Money.wons(10000),
  new AmountDiscountPolicy(Money.wons(800),
  		new SequenceCondition(1),
  		new SequenceCondition(10),
  		new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(12, 0)),
  		new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(21, 0))));

클라이언트가 생성의 책임을 가진다.

훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.


유연한 설계

개방-폐쇄 원칙

  • Open-Closed Principle , OCP

  • 소프트웨어 개체는 확장에 대해 열려있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

  • 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.

  • 컴파일타임 의존성을 고정시키고 런타임 의존성을 확장하고 수정할 수 있는 구조는 OCP를 만족한다.

MovieDiscountPolicy 의 컴파일타임 의존성은 고정, 런타임 의존성은 확장


  • 추상화에 의존하는 것이 핵심

    • 추상화 부분은 다양한 상황에서의 공통점이므로 수정에 닫혀있다.

    • 추상화를 통해 생략된 부분은 확장의 여지를 남긴다.


생성 사용 분리

  • 객체의 생성과 사용이 공존하면 OCP를 위반하게 된다.

    • 숨겨진 의존성 코드 참고
  • 객체를 생성하는 책임은 클라이언트로 옮겨 생성과 사용을 분리할 수 있다.

    • 이 경우 클라이언트는 생성과 사용을 모두 하기는 한다.
  • 생성에만 특화된 FACTORY를 사용할 수도 있다.

    • 이 경우, 클라이언트가 특정한 클래스에 의존하지 않는다.
public class Factory {
 public Movie createAvatarMovie() {
    return new Movie("아바타",
       				Duration.ofMinutes(120),
  					Money.wons(10000),
  					new AmountDiscountPolicy(...));
  }
}

객체 생성만을 전담하는 Factory


public class Client {
  private Facotry factory;
  
  public Client(Factory factory) {
    this.factory = factory;
  }
  
  public Money getAvatarFee() {
    Movie avatar = factory.createAvatarMovie();
    return avatar.getFee();
  }
}

Client 가 필요로 하는 객체 생성을 Factory 에 위임할 수 있다.


이 경우 Client 는 사용의 책임만 가진다.


의존성 주입

  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 전달되어 의존성을 해결하는 것

  • 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내 필요한 의존성을 전달받을 수 있다.

    • 생성자 주입

    • setter 주입

    • 메서드 주입

  • 명시적이기 때문에 의존성을 파악하기 위해 코드 내부를 읽을 필요 없다.

    • 객체를 캡슐화
    • 의존성 관련 문제를 컴파일 타임에 찾을 수 있다.

숨겨진 의존성

  • SERVICE LOCATOR 패턴은 의존성을 감추는 패턴이다.

  • 의존성을 구현 내부로 감추면 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 발견된다.

    • 디버깅하기 어렵다.
  • 코드의 내부 구현을 이해할 것을 강요한다.

    • 클래스의 사용법을 익히기 위해 내부 구현을 샅샅이 뒤져야 한다면 캡슐화는 무너진 것

의존성 역전 원칙

  • 객체 간 협력할 때 협력의 본질은 상위 수준의 클래스가 갖고 있다.

  • 상위 수준 클래스는 하위 수준 클래스에 의존해선 안됨

    • 만약 상위 수준 클래스가 하위 수준 클래스에 의존하면 하위 수준의 변경에 상위 수준 클래스가 영향을 받는다.

상위 수준 클래스 Movie 가 하위 수준 클래스 AmountDiscountPolicy 에 의존


  • 상위 수준 클래스와 하위 수준 클래스 모두 추상화에 의존하도록 해야 한다.

상위 수준 클래스, 하위 수준 클래스 모두 추상클래스인 DiscountPolicy 에 의존


  • 이를 의존성 역전 원칙이라 한다.

    • Dependency Inversion Principle, DIP

    • 전통적인 소프트웨어 개발에선 상위 수준 모듈이 하위 수준 모듈에 의존한다.

    • 이러한 전통적인 의존성 구조를 뒤집기 때문에 DIP 라 부른다.


유연한 설계의 양면성

  • 유연한 설계는 변경하기 쉽고 확장하기 쉽다.

    • 반면 정적인 클래스 구조와 동적인 객체 구조의 차이 때문에 이해하기 어렵다.
  • 단순하고 명확한 설계는 이해하기 쉽다.

  • 따라서 미리 걱정하지 말고 유연성이 필요할 때 유연하게 설계하자


상속과 코드 재사용

https://github.com/ghkdgus29/object-code/tree/main/chapter10/inheritance

코드 재사용을 위해 상속을 사용


  • 중복 코드는 변경을 방해한다.

    • 중복 코드 결정 기준은 모양이 아니다.

    • 중복 여부를 판단하는 기준은 변경이다.

      • 요구사항이 변경될 때 두 코드를 함께 수정해야 한다면 두 코드는 중복이다.
  • 클래스를 재사용하기 위해 새로운 클래스를 추가하는 기법

    • 그러나 단순히 코드 재사용을 위해 상속하는 것은 바람직하지 않다.

코드 재사용을 위한 상속의 단점

  • 상속 계층의 계단을 내려갈 때마다 코드를 이해하기 어려워진다.

  • 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 구현방법을 정확하게 알아야 한다.

    • 부모 클래스와 자식 클래스간의 결합도를 높인다.

    • 부모 클래스의 수정은 수많은 자식 클래스의 수정을 동반한다.

      • 이를 취약한 기반 클래스 문제라 한다.

취약한 기반 클래스 문제

  • 부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상

  • 상속은 자식 클래스를 점진적으로 추가해 기능을 확장하는데는 용이하지만

    • 부모 클래스의 점진적인 개선을 어렵게 한다.
  • 객체지향의 기반은 캡슐화를 통한 변경의 통제

    • 상속은 캡슐화를 약화하고 부모클래스의 구현에 대한 결합도 ⬆
    • 객체지향의 기반을 무너뜨린다.
  • 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깰 수 있다.

  • 문제를 해결하기 위해서는 자식 클래스들이 추상화에 의존해야 한다.

    • 차이점 (세부 구현) 은 자식 클래스로,

    • 공통적인 부분 (퍼블릭 인터페이스) 은 부모 클래스로 이동하라

    • 그러나 이렇게 하더라도 부모 클래스의 인스턴스 변수의 변경은 자식 클래스들의 생성자에 영향을 미친다.

상속으로 인한 클래스사이의 결합은 피할 수 없다.


합성과 유연한 설계

https://github.com/ghkdgus29/object-code/tree/main/chapter11

  • inheritance 패키지 : 상속을 사용해 코드 재사용
  • synthesis 패키지 : 합성을 사용해 코드 재사용

  • 상속은 부모 클래스 - 자식 클래스를 연결해 코드를 재사용

    • 컴파일 타임 의존성 해결
  • 합성은 재사용 코드를 가진 객체를 내부 필드로 포함하여 코드를 재사용

    • 런타임 의존성 해결
  • 합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용한다.

    • 상속을 사용할 때 : 클래스 사이의 높은 결합도

    • 합성을 사용할 때 : 객체 사이의 낮은 결합도


상속을 이용한 코드 재사용

  • 모든 가능한 조합별로 자식 클래스를 만들어야 한다.

    • 새로운 정책 추가 시 많은 수의 클래스를 상속계층안에 추가해야 한다.

    • 클래스 폭발

  • 조합별로 자식 클래스를 만들어야 한다.
  • 복잡한 이름, 많은 수의 자식 클래스

  • 새로운 정책 추가 시, 또다시 많은 수의 자식 클래스 생성 필요

  • 특정 정책 수정 시 관련된 자식 클래스들을 모두 수정해야 한다.

코드 재사용을 위한 상속은 지양하라


합성을 이용한 코드 재사용

  • 컴파일 타임 관계를 런타임 관계로 변경

    • 실행 시점에 인스턴스를 조립하는 방법
  • 모든 가능한 조합별로 클래스를 만들지 않아도 된다.

    • 원하는 객체 생성 및 의존성 주입으로 다양한 조합 가능

합성을 사용한 구조


public class Client {
    public static void main(String[] args) {

        // 일반 요금제 -> 세금 정책
        Phone phone1 = new Phone(
                         new TaxablePolicy(
                                new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
                                 0.05));


        // 일반 요금제 -> 기본 요금 할인 정책 -> 세금 정책
        Phone phone2 = new Phone(
                new TaxablePolicy(
                        new RateDiscountablePolicy(
                                new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
                                Money.wons(500)),
                        0.05));


        // 일반 요금제 -> 세금 정책 -> 기본 요금 할인 정책
        Phone phone3 = new Phone(
                new RateDiscountablePolicy(
                        new TaxablePolicy(
                                new RegularPolicy(Money.wons(1000), Duration.ofSeconds(10)),
                                0.05),
                        Money.wons(1000)));


        // 심야할인 요금제 -> 세금 정책 -> 기본 요금 할인 정책
        Phone phone4 = new Phone(
                new RateDiscountablePolicy(
                        new TaxablePolicy(
                                new NightlyDiscountPolicy(Money.wons(700), Money.wons(1000), Duration.ofSeconds(10)),
                                0.05),
                        Money.wons(1000)
                )
        );

    }

}

인스턴스 생성 시점에 다양한 조합 가능


  • 새로운 정책의 추가 및 수정이 쉽다.
    • 필요한 정책을 구현한 클래스 하나만 추가하면 된다.

새로운 기본 정책 추가

  • 상속을 코드 재사용에 사용한 경우, 4 + 1개의 클래스가 추가되었다.
  • 반면, 합성을 코드 재사용에 사용한 경우, 1개의 클래스만 추가하면 된다.

  • 정책 변경 시 오직 하나의 클래스만 수정하면 된다.

    • 단일 책임 원칙 준수

다형성


https://github.com/ghkdgus29/object-code/tree/main/chapter12

  • 많은 형태를 가질 수 있는 능력, Polymorphism

  • 하나의 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력


다형성의 분류

  • 오버로딩 다형성

    • 메서드명은 같지만 메서드 인자가 다르면 다른 메서드로 취급한다.

    • 덕분에 유사한 작업을 수행하는 메서드명을 통일할 수 있다.

  • 강제 다형성

    • 동일한 연산자를 다양한 타입에 사용할 수 있는 능력
  • 매개변수 다형성

    • 변수를 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정

    • 제네릭 프로그래밍과 관련이 있다.

  • 포함 다형성

    • 메시지가 동일하더라도 수신한 객체의 실제 타입에 따라 실제 수행되는 행동이 달라지는 능력

    • 서브타입 다형성이라고도 함

    • 객체지향 프로그래밍에서 가장 대표적인 형태의 다형성


상속의 목적

  • 상속의 목적은 코드 재사용 X

  • 다형성을 가능하게 하는 타입 계층을 구축하는 것이 진짜 목적


메서드 오버라이딩

  • 부모 클래스와 자식 클래스에 동일한 시그니처를 가진 메서드가 존재할 경우

  • 자식 클래스의 메서드 우선순위가 더 높다.

    • 메시지를 수신했을 때 부모 클래스의 메서드가 아닌 자식 클래스의 메서드가 실행

    • 이처럼 부모 클래스의 구현을 새로운 구현으로 대체하는 것을 메서드 오버라이딩 이라 한다.

    • 자식 클래스의 메서드가 부모 클래스의 메서드를 감춘다.


클래스와 인스턴스의 구조

  • 인스턴스 개수와 상관없이 클래스는 하나만 메모리에 로드된다.

  • 각 인스턴스는 자신의 클래스를 가리키는 포인터(class)를 가진다.

  • 클래스는 자신의 부모 클래스를 가리키는 포인터(parent)를 가진다.

    • 만약 현재 인스턴스가 가리키는 클래스에 원하는 메서드가 존재하지 않으면

    • 부모 클래스를 차례로 거슬러 올라가며 원하는 메서드를 찾는다.

GradeLecture 인스턴스의 메모리 구조


업 캐스팅

  • 부모 클래스 타입으로 선언된 변수에 자식 클래스를 할당하는 것

  • 부모는 마음이 넓어 자식을 담을 수 있다.

  • 명시적인 타입 변환 필요 X

    • 다운 캐스팅은 명시적인 타입 캐스팅 필요


동적 바인딩

  • 선언된 변수의 타입이 아니라 메시지를 수신하는 실제 객체의 타입에 따라 실행되는 메서드가 결정되는 것

  • 객체지향 시스템이 메시지를 처리할 메서드를 컴파일 시점이 아닌 런타임 시점에 결정하기 때문에 가능

  • 컴파일 타임에 호출할 함수를 결정하는 정적 바인딩에 대응되는 개념


동적인 문맥

  • 메시지 전송 코드만으로는 어떤 클래스의 어떤 메서드가 실행될 지 알 수 없다.

  • 따라서 실제 객체의 클래스의 메서드를 호출한다는 표현은 옳지 않다.

    • 실제 객체에게 메시지를 전송한다는 표현이 맞다.

    • super 참조 역시 마찬가지로 부모 클래스에게 메시지를 전송한다.

      • 실제 호출되는 메서드는 더 상위에 위치할 수 있다.

self 전송 vs super 전송

  • self 전송 은 타입 상관없이 실제 들어온 현재 객체의 클래스부터 메서드를 탐색한다.

  • super 전송 은 클래스에 코드로서 존재한다.

    • 따라서 현재 객체와 상관없이 해당 클래스의 바로 부모부터 메서드를 탐색한다.

위임

  • 객체가 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해 처리를 요청하는 것

  • 위임은 현재 실행 객체를 가리키는 self 참조 를 인자로 함께 전달

    • 포워딩은 self 참조 전달 X

    • 이를 통해 두 객체간의 실행 문맥을 공유할 수 있다.

  • 상속은 이러한 메시지 위임을 자동화해준다.

    • 동적인 메서드 탐색이 바로 자동적인 메시지 위임

서브클래싱과 서브타이핑

https://github.com/ghkdgus29/object-code/tree/main/chapter13

  • LSP를 위반하는 직사각형 - 정사각형 예제

  • 상속은 부모와 자식을 묶는 타입 계층을 구현하기 위해 사용되어야 한다.

    • 부모 클래스는 자식 클래스의 일반화

    • 자식 클래스는 부모 클래스의 특수화

  • 이를 통해 다형적으로 동작하는 객체들의 관계를 얻을 수 있다.


타입

  • 사물을 분류하기 위한 틀

  • 공통의 특징을 공유하는 대상들의 분류

  • 타입에 속하는 개별 대상을 타입의 인스턴스 (객체) 라 부른다.

  • 타입의 목적

    • 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의

    • 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공


객체지향 프로그래밍에서의 타입

  • 프로그래밍 언어의 관점에서 타입은 호출가능한 오퍼레이션의 집합을 정의한다.

  • 객체지향 프로그래밍에서의 오퍼레이션 == 객체가 수신할 수 있는 메시지

  • 따라서 객체의 타입은 객체가 수신할 수 있는 메시지의 종류 (퍼블릭 인터페이스) 를 정의한다.

  • 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.

  • 객체에게 중요한 것은 행동

    • 객체들이 동일한 상태를 갖더라도 퍼블릭 인터페이스가 다르면 서로 다른 타입이다.

    • 객체들이 다른 상태를 갖더라도 퍼블릭 인터페이스가 같다면 서로 같은 타입이다.


타입 계층

  • 자바, C++, 루비 ➔ 인스턴스
  • 클래스기반, 객체지향 언어, 프로그래밍 언어 ➔ 타입
  • 타입들을 일반화와 특수화 관계를 가진 계층으로 표현하는 것

  • 일반적인 상위타입을 슈퍼타입

  • 구체적인 하위타입을 서브타입이라 한다.

  • 슈퍼타입과 서브타입간의 관계를 형성하는 기준은 퍼블릭 인터페이스다.

    • 일반적인 퍼블릭 인터페이스를 가지는 객체 ➔ 슈퍼타입

    • 구체적인 퍼블릭 인터페이스를 가지는 객체 ➔ 서브타입

  • 서브타입 인스턴스 집합은 슈퍼타입의 인스턴스 집합에 포함된다.

    • 서브타입 인스턴스는 슈퍼타입 인스턴스로 간주될 수 있다.

서브클래싱 & 서브타이핑

  • 둘을 나누는 기준은 상속을 사용하는 목적

  • 서브클래싱 : 코드 재사용을 목적으로 상속

  • 서브타이핑 : 부모 클래스 인스턴스 대신 자식 클래스 인스턴스를 사용할 목적으로 상속 (인터페이스 상속)

  • 자식 클래스는 모두 서브 클래스지만, 모든 서브 클래스가 서브 타입을 만족하진 않는다.


서브타이핑

  • 코드상으로는 슈퍼타입으로 정의되나, 런타임에 서브타입 객체로 대체할 수 있다.

  • 타입을 구현하는 일반적인 방법 ➔ 클래스

  • 타입계층을 구현하는 일반적인 방법 ➔ 상속 + α

    • 타입계층을 구현하는 것 == 서브타이핑
  • 타입계층을 구현할 때 지켜야 하는 제약사항

    • 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?

    • 클라이언트는 부모 클래스와 자식 클래스의 차이점을 몰라야 한다.

      • 부모-자식간의 행동호환성

행동호환성

  • 두 타입사이에 행동이 호환될 경우에만 타입계층으로 묶어야 한다.

  • 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.

  • 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.


인터페이스 분리 원칙

  • Interface Segregation Principle, ISP

  • Client1 의 요구사항 변경에 의한 Flyer 인터페이스의 변경은
    • Bird 에만 영향을 준다.
    • Client2, Walker, Penguin 에는 영향을 주지 않는다.

  • 클라이언트의 요구에 맞춰 인터페이스를 분리하면 변경에 대한 영향을 제어할 수 있다.

  • 클라이언트의 요구가 바뀌더라도 영향의 파급효과를 효과적으로 제어할 수 있다.

  • 비대한 인터페이스는 응집도 있는 특화된 인터페이스로 분리해야 한다.

  • 클라이언트는 자신이 실제로 호출하는 메서드에만 의존해야 한다.

    • ISP를 통해 클라이언트가 호출하지 않는 메서드에 대한 의존성을 끊어 낸다.

리스코프 치환 원칙

  • Liskov Substitution Principle, LSP

  • 올바른 상속 관계 (타입 계층) 를 구축하기 위한 지침

  • 부모 클래스를 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것임을 보장해야 한다.

    • 대체 가능성 + 행동호환성
  • 유연한 설계의 기반

    • 클라이언트 코드의 변경 없이 새로운 자식 클래스와 협력할 수 있다.

계약에 의한 설계

  • 사전조건 : 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건

  • 사후조건 : 서버가 클라이언트에게 보장해야 하는 조건

  • 클래스 불변식 : 메서드 실행 전후로 인스턴스가 만족시켜야 하는 조건


계약에 의한 설계와 서브타이핑

  • 서브타입이 LSP를 만족시키기 위해서는 클라이언트-슈퍼타입간에 체결된 계약을 준수해야 한다.

  • 서브타입에 더 강력한 사전조건을 정의할 수 없다.

    • 더 약한 사전조건은 가능
    • 클라이언트가 예상하는 조건을 상회하는 순간, 계약 위반
  • 서브타입에 더 약한 사후조건을 정의할 수 없다.

    • 더 강한 사후조건은 가능
    • 클라이언트가 서버로부터 받기를 기대하는 조건보다 밑도는 순간, 계약 위반

일관성 있는 협력

https://github.com/ghkdgus29/object-code/tree/main/chapter14

  • inconsistent 패키지 : 일관적이지 않은 설계
  • consistent 패키지 : 일관적인 설계

  • 일관성은 설계에 드는 비용을 감소시킨다.

  • 코드의 이해가 쉬워진다.

    • 특정 문제를 유사한 방법으로 해결하고 있다는 사실을 알면
    • 문제를 이해하는 것만으로도 코드의 구조를 예상 가능하다.

일관성 있는 협력을 위한 지침

  • 변하는 개념을 변하지 않는 개념으로부터 분리한다.

  • 변하는 개념을 캡슐화하라.


일관성은 요구사항이 추가될수록 깨질 수 있다.

현재의 설계에 맹목적으로 일관성을 맞추지 말고, 변경에 맞춰 지속적인 코드 개선이 필요하다.


디자인 패턴과 프레임워크

  • 디자인 패턴

    • 소프트웨어 설계에서 자주 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결방법

    • 여러 용도로 사용할 수 있는 설계 아이디어

  • 프레임워크

    • 설계를 구현한 코드 템플릿 제공

디자인 패턴

  • 반복적으로 발생하는 문제와 해법의 쌍

  • 패턴의 이름을 통해 다른 사람과의 의사소통을 돕는다.

  • 실무 컨텍스트에서 유용하게 사용해왔고 다른 실무 컨텍스트에서도 유용할 것이라 예상되는 아이디어

  • 특정한 상황에 적용할 수 있는 설계를 쉽고 빠르게 떠올릴 수 있다.

  • 디자인 패턴은 설계의 방향을 제시하는 출발점

    • 설계의 목표가 돼서는 안된다.

    • 패턴은 맹목적으로 사용해선 안된다.


프레임 워크

  • 애플리케이션 개발자가 요구사항에 맞게 커스터마이징 할 수 있는 애플리케이션 골격

  • 부분적으로 구현된 추상 클래스, 인터페이스 집합, 다양한 컴포넌트들을 제공

  • 제어 흐름에 대해 미리 정의

    • 제어 역전 원리

    • 도메인 핵심 개념들 사이의 협력 관계를 재사용

    • 프레임워크가 개발자가 구현한 서브클래스를 사용

  • 상위 패키지 : 프레임워크
  • 하위 패키지 : 개발자가 구현해야 하는 세부사항

출처 : 조영호님의 오브젝트
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=266736479

6개의 댓글

comment-user-thumbnail
2023년 3월 5일

와 벌써 2회독이신가요

1개의 답글
comment-user-thumbnail
2023년 3월 12일

정리 너무 잘하셔서 저는 따로 정리 안하고 현 벨로그만 봐도 될것 같아요🙊

1개의 답글
comment-user-thumbnail
2023년 4월 6일

책 안사도 되겠네요

1개의 답글