오브젝트; 객체지향 프로그래밍

🗓️

이번 장은 이 책을 읽으면서 이해하게 될 다양한 주제들을 얕은 수준으로 가볍게 살펴보는 것

영화 예매 시스템

책의 예제 소개를 위한 요구사항

  • 할인 조건
    • 순서 조건 : 상영 순번으로 할인 여부 결정
    • 기간 조건 : 영화 사영시작 시간으로 할인 여부 결정
  • 할인 정책 : 할인 할 요금 결정
    • 금액 할인 정책 : 정액 할인
    • 비율 할인 정책 : 비율 할인

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

협력, 객체, 클래스

객체지향 패러다임으로 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.

  1. 어떤 클래스가 필요한지 고민 하기 전에 어떤 객체들이 필요한지 고민하라
    • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화 한 것
    • 어떤 상태와 행동을 가지는 객체인지 먼저 생각해야함
  2. 객체를 독립적인 존재가 아닌 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
    • 객체는 다른 객체에게 도움을 주거나 의존하는 협력 존재

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

도메인

문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

  • 객체지향 패러다임의 장점은 요구사항 분석 단계부터 구현까지 객체라는 추상화 기법을 사용하는것이다.
  • 요구사항과 프로그램을 객체라는 동일 관점에서 본다. 때문에 매끄럽게 연결도니다.

클래스 구현

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
        this.movie = movie;
        this.sequence = sequence;
        this.whenScreened = whenScreened;
    }

    public LocalDateTime getStartTime() //...
    public boolean isSequence() //...
    public Money getMoiveFee() //...

}
  • publicprivate 같은 접근 수정자를 통해 접근을 제한함으로써 경계의 명확성이 생긴다.
  • 경계의 명확성이 생기면 객체의 자율성을 보장할 수 있다.
  • 구현의 자유가 생긴다.

자율적인 객체

  • 객체는 상태 state와 행동 behavior를 함께 가지는 복합적인 존재다.
  • 객체가 스스로 판단하고 행동하는 자율적인 존재다.
  • 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 한다.
  • 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶는 캡슐화를 한다.

객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하는 것이다. 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다. 외부에서는 객체가 어떤 상태에 놓여있는지, 어떤 생각을 하고 있는지 알아서 안되며, 결정에 직접적으로 개입하려고 해서도 안된다. 객체에게 원하는 것을 요청하고 객체가 스스로 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.

  • 퍼블릭 인터페이스 public interface : 외부에서 접근 가능한 부분
  • 구현 interface : 외부에서 접근 불가능하고 오직 내부에서만 접근 가능한 부분

프로그래머의 자유

  • 구현 은닉 implementation hiding : 클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨김으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있는 기법

협력에 관한 짧은 이야기

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있다.
  • 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답 한다.
  • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐이다.
  • 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
  • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드 method 라고 한다.

할인 요금 구하기

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

public class Movie {
    private String title;
    private Duration runningTime;
    private Money money;
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        // constructor...
    }

    public Money getFee() // getter..

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

어떤 할인정책을 사용할지는 discountPolicy 에 메시지를 전송할 뿐이다.

할인 정책과 할인 조건

할인 정책의 금액할인정책과 비율할인정책을 각각 구현한다. 클래스를 작성할때 중복코드를 없애기 위해 추상클래스를 사용한다

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

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

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

        retunr Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}
  • 할인 조건을 만족하는 DiscountCondition이 하나라도 존재하는 경우 추상매서드인 getDiscountAmount()를 호출한다.
  • DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만, 실제로 요금 계산을 하는 부분은 추상메서드에 위임한다.
  • 부모 클래스에 기본적인 알고리즘의 프름을 구현하고 중간에 피룡한 처리를 자식 클래스에게 위임하는 디자인을 템플릿 메소드 Template method 패턴 이라고 한다.

그리고 할인 조건은 인터페이스로 구현한다.

코드 보기
public interface DiscountCondition {
    boolean isSaticefiedBy(Screening screening);
}

public class SequenceCondition implements DiscountCondition {
    private int sequence;

    public SequenceCondition(int sequnce){
        // constructor...
    }

    public boolean isSatisfiedBy(Screening screening) {
        return screening.isSatisfiedBy(sequence);
    }
}

public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
        // constructor...
    }

    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
        endTime.compareTo(screening.getStartTime().toLocalTime()) >=0;
    }
}

public class AmountDiscountPolicy extends DiscountPolicy {
    private Money discountAmount;

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

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

public class PercentDiscountPolicy extends DiscountPolicy {
    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);
    }
}

상속과 다형성

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

  • AmountDiscountPolicyPercentDiscountPolicyDiscountCondition를 상속받는다.
  • 그러므로 영화요금을 계산 할 때는 AmountDiscountPolicyPercentDiscountPolicy 인스턴스에 의존하는데 코드 수준에서는 DiscountCondition에만 의존한다.

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수있다. 유연하고, 쉽게 재사용 될 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 의존성과 실행 시점의 의존성이 다른다는 것이다.

차이에 의한 프로그래밍

  • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그램이라고 한다
  • 자바에서는 상속을 사용해 코드 재사용을 한다.

상속과 인터페이스

  • 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 메서드나 인스턴스 변수를 재사용하는 것이 목적은 아니다.
  • 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다. 컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.
  • 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅 upcasting 이라고 한다.

다형성

  • 호출하는 쪽에서 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지느 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이것을 다형성이라 한다
  • 다형성의 기본 전제는 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 점을 기반으로 한다.
  • 다형성이랑 동일한 메시지를 수신 했을때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다. 따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 이것은 인터페이스가 동일하다는 말이다.
  • 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정하는(바인딩하는) 것을 지연 바인딩 lazy binding 또는 동적 바인딩 dynamic binding 이라고 한다.
  • 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 early binding 또는 정적 바인딩 static binding 이라고 한다.
  • 객체지향이 컴파일 시점의 의존성과 실행 시점의 의존성을 분리하고, 하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유가 바로 지연 바인딩 메커니즘 덕분이다.

인터페이스와 다형성

  • 구현은 공유할 필요가 없고 인터페이스만 공유하고 싶을때 인터페이스 요소가 있다.
  • 추상클래스와 마찬가지로 업캐스팅이 적용된다

추상화와 유연성

추상화의 힘

  • DiscountConditionDiscountPolicy는 각각 두 구현체보다 추상적이다
  • 추상적이라는 것은 인터페이스와 관련이 있다.
    • DiscountConditioncalculateDiscountAmount()의 메시지를 결정한다.
    • DiscountPolicyisSatisfiedBy()의 메시지를 정의한다.
  • 부모 클래스는 인터페이스를 정의하며 구현의 일부 또는 전체를 자식 클래스가 결정할 수 잇도록 위임한다.
  • 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
  • 세부사항에 억눌리지 않고 상위 개념만으로 도멩니의 중요한 개념을 설명할 수 있게 한다.
  • 자식 클래스들은 추상화를 이용해서 정의한 사우이의 협력 흐름을 그대로 따른다. 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용한다.

유연한 설계

이러한 추상화 구조가 가지는 장점은 변경이 자유롭다는데 있다. 할인정책의 할인 금액이 0원이라는 계산을 추가한다면 다음과 같이 할 수 있다.

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

이로 인해 사용처와 구현을 수정하지 않고 새로운 클래스를 추가하는 것 만으로도 어플리케이션의 기능을 간단하게 확장했다.

DiscountPolicyDiscountCondition가 특정 구현이나 조건에 묶여있지 않기 때문에 상속을 받은 어떤 클래스와도 협력이 가능해진다. 이를 컨텍스트 독립성 Context independent 라고 한다

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

  • 추상클래스를 인터페이스화 하면 오버라이딩을 통해 사용하지 않는 인터페이스를 제거할 수 있다.

코드 재사용

상속 보다는 합성 Composition이 더 좋은 방법이다.

  • 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

상속

상속은 두가지 관점에서 설계에 좋지 못한 영향이 있다.

첫번째. 상속은 캡슐화를 위반한다

  • 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 알고 있어야 한다.
  • 이로 인해 부모 클래스의 구현이 자식 클래스에 노출되기 때문에 캡슐화가 약화된다
  • 그렇기 때문에 부모 클래스의 변화가 생기면 자식 클래스도 변경된다.

두번째. 설계를 유연하지 못하게 한다.

  • 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
  • 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.

합성

호출하는 쪽에서 비즈니스 요구사항을 수행하기 위해 DiscountPolicy 코드를 재사용한다.

public class Movie { // DiscountPolicy를 호출하는 쪽
    private DiscountPolicy discountPolicy;
    // ... and more member

    // ... constructor

    public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    public void booking() {
        Movie avatar = new Movie(new AmountDiscountPolicy(Money.wons(800), ...), ...);

        avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1 ...));
    }
}

이 방법이 상속과 다른 점은 상속이 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는데 비해 호출하는 쪽이 DiscountPolicy의 인터페이스를 통해 약하게 결합된다는 것이다.

  • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.

합성은 상속이 가지는 두가지 문제를 모두 해결한다.

  • 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 한다.
  • 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
  • 상속은 클래스를 통해 강하게 결합되는데 비해 합성은 메시지를 통해 느슨하게 결합된다.