스터디/오브젝트 스터디

[오브젝트] 2장. 객체 지향 프로그래밍

제이온 (Jayon) 2022. 4. 25.

objects-study에서 스터디를 진행하고 있습니다.

 

영화 예매 시스템

요구 사항 살펴보기

  • 온라인 영화 예매 시스템을 구현하려고 한다.
  • 영화는 영화에 대한 기본 정보를 표현한다.
    • 제목, 상영 시간, 가격 정보 등
  • 상영은 실제로 관객이 영화를 관람하는 사건을 표현한다.
    • 상영 일자, 시간, 순번 등
  • 사람들은 영화를 예매한다고 하지만, 사실 특정 상영되는 영화를 예매한다고 해야 옳다.
  • 할인 조건
    • 가격의 할인 여부
    • 순서 조건: 상영 순번을 이용해 할인 여부를 결정
    • 기간 조건: 영화 상영 시작 시간을 이용해 할인 여부를 결정
  • 할인 정책
    • 할인 요금을 결정
    • 금액 할인 정책: 예매 요금에서 일정 금액을 할인
    • 비율 할인 정책: 정가에서 일정 비율의 요금을 할인
  • 영화 별로 하나의 할인 정책을 할당하거나 아예 안 할 수 있고, 할인 조건은 다수의 할인 조건을 섞을 수 있다.
  • 할인 정책이 적용되어 있지만 할인 조건을 만족하지 못하거나, 할인 정책이 적용되어 있지 않은 경우에는 요금을 할인하지 않는다.

 

객체 지향 프로그래밍을 향해

협력, 객체, 클래스

  • 객체 지향은 객체를 지향하는 것이다.
    • 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민하지 말아야 한다.
    • 어떤 객체들이 어떤 상태와 행동을 가지는지 결정해야 한다.
    • 객체를 독립적인 존재가 아니라, 협력하는 공동체의 일원으로 봐야 한다.

 

도메인의 구조를 따르는 프로그램 구조

  • 도메인은 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 말한다.

 

Untitled

 

  • 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.
  • 클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어서 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.

 

클래스 구현하기

  • 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위해서는 어떤 부분을 외부에 공개하고, 어떤 부분을 감출지 결정해야 한다.
    • 객체의 속성은 private로 막고, 내부 상태를 변경하기 위해 필요한 메서드를 public으로 열어 둔다.
  • 클래스의 내부와 외부룰 구분해야 객체의 자율성을 보장함으로써 프로그래머에게 구현의 자유를 제공한다.

 

자율적인 객체

  • 객체는 상태와 행동을 가진 자율적인 객체여야 한다.
  • 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 한다.
  • 접근 제어를 통해 외부의 간섭을 줄여야 객체는 스스로 자기의 행동을 결정할 수 있게 된다.
  • 인터페이스와 구현의 분리 원칙은 객체 지향 프로그래밍을 위해 따라야 하는 주요 원칙이다.
    • 퍼블릭 인터페이스: 외부에서 접근 가능한 부분
    • 구현: 내부에서만 접근 가능한 부분

 

프로그래머의 자유

  • 프로그래머의 역할은 클래스 작성자와 클라이언트 프로그래머로 구분된다.
    • 클래스 작성자는 데이터 타입을 새로 추가
    • 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용
  • 구현 은닉
    • 클래스 작성자는 필요한 부분만 클라이언트 프로그래머에게 제공하여 내부 구현을 감출 수 있다.
    • 클라이언트 프로그래머는 인터페이스만 알면 되므로 지식의 양을 줄일 수 있다.

 

협력하는 객체들의 공동체

  • 돈을 표현할 때 단순히 Long 타입의 변수를 선언하기 보다는, Money와 같이 객체로 포장해 주는 것이 좋다. 객체를 사용하면 의미 전달이 보다 잘 되고, 중복된 연산 처리를 한 곳에서 수행할 수 있기 때문이다.
  • 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호 작용을 협력이라고 부른다.

 

협력에 관한 짧은 이야기

  • 객체가 다른 객체와 상호 작용할 수 있는 유일한 방법은 메시지를 전송하거나 수신하는 것이다.
  • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드라고 부른다.
  • 메시지와 메서드를 구분하는 것은 중요하며, 여기서부터 다형성의 개념이 출발한다.

 

할인 요금 구하기

할인 요금 계산을 위한 협력 시작하기

  • Movie 클래스에는 할인 정책에 관한 자세한 로직이 들어 있지 않고, DiscountPolicy 인터페이스에 위임하였다. 상속과 다형성, 추상화를 이용하는 것이 정말 중요하다.

 

할인 정책과 할인 조건

Untitled

 

상속과 다형성

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

  • 코드의 의존성과 실행 시점의 의존성이 다를 수 있다.
  • 둘 간의 의존성이 다를수록 코드를 이해하기 어려워지지만, 유연하고 확장 가능한 코드가 된다.

 

차이에 의한 프로그래밍

  • 상속은 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있으며, 부모 클래스의 구현을 재사용할 수 있다.
  • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.

 

상속과 인터페이스

  • 상속은 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려 받게 해 준다.
  • 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.

 

public class Movie {

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

  • Movie는 DiscountPolicy에게 calculateDiscountAmount 메시지를 보내고 있다. Movie 입장에서는 어떤 클래스의 인스턴스가 응답을 주는 지는 상관 없고, 응답을 성공적으로 주기만 하면 된다.
  • 따라서 AmountDiscountPolicy와 PercentDiscountPolicy 모두 DiscountPolicy를 대신해서 Movie와 협력할 수 있다.
  • 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고 부른다. 자식 클래스가 위에 위치한 부모 클래스로 자동적으로 타입 캐스팅되는 것처럼 보이기 때문이다.

 

다형성

  • 다형성은 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 뜻한다.
    • Movie는 동일한 메시지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
  • 다형성은 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실에 기반한다.
  • 다형성은 실행될 메서드를 실행 시점에 결정하므로 지연 바인딩 또는 동적 바인딩이라고 부른다.

 

인터페이스와 다형성

  • 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶다면 추상 클래스가 아닌 인터페이스를 사용하면 된다.

 

추상화와 유연성

추상화의 힘

  • 추상화의 장점
    • 추상화의 계층만 따로 떼어 놓고 살펴보면 요구 사항의 정책을 높은 수준에서 서술할 수 있다.
    • 설계가 좀 더 유연해진다.

 

유연한 설계

  • 추상화는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문에 유연한 설계를 만들 수 있다.

 

추상 클래스와 인터페이스 트레이드 오프

public abstract class DiscountPolicy {

    private List<DiscountCondition> conditions;

    public DiscountPolicy(DiscountCondition... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition condition : conditions) {
            if (condition.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }
        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}

public class NoneDiscountPolicy extends DiscountPolicy {

    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

 

  • 현재 NoneDiscountPolicy는 사실 없어도 DiscountPolicy의 calculateDiscountAmount() 메서드에서 할인 조건이 없으면 0을 반환하고 있어서 문제가 없다. 그러나 사용자 입장에서 Movie의 인자로 할인 정책이 없는 None 정책을 넣어 주어야 해서 추가하였다.
  • 따라서 개념적으로 NoneDiscountPolicy 클래스가 혼란스러우므로 아래와 같이 수정하면 이해하기 좋다.

 

public interface DiscountPolicy {

    Money calculdateDiscountAmount(Screening screening);
}

public abstract class DefaultDiscountPolicy implements DiscountPolicy {

    private List<DiscountCondition> conditions;

    public DefaultDiscountPolicy(DiscountCondition... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition condition : conditions) {
            if (condition.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }
        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}

public class NoneDiscountPolicy implements DiscountPolicy {

    @Override
    public Money calculateDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

public class PercentDiscountPolicy extends DefaultDiscountPolicy {

    private double percent;

    public PercentDiscountPolicy(double percent, DiscountCondition... conditions) {
        super(conditions);
        this.percent = percent;
    }

    @Override
    protected Money getDiscountAmount(Screening screening) {
        return screening.getMovieFee().times(percent);
    }
}

public class AmountDiscountPolicy extends DefaultDiscountPolicy {

    private Money discountAmount;

    public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) {
        super(conditions);
        this.discountAmount = discountAmount;
    }

    @Override
    protected Money getDiscountAmount(Screening screening) {
        return discountAmount;
    }
}

 

  • 위와 같이 DiscountPolicy를 인터페이스로 추상화하고, 그것의 구현체인 NoneDiscountPolicy와 일반적인 할인 정책을 뜻하는 DefaultDiscountPolicy로 나누는 것이다.

 

Untitled

 

  • NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 증가한 복잡도에 비해 효율이 떨어진다고 생각할 수도 있다.
  • 구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있음을 인지하고 설계해야 한다. 즉, 작성하는 모든 코드에는 합당한 이유가 있어야 한다.

 

코드 재사용

  • 상속은 코드를 재사용하기 위해 사용할 수 있다.
  • 하지만 코드 재사용을 위해서는 상속보다는 합성을 사용하는 것이 좋다.
  • 현재 코드에서 Movie가 DiscountPolicy의 코드를 재사용하는 방법이 바로 합성이다.
  • 만약 Movie를 상위 클래스로 두고, AmountDiscountMovie와 PercentDiscountMovie로 나눈다면 상속을 사용한 것이라 할 수 있다.

 

상속

  • 상속의 단점
    • 캡슐화를 위반한다.
      • 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
      • 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률이 높다.
    • 설계를 유연하지 못하게 만든다.
      • 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다.
      • 실행 시점에서 객체의 종류를 변경하는 것이 불가능하다.
      • 하지만 합성은 사용하는 객체 쪽에서 chageDiscountPolicy() 와 같은 메서드를 통해 실행 시점에서 인스턴스를 교체할 수 있다.

 

합성

  • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
  • 캡슐화를 구현할 수 있고, 느슨한 결합을 유지할 수 있다.
  • 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 조합해서 사용해야 한다.
    • 가령, DiscountPolicy 인터페이스는 하위 구현체를 구현하기 위해 상속을 사용할 수 밖에 없다.

 

출처

  • 오브젝트

'스터디 > 오브젝트 스터디' 카테고리의 다른 글

[오브젝트] 1장. 객체, 설계  (0) 2022.04.17

댓글

추천 글