오브젝트(조영호) 느낀점 정리(1)

taehee kim·2023년 3월 8일
0

책을 읽고 깨달은 점

객체지향의 4대요소에 대한 오해

  • 객체지향의 목표는 변경과 확장에 유리한 코드를 만드는 것이며 자율적인 객체의 협력을 통해 이 목표를 이룬다.
  • 이 과정에서 4대 요소는 중요한 역할을 한다.

캡슐화 (Encapsulation)

  • 이 책을 읽기전에는 캡슐화는 단순히 하나의 객체에 내용을 변수와 메서드로 정의하고 접근지정자를 주어 제한하는 것이다 정도로 알고 있었고 왜 이렇게 해야하는지는 전혀 이해하지 못했었다.
  • 하지만 책의 첫번째 부분을 읽고 나서 어쩌면 객체지향에서 가장 중요한 것이 캡슐화일 수 있겠다는 생각이 들 정도로 잘못 이해하고 있었다는 것을 깨달았다.
  • 캡슐화의 핵심은 행위와 그 행위에 필요한 상태를 하나의 클래스에 응집하여야 한다는 것이며 이를 다른 객체가 함부로 알게 하거나 다룰 수 있게 해서는 안된다는 것이다.
  • 즉, 객체의 상태는 최대한 객체 스스로 자율적으로 처리하도록 해야하며 이를 위해서 상태와 행동을 하나의 클래스에 모아서 작성하고 접근지정자로 제한해야하는 것이다.
  • 이렇게 하지 않고 객체의 상태를 다른 객체가 다루는 방식으로 코드가 작성되게 되면 의존성과 결합성이 증가하는 코드가 만들어질 수 밖에 없고 변경 발생 시 여러 지점을 고쳐야할 것이다.(책의 1장에서 잘 설명 되어 있으니 한번 읽어 보시길 권해드립니다.)

추상화 (Abstraction), 다형성 (Polymorphism)

  • 추상화가 중요한 이유는 추상적인 대상이 있기 때문에 구체적인 것에 의존하지 않을 수 있고 이것이 결국 변경및 확장에 유리함으로 동작하는 것 때문이라고 생각한다.
  • 또한 객체지향 언어에서 다형성을 제공하기 때문에 추상적인 클래스 변수로 구체적인 객체를 담고 업캐스팅을 통해 자동 형변환되는 것이 가능하며 동적 바인딩을 통해 같은 메시지에 대해서 런타임에 특정 객체의 메서드로 응답하는 것이 가능하다.
  • 객체지향 언어의 이와 같은 특징은 변경에 닫혀있고 확장에는 열려있는 코드를 작성하는 필요조건이라고 생각한다.(충분 조건이 아닌 이유는 OCP에서 설명되어 있습니다.)
  • 객체지향이 클래스지향이 아니라 객체 지향인 이유도 런타임에 생성되는 객체에 따라 위와 같은 성질을 이용하여 프로그래밍을 하는 것이기 때문이다. 이전에는 객체지향을 클래스지향의 차이를 인지하지 못하고 있었다.

상속성 (Inheritance)

  • 상속이 코드 재사용을 위해 사용되는 것은 바람직하지 않으며 코드 재사용을 원할 경우 합성을 사용하는 것이 좋다는 것은 알고 있었는데 그렇다면 상속은 언제 쓰는지 잘 와닿지 않았고 단순히 is-a관계가 명확할 때 사용해야한 다는 정도로 알고 있었다.
  • 이 책에서 설명하는 상속이 필요한 주 된 이유는 타입 계층을 이루기 위해서이다.
  • 어떻게 보면 추상화나 다형성과도 연관 될 수 있는 이야기인데 상속이 있기 때문에 특정 타입이 계층화 되어 더 추상적이거나 혹은 더 구체적이게 될 수 있고 이를 활용하여 다형성을 잘 활용할 수 있게 되기 때문이다.
  • 또한 언제 상속을 해야하는 지는 상속은 행동의 is-a관계가 명확 할 때 사용해야한다라고 되어있다.
  • 예를 들어 Bird Class가 Penguin Class의 부모가 되도록 상속하는 것이 바람직한가에 대한 답은 그럴 수도 있고 아닐 수도 있는데 이를 결정하는 것이 어떤 행위 즉, 메서드가 있는지에 따라서 달라진다는 것이다.
  • 만약 Bird 클래스에 fly()라는 메서드가 있다면 Penguin은 행동 관점에서 fly를 아예 할 수 없기 때문에 is-a관계가 성립하지 않고 상속을 하면 안된다.
  • 하지만 만약 알을낳다()라는 메서드가 Bird 클래스에 있다면 펭귄도 알을 낳기 때문에 알을낳다()라는 행동이 is-a관계가 되어 상속을 하는 것이 바람직하다.

SOLID원칙 존재 이유에 대해서 명확하게 이해할 수 있었다.

  • 결국에 5가지 원칙들은 의존성, 결합성이 낮으며 응집성이 높은 코드를 작성하기 위한 원칙들이라고 생각한다.

SRP

  • SRP는 객체를 설계할 때 하나의 책임만을 가지도록 설계해야한다는 원칙이다. 이전에는 하나의 책임이라는 말이 너무 추상적이고 왜 꼭 하나의 책임만을 가져야 하는지 정확하게 이해가 가지 않았었다.
  • 이 책을 읽고 내가 이해한 바에 따르면 객체가 하나의 책임을 가져야 하는 이유는 올바르게 작성된 객체는 객체들간의 협력관계에서 특정 책임을 수행하고 이 책임을 수행하기 위한 행위(메서드)와 행위를 위한 상태(변수)가 존재하는데 여러 책임을 가져 이것들이 하나의 클래스에 있다면 클래스 내의 행위 간에 의존성이 증가할 수 밖에 없기 때문이다.
  • 만약 하나의 객체가 여러 책임을 가지고 있다면 여러 책임을 지기 위한 상태, 행동을 한 클래스 내에서 정의하여 가지고 있을 것이다.
  • 이는 원래는 다른 클래스 내에 정의 해야할 상태를 모두 공유하면서 내것 인 것 처럼 마음대로 행동을 취할 수 있다는 것이며 캡슐화를 전혀 지키지 않는 코드가 작성된 다는 의미이다.
  • 캡슐화가 지켜지지 않게 되면 상태와 행동들의 의존성과 결합성이 높아질 수 밖에 없고 응집성이 낮아지면서 특정 부분이 변경 혹은 확장해야 할때 그것에 의존하는 다른 코드들도 변경에 영향을 받을 수 밖에 없게 된다.
  • 즉 SRP원칙은 책임에 따라 행동과 그에 필요한 상태를 각 객체에 분리해야하고 캡슐화를 잘 지켜서 객체의 자율성을 높여야 한다는 의미로 이해할 수 있었다.

OCP, DIP

  • OCP와 DIP가 같이 묶이는 이유는 추상화와 다형성이라는 관점에서 설명 되기 때문이다.
  • OCP, DIP를 만족하는 방법에 대한 조건은 다형성을 활용하여 업캐스팅과 동적 바인딩으로 같은 메시지에 대해 런타임에 특정 객체가 메서드로 반응하도록 하고 객체를 생성하는 부분의 코드는 관심사를 분리하고 의존성을 주입 받는 방식으로 하여 추상화된 대상에만 의존하도록 코드를 작성하는 것이다.
  • 이렇게 하면 변경을 원할 때에는 생성되는 객체를 변경해주기만 하면 되고 이것이 factory에서 이루어지기 때문에 코드를 변경하지 않아도 되고 확장을 원할 경우 클래스를 추가해주면 되며 추상화된 것에만 의존할 수 있기 때문에 DIP를 만족한다.

LSP(리스코프 치환 원칙)

  • SOLID원칙 중 가장 이해가 가지 않았던 원칙인데 이 책을 읽으면서 이해를 잘 할 수 있었다.
  • LSP는 올바른 상속관계라면 모든 코드의 부모 클래스 부분의 코드를 자식 클래스 코드로 치환하여도 다른 코드를 전혀 수정하지 않고 정상적으로 동작해야함을 의미한다.
  • 좀 더 와닿는 표현으로 설명하면 클라이언트 객체 입장에서 메시지를 보내는 객체가 부모 객체인지 자식 객체인지에 몰라도 상관이 없어야 하고 자식 객체로 변경되었다고 해서 클라이언트 코드를 수정할 필요가 있어서는 안 된다는 것이다.
  • 가장 유명한 예시로 직사각형 정사각형 문제가 있는데 정사각형이 직사각형을 상속 받으면 안되는 이유는 직사각형 코드가 정사각형으로 수정될 경우 직사각형을 원래 사용하던 클라이언트 코드를 고쳐야 정상 동작하기 때문이다.
public class Rectangle
{
    protected int width;
    protected int height;
    
    /**
     * 너비 반환 함수
     *
     * @return [int] 너비
     */
    public int getWidth()
    {
        return width;
    }
    
    /**
     * 높이 반환 함수
     *
     * @return [int] 높이
     */
    public int getHeight()
    {
        return height;
    }
    
    /**
     * 너비 할당 함수
     *
     * @param width: [int] 너비
     */
    public void setWidth(int width)
    {
        this.width = width;
    }
    
    /**
     * 높이 할당 함수
     *
     * @param height: [int] 높이
     */
    public void setHeight(int height)
    {
        this.height = height;
    }
    
    /**
     * 넓이 반환 함수
     *
     * @return [int] 넓이
     */
    public int getArea()
    {
        return width * height;
    }
}

public class Square extends Rectangle
{
    /**
     * 너비 할당 함수
     *
     * @param width: [int] 너비
     */
    @Override
    public void setWidth(int width)
    {
        super.setWidth(width);
        super.setHeight(getWidth());
    }
    
    /**
     * 높이 할당 함수
     *
     * @param height: [int] 높이
     */
    @Override
    public void setHeight(int height)
    {
        super.setHeight(height);
        super.setWidth(getHeight());
    }
}

public class Main
{
    /**
     * 메인 함수
     *
     * @param args: [String[]] 매개변수
     */
    public static void main(String[] args)
    {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10);
        rectangle.setHeight(5);
        
        System.out.println(rectangle.getArea());
    }
}
  • 다음 코드에서 주목할 부분은 Main class 의 코드인데 지금 코드에서는 setWidth, setHeight를 둘다 호출하며 순서가 상관없는 상황인데 만약 Rectangle이 Square로 바뀔 경우 Main의 코드를 정상동작 시키기 위해서는 setWidth, 혹은 setHeight중 하나를 지우는 것이 맞으므로 코드를 변경해야하는 문제가 생기게 된다.
  • 따라서 리스코프 치환원칙을 위배하게 되고 잘못된 상속인 것이다.

ISP(인터페이스 분리 원칙)

  • 클라이언트에 맞추어 인터페이스를 분리하고 각 인터페이스는 메시지를 보내는 클라이언트에게 필요한 역할만 가지고 있어야한다.

객체 설계를 어떻게 하면 잘 할 수 있는지(책임 주도 설계)

  1. 도메인을 구조를 먼저 설계하자(도메인은 해결하고 싶은 문제를 나타낸다.)
  2. 도메인이 정립 되었다면 필요한 메시지를 생각해보자.
  3. 메시지가 정의 되었다면 이 메시지를 받을 책임에 대해서 정하고 그 책임을 수행할 객체를 정한다. 책임을 수행할 객체는 책임을 수행하기 위한 정보에 대해서 접근하기 쉬울 수록 좋다.
  4. 책임을 위한 행위를 먼저 생각하고 상태(데이터)를 결정하자. 먼저 필요한 데이터를 생각하는 식으로 설계하면 데이터 주도 설계에 가까워진다.
  5. 다형성을 활용하여 유연성을 증가시키자.

1장 객체, 설계

핵심 주제: 어떻게 객체를 설계해야할까?

  • 객체간의 의존성을 낮추는 방향으로 설계한다.
  • 의존성을 낮추기 위해서는 객체의 자율성을 높이는 방향으로 설계해야한다.
  • 이를 위해서는 객체의 상태와 그 상태에 대한 행동을 하나의 객체에 응집하고 접근을 제한하여 제공한다.(캡슐화)

객체의 자율성

  • 객체의 자율성이란 각각의 객체가 스스로 구체적인 구현을 수행하는 것을 말한다.
  • 각각의 객체가 자율적으로 책임을 가지고 행동하기 때문에 그 객체에 메시지를 보내는 객체는 세부적인 구현을 모르며 이를 통해 객체간의 의존도를 낮출 수 있다.
  • 객체를 의지를 가진 것처럼 의인화하여 생각
    • ex. 사람이 들고있는 가방에서 돈을 꺼낸다.
      • 이 경우 사람은 가방과 돈에 모두 의존한다.
      • 만약 가방에 돈이 아니라 신용카드가 들어있는 것으로 바뀐다면 사람의 코드를 수정해야한다.
      • 따라서 이 경우에는 가방을 의인화하여
        • 사람이 들고있는 가방에 접근한다.
        • 가방이 돈을 꺼낸다.
      • 이런 형태로 작성해야한다.

의존성 트레이드 오프

  • 객체내의 상태와 행동을 잘 응집하는 방향으로 코드를 수정하더라도 어느 단계에 이르면 전체적인 의존성은 오히려 증가하는 지점에 도달할 수 있다.
  • 이 시점에서 트레이드 오프를 고려하여 설계를 선택한다.

객체지향의 목적

  • 변경과 수정에 유리한 코드
  • 객체 지향 설계도 변경에 유리한 코드를 작성하는 것에 중점을 두고 의존성을 낮추어야 한다.

2장 객체지향 프로그래밍

협력, 객체, 클래스

  • 객체 지향 프로그래밍을 작성할 때 어떤 클래스가 필요한지 고민하지 말라.
    • 어떤 객체가 필요한지 고민하자.
    • 클래스는 필요한 객체의 상태와 행동을 추상화 한 것이다.
    • 따라서 어떤 객체의 상태와 행동이 필요한지 먼저 고민하자.
  • 객체는 독립적인 존재가 아니라 협력하는 일원이다.

도메인

  • 어떤 객체가 필요한지는 어떤 도메인이 필요한지 고민하여 생각해낼 수 있따.
  • 영화 예매 프로그램을 만들기 위해서는 다음과 같은 도메인이 필요한 것을 떠올릴 수 있다.
    • 영화
    • 상영
    • 예약
    • 할인 정책
    • 할인 조건
  • 상영과 영화를 동일하게 생각할 수 있지만 영화는 영화 자체를 의미하는 반면 상영은 영화가 상영되는 사건을 말하므로 분명히 다른 도메인이다.

자율적인 객체

  • 1장에서 설명한 것처럼 객체는 상태와 그에 대한 행동을 가지며 스스로 판단하고 행동하는 자율적인 존재여야한다.
  • 객체 지향 이전에는 데이터와 기능을 독립적인 존재로 프로그램을 구성했다.
  • 객체 지향에서는 데이터와 기능을 한 덩어리로 묶어서 캡슐화한다.
  • 캡슐화를 위해서는 외부 접근 제어가 필요하다.
  • 자율적인 객체를 위해서는 외부에서는 객체 내부의 구현이 공개 되지 않도록 해야한다.

협력

  • 객체는 다른 객체에 메시지를 전송하는 방법을 통해서만 상호작용할 수 있다. 이를 협력이라고 한다.
  • 네트워크 통신을 하는 것처럼 객체는 다른 객체에게 요청하고 요청을 받은 객체는 자율적으로 처리한 후 응답한다.

상속과 다형성

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

  • 다형성
  • 코드의 의존성과 실행시간 의존성의 차이가 높을 수록 더 유연해지지만 코드를 파악하는 것이 어려워진다.(서로 트레이드오프의 관계이다.)

다형성과 동적 바인딩

  • 같은 메시지에 대해서 런타임에 어떤 객체의 메서드를 호출할 지 결정하는 동적 바인딩을 통해 다형성이 구현된다.
  • 다형성을 통해 업캐스팅(부모 클래스(인터페이스) 변수로 자식 클래스로 생성한 객체를 지칭하는 것.)을 할 수 있고 런타임에 어떤 자식 객체를 생성해서 할당하는 지에 같은 메시지에 따른 동적으로 바인딩 되는 메서드가 달라질 수 있다.
  • 이를 통해 유연한 설계를 할 수 있다.

추상화의 힘

  • 추상화를 하면 구현은 동적 바인딩을 통해 런타임에 결정되기 때문에

코드 재사용과 상속, 합성

  • 코드 재사용을 위해서는 상속을 사용하지 말자
    • 상속의 단점은
      • 캡슐화 위반(부모 클래스의 메서드 구현이 그대로 유출되면서 의존성이 생김. )
      • 설계를 유연하지 못하게 한다.
  • 영화와 영화 예매 시 할인하는 정책을 구현한다고 하면 두가지 방법이 있다.
    • 상속을 이용한 방법은 Movie 추상클래스로 만들고 이를 상속받는 ADiscountPolicyMovie, BDiscountPolicyMovie를 만드는 것이다.

      • 이 경우 실행 시점에 영화 A→BDiscountPolicyMovie로 할인정책을 바꾸기 위해서는 BDiscountPolicyMovie를 새로 생성하면서 원래 있던 ADiscountPolicyMovie의 상태를 복제 해주어야 할 것이다.
      • 또한 만약 할인 정책이 할인 조건, 할인 방식등으로 구분되게 되면 이 종류에 따른 무수히 많은 DiscountMovie를 만들어야 할 것이다. 이 문제를 해결하기 위해서 합성을 사용한다.
    • 합성을 이용한 방법에서 Movie클래스에 인스턴스 변수로 DiscountPolicy 인터페이스 구현 객체들을 할당하는 방식으로 구현한다.

      public class Movie {
          private String title;
          private Duration runningTime;
          private Money fee;
          private DiscountPolicy discountPolicy;
          private DiscountCondition discountCondition;
    • ADiscountPolicy, BDiscountPolicy 구현 객체로서 런타임에 알맞게 생성한 후 할당해주기만 하면된다.

    • 할인 조건이 추가된다면 인스턴스 변수에 DiscountCondition 인터페이스를 정의하여 원하는 구현객체를 넣어주면 된다.

    • 합성 방식의 장점은 상속과 달리 자식클래스가 부모클래스에 의존하지 않으며 의존 인스턴스를 교체하는것을 쉽게 만든다.

  • 언제 상속을 사용해야하는지 합성을 사용해야하는지에 대해서는 이후 내용에 있다.

3장 역할, 책임, 협력

  • 객체지향 패러다임의 핵심은 역할, 책임, 협력이다.
  • 협력은 여러 객체가 공동의 목표를 위해 메시지를 송수신하면서 상호작용하는 것을 말한다.
  • 책임은 객체가 협력에 참여하기 위해 수행하는 로직을 말한다.
  • 역할은 객체가 수행하는 책임들이 모인것이다.

책임

  • 하는것과 아는것
  • 하는것
    • 객체를 생성하거나 계산을 수행하는등의 스스로 하는 것
    • 다른 객체의 행동을 유발 하는것
  • 아는 것
    • 사적인 정보에 대해 아는것
    • 관련된 객체에 대해 아는 것
  • 책임은 메시지 보다 더 추상적이고 개념적으로도 크다.
    • 하나의 책임을 수행하기 위한 메서드는 여럿 필요할 수 있고 메시지도 여럿 필요할 수 있다.
  • 책임의 예시
    • Movie
      • 영화 정보를 알고 있다.
      • 가격을 계산한다.
    • DiscountPolicy
      • 할인된 가격을 계산한다.
      • 할인 정책을 알고있다.
    • Screening
      • 상영정보를 알고있다
      • 예매 정보를 생성한다
    • DiscountCondition
      • 할인 조건을 알고있다.
      • 할인 여부를 판단한다.

책임주도설계

메시지가 객체를 결정한다.

  1. 객체가 최소한의 인터페이스를 가질 수 있게 된다.
  2. 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다.

행동이 상태를 결정한다.

  • 객체에 어떤 행동이 필요한지 먼저 결정하고 그 후에 상태를 결정한다.

영화 예매 시스템을 설계하는 과정.

  • 시스템이 사용자에게 제공해야할 기능은 영화를 예매하는 것.
  1. 예매하다 라는 메시지가 필요하다.
  2. 예매하다 라는 메시지를 어떤 객체가 처리해야할지 정하자. 이 과정이 객체에 책임을 할당하는 과정이다.
  3. 영화를 예매하기 위해서는 상영 시간(할인에 필요.)과 기본 요금을 알아야한다.
  4. 이 정보를 알고있거나 해당 정보의 소유자를 잘 아는 전문하는 Screening이다.
  5. 영화를 예매하기 위해서는 예매 가격을 계산해야한다. Screening은 예매 가격을 계산할 정보를 충분히 알고 있지 않다.
  6. 따라서 ‘가격을 계산하라’ 라는 메시지를 보내야하고 이를 보낼 객체를 찾아야한다. 마찬가지로 가격을 계산하는데 필요한 정보를 가장 많이 알고 있는 정보 전문가를 찾으면 Movie이다. 따라서 Screening은 Movie에 가격을 계산하라라는 메시지를 보낸다.

이왕 같은 과정 처럼 객체 지향 설계는 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하여 메시지를 통해 책임을 부여하는 반복적인 과정을 통해 이루어진다.

역할

  • 특정 협력에서 객체가 수행하는 협력의 집합이 역할이다.
  • 즉 역할은 책임을 추상화한것이라고 할 수 있다.
  • 할인 요금 계산이라는 메시지가 있고 이에 대한 책임은 여러 객체가 다른 방식으로 질 수 있다. 여기서 책임은 여러 방식으로 존재하지만 할인 요금 계산 메시지에 대한 역할은 하나이다.
  • 책임에 대한 역할이 있기 때문에 손쉽게 역할의 위치에 여러 책임을 갈아끼울 수 있고 유연한 설계가 가능하다.

4장 설계품질과 트레이드 오프

데이터 중심의 설계 예시

  • 2장의 영화 예매 시스템은 책임 주도 설계 기반으로 이루어진 반면 4장의 코드는 데이터 중심의 영화 예매 시스템이다.
  • 이 경우 각 객체에 대해 어떤 데이터가 필요한지를 기반으로 설계되어있다.
  • 데이터 중심 설계는 데이터와 행동을 잘 응집 하더라도 캡슐화를 위반할 확률이 높다.
    • 어떠한 행동을 데이터로 표현했기 때문에 이 데이터에 대해서 접근자(Getter), 수정자(Setter)등을 활용해 접근할 경우 private속성으로 데이터를 지정했음에도 public 속성과 다를바 없는 경우가 발생하게 된다.
    • 예시: 다음 코드는 Screening이라는 객체가 요금을 계산하는 메서드이다.
      public Money calculateFee(int audienceCount) {
              switch (movie.getMovieType()) {
                  case AMOUNT_DISCOUNT:
                      if (movie.isDiscountable(whenScreened, sequence)) {
                          return movie.calculateAmountDiscountedFee().times(audienceCount);
                      }
                      break;
                  case PERCENT_DISCOUNT:
                      if (movie.isDiscountable(whenScreened, sequence)) {
                          return movie.calculatePercentDiscountedFee().times(audienceCount);
                      }
                  case NONE_DISCOUNT:
                      movie.calculateNoneDiscountedFee().times(audienceCount);
              }
      
              return movie.calculateNoneDiscountedFee().times(audienceCount);
          }
      • Movie가 가지고 있는 MovieType객체에 접근하는 getMovieType에 집중해보자.
      • 이 데이터는 영화가 어떤 할인정책을 가지고 있는지에 대한 타입 데이터이다.
      • 영화는 자신의 할인 액을 계산할 수 있어야하고 이 때문에 이러한 데이터를 가지고 있는 방식으로 설계된것이다.
      • 이러한 데이터 기반 설계의 문제점은 할인 방식을 책임이 아니라 데이터로 정의했기 때문에 데이터의 값을 getter를 사용해서 노출시키고 응집도를 낮추면서 의존성을 증가시키기 때문이다.
      • 또한 MovieType이 늘어날 경우 분기는 계속해서 증가하게 된다.
      • 이 문제를 해결하기 위해서는 MovieType이라는 객체 본인이 할인 액을 결정할 책임을 가지게 하면 된다.
        • Movie는 할인액을 계산하라는 메시지를 외부로 보내야한다.
        • 이 메시지를 처리할 역할을 DiscountPolicy로 한다.
        • 역할을 할당한 책임은 AmountDiscountPolicy, PercentDiscountPolicy에 할당한다.
        • Movie는 DiscountPolicy 에 할인액을 계산하라는 메시지를 보내고 할인액은 생성된 객체에 따라 적절히 구현될 것이다.
        • 이렇게 구현된 것이 2장의 코드이다.

5장 책임 할당하기

책임 주도 설계를 향해

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

  • 객체에게 중요한것은 상태가 아니라 행동이고 상태는 행동을 위해 필요한 것일 뿐이다.

협력이라는 문맥 안에서 책임을 결정하라

  • 협력에서의 책임이란 메시지 전송자인 클라이언트가 선택할 책임을 말한다.
  • 따라서 메시지를 먼저 정하고 책임을 정한다.

GRASP패턴(책임 할당 기법)

도메인 개념에서 출발하기

  • 먼저 도메인의 개략적인 모습을 그려보자
  • 책임은 도메인의 개념과 유사한 형태로 필요하게 된다.

정보 전문가에게 책임을 할당하라

  • 메시지가 이미 존재할 때 그 메시지에 대한 책임을 할 객체는 책임을 수행하기 위한 정보를 가장 많이 가지고 있는 객체에게 할당한다.(정보 전문가 패턴)
  • 정보를 가지고 있다는 의미는 무조건 직접 저장하고 있어야 하는 것이 아니라 그 정보를 가지고 있는 다른 객체에 접근할 수 있다는 것을 말한다.(객체의 상태로 가지고 있을 필요는 없음.)

창조자에게 객체 생성 책임을 할당하라

  • 다른 객체를 생성하는 책임은 그 객체최대한 많이 결합되어있는 객체에서 하는 것이 유리하다.(이미 그 객체와 결합도가 높기 때문에 전체적인 결합도를 최소한으로 높이기 때문이다.)

객체 응집도 평가

public class DiscountCondition {
    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public boolean isSatisfiedBy(Screening screening) {
        if (type == DiscountConditionType.PERIOD) {
            return isSatisfiedByPeriod(screening);
        }

        return isSatisfiedBySequence(screening);
    }

    private boolean isSatisfiedByPeriod(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0;
    }

    private boolean isSatisfiedBySequence(Screening screening) {
        return sequence == screening.getSequence();
    }
}
  • 다음 클래스는 응집도가 낮다.
    1. 새로운 할인 타입이 추가되면 if-else문이 무한으로 늘어나야한다.
    2. 할인 타입에 따른 검증 메서드에서 사용되는 데이터가 분리되어있다.
      • isSatisfiedByPeriod 에서는 시간과 관련된 3변수
      • isSatisfiedBySequence에서는 sequence만 사용된다.
      • 이는 사실 두 메서드와 데이터를 서로 다른 클래스로 분리할 수 있는 가능성이 높음을 의미한다.
    3. DiscountCondition에서 클래스의 속성이 서로 다른 시점에 초기화 되거나 일부만 초기화 된다.
  • SequenceCondition, PeriodCondition으로 분리하자

다형성을 통해 분리하기

  • DiscountCondition을 분리할 때 다형성을 활용하면
    • DiscountCondition이라는 interface를 사용하고 SequenceCondition, PeriodCondition가 각각 이를 구현하면 된다.

책임 주도 설계의 대안

  • 바로 책임주도 설계로 완벽하게 하는것은 매우 힘든일이며 데이터 중심 설계가 존재하게 된다.
  • 이 경우 하나의 클래스 내에서 메서드 응집도를 높이는 방향으로 응집도를 높일 수 있다.
public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer,
                               int audienceCount) {
        boolean discountable = checkDiscountable(screening);
        Money fee = calculateFee(screening, discountable, audienceCount);
        return createReservation(screening, customer, audienceCount, fee);
    }

    private boolean checkDiscountable(Screening screening) {
        return screening.getMovie().getDiscountConditions().stream()
                .anyMatch(condition -> condition.isDiscountable(screening));
    }

    private Money calculateFee(Screening screening, boolean discountable,
                               int audienceCount) {
        if (discountable) {
            return screening.getMovie().getFee()
                    .minus(calculateDiscountedFee(screening.getMovie()))
                    .times(audienceCount);
        }

        return  screening.getMovie().getFee();
    }

    private Money calculateDiscountedFee(Movie movie) {
        switch(movie.getMovieType()) {
            case AMOUNT_DISCOUNT:
                return calculateAmountDiscountedFee(movie);
            case PERCENT_DISCOUNT:
                return calculatePercentDiscountedFee(movie);
            case NONE_DISCOUNT:
                return calculateNoneDiscountedFee(movie);
        }

        throw new IllegalArgumentException();
    }

    private Money calculateAmountDiscountedFee(Movie movie) {
        return movie.getDiscountAmount();
    }

    private Money calculatePercentDiscountedFee(Movie movie) {
        return movie.getFee().times(movie.getDiscountPercent());
    }

    private Money calculateNoneDiscountedFee(Movie movie) {
        return movie.getFee();
    }

    private Reservation createReservation(Screening screening,
                                          Customer customer, int audienceCount, Money fee) {
        return new Reservation(customer, screening, fee, audienceCount);
    }
  • 이렇게 메서드 응집도를 높이고 난 후 특정 데이터를 활용하는 메서드만 모아서 다른 클래스로 분리할 수 있다.
  • 데이터 중심 설계가 자연스럽게 책임 주도 설계로 이동할 수 있게 된다.

9장 유연한 설계

  • 추상화에만 의존하도록 설계하자
    • 이를 통해 변경시 코드를 고치지 않을 수 있으며, 확장시 확장하는 코드만 추가할 수 있다.(OCP)
    • DIP: 상위 모듈이 하위 모듈에 의존하면 안되며 모두 구체적인것에 의존하면 안된다.
  • 추상화에만 의존하는 것이 좋은 것은 알겠지만 코드를 작성하다보면 이는 실질적으로 불가능하다고 느껴질 수 있다. 이를 어떻게 해결할 수 있을까?

생성 사용 분리

  • 객체 생성 코드는 new (추상화 되지않은 실제 구현 클래스명)의 형식으로 이루어지면서 구체화된 대상에 의존하도록 만든다.
  • 따라서 객체 생성의 관심사는 분리하여 따로 처리한다.(Factory pattern)

의존성 주입

  • 추상화에만 의존하도록 하기 위해 다형성을 최대한 활용하여 코드를 작성해도 객체를 생성하고 할당하는 부분에서 구체적인 것에 의존하게 되면서 OCP, DIP를 위반하게 된다.
  • 이를 위해 의존성 주입이라는 기능을 도입한다.
  • 의존성 주입은 어떤 구체화된 객체를 생성할지, 어디에 주입할지에 대한 관심사를 분리하여 처리하는 것을 말한다.
  • 번외로 생성자 주입을 활용해야한다.(필드주입, setter주입등은 문제가 많다.)

10장 상속과 코드 재사용

  • 상속은 자식 부모 관계에서 캡슐화를 위배하고 의존성을 높인다.
  • 또한 자식이 상황에 따라 매우 여러 조건으로 폭발적으로 많이 만들어져야 할 수 있다.
    • 만약 A조건이 10종류, B조건이 10종류, C조건이 10종류 있다면 상속을 통해서는 1000개의 자식 클래스를 만들어야한다.
    • 하지만 합성을 활용하면 30종류의 클래스를 만들면 된다.

이 때문에 코드 재사용을 목적으로는 상속보다 합성을 사용한다.

11장 합성과 유연한 설계

  • 상속 관계는 is-a, 합성 관계는 has-a라고 부른다.
  • 합성이 대부분의 경우 유연한 설계로 이루어 지는 경우가 많다.

상속으로 인한 조합의 폭발적인 증가

  • 자식이 상황에 따라 매우 여러 조건으로 폭발적으로 많이 만들어져야 할 수 있다.
    • 만약 A조건이 10종류, B조건이 10종류, C조건이 10종류 있다면 상속을 통해서는 1000개의 자식 클래스를 만들어야한다.
    • 하지만 합성을 활용하면 30종류의 클래스를 만들면 된다.

12장 다형성

  • 다형성
    • 유니버셜
      • 매개변수: 제네릭 프로그래밍과 관련
      • 포함: subtype 다형성 (지금까지 말해왔던 다형성)
    • 임시
      • 오버로딩: 한 클래스안에 동일한 이름의 메서드가 존재할 수 있다.
      • 강제: 강제 자동 형변환

상속의 양면성

  • 상속의 목적은 데이터(인스턴스 변수)와 행동(메서드)를 재사용하기 위한 목적이 아니라 타입 계층을 구축하기 위한 것이다.

업캐스팅

  • 컴파일러에 명시적인 형변환이 없더라도 부모 클래스 변수로 자식 객체를 가리키는 경우 자식 객체의 타입은 부모 클래스의 타입으로 자동 형변환 된다.
  • 다운 캐스팅
    • 반대로 자식 클래스 변수로 부모 객체를 가리키는 경우인데 이 경우 명시적으로 형변환 해야한다.

동적 바인딩

  • 런타입에 같은 메시지에 대한 실행 메서드를 결정하는 것.

13장 서브클래싱과 서브타이핑

언제 상속을 사용해야 하는가

  • is-a관계를 모델링 하는가
    • 상식 선에서의 is-a관계가 아님 행동(메서드 )관점에서의 is-a관계를 말함.
    • 펭귄은 새이고 새는 날 수 있다.
      public class Bird{
      	public void fly(){}
      }
      
      public class Penguin extends Bird{
      
      }
    • 다음과 같은 잘 못된 상속이다.
    • 새를 날 수 있는것으로 정의 했기 때문에 행동의 is-a관계가 Penguin과 맞지 않는것이다.
    • 이를 판단하는 것은 클라이언트의 관점이다.
    • Bird, Penguin에게 메시지를 보내는 클라이언트는 Bird의 자식은 모두 fly 행동을 취할 수 있는 것으로 기대하기 때문에 이에 맞게 상속구조를 가져야한다.
      • Penguin의 fly에서 예외를 던지거나 아무 행동도 하지 않으면 되지 않을까?
        • 이것은 클라이언트가 기대하는 것이 아니기 때문에 안된다.

인터페이스 분리 원칙

  • 클라이언트에 맞추어 인터페이스를 분리한다.
  • 클라이언트에 맞추어 인터페이스를 분리해야 클라이언트와 상관없는 메서드로 부터 영향을 받지 않는다.

리스코프 치환 원칙

  • 코드 내에서 부모 클래스를 자식클래스로 모두 치환했을 때 모든 프로그램 코드가 변하지 않아야한다.
  • 직사각형 정사각형 문제
  • 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가
    • 클라이언트가 자식 클래스 객체가 사용되는지 몰라도 동작에 문제가 없어야함.
profile
Fail Fast

0개의 댓글