본문 바로가기

IT/오브젝트

[오브젝트] 2장 객체 지향 프로그램

 

안녕하세요 남갯입니다

 

오늘은 오브젝트 2장 3장에 대해 포스팅 해보려고 합니다.

 

영화

- 영화는 영화에 대한 기본정보를 표현한다.

- 제목, 상영시간 가격정보와 같이 영화가 가지고 있는 기본정보를 가리킬때 영화라는 단어를 사용

 

상영

- 상영일자, 시간, 순번등을 가리키기 위해 상영이라는 용어를 사용한다.

 

할인액을 결정하기 위한 조건

할인조건

할인 조건은 가격의 할인 여부를 결정하며 '순서조건' , '기건조건' 두 종류

순서조건

순서조건은 상영순번을 이용해 할인여부를 결정

순번이 10인경우 10번째로 상영되는 영화를 예매한 사용자들에게 할인 혜택을 제공

기간조건 

기간조건은 상영시작 시간을 이용해 할인 여부를 결정한다.

요일, 시작시간, 종료시간 세부분으로 구성되며 영화 시작 시간이 해당 기간안에 포함될 경우 요금을 할인한다.

ex) 요일이 월요일, 10시~ 오후 1시인 기간 조건을 사용하면 모든 영화에 대한 할인 혜택 적용

 

 

할인정책

할인 정책에는 '금액 할인 정책', '비율 할인 정책'이 있다.

 

금액할인정책

예매요금에서 일정 금액을 할인해주는 방식이다.

9000원의 영화가 800원의 금액 할인정책일 경우 8200원이 된다.

 

비율할인정책

9000원이 비율할인정책으로 10%의 혜택을 받을 경우 8100원이 된다.

 

 

 

영화별로 하나의 할인 정책만 할당할 수 있다, 할인정책을 지정하지 않는것도 가능

할인 조건은 다수의 할인 조건을 함께 지정할 수 있다. 

 

 

 

협력 객체 클래스

객체지향은 가장 먼저 클래스의 필요를 고민하고 메서드를 고민할 것이다.

하지만 객체지향 언어는 이런것과는 본질적으로 거리가 멀다고 한다. 말 그대로 객체를 지향하는것이다.

 

따라서 프로그래밍에서 두가지를 집중해야한다.

1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라. (객체에 중점을 둬라)

2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다. 즉 객체는 협력적인 존재이므로 공동체의 일원으로 바라보는것이 설계를 유연하고 확장 가능하게 만든다.

 

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

도메인이라는 용어를 살펴보는 것이 도움된다.

영화 예매 시스템의 목적은 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는것

이처럼 사용자가 프로그램을 사용하는 분야를 도메인 이라고 부른다.

 

영화 예매 도메인을 구성하는 개념 관계를 표현한 것이다.

 

도메인에 대한 설명

영화는 여러 번 상영될 수 있고 상영은 여러 번 예매 될 수 있다는 것을 알 수 있다.

영화에는 할인정책을 할당하지 않거나 할당 가능하고,

할인정책이 존재하는 경우에는 하나의 할인 조건이 반드시 존재한다.

할인 정책의 종류는 금액할인, 비율할인 정책이 있고 할인 조건의 종류로는 순번조건과 기간조간이 있다.

이 도메인의 개념을 따르는 클래스 구조에 맞춰 짜야한다.

 

클래스 구현하기

 

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

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

    public Movie getMovie() {
        return movie;
    }

    public boolean isSequence(int sequence) {
        return this.sequence == sequence;
    }

    public Money getMovieFee() {
        return movie.getFee();
    }
}

 

여기서 중요한점은 인스턴스 변수의 가시성은 private이고 메서드는 public 이라는것이다.

외부에서는 private를 접근 못하게하고 public 메서드를 통해서만 내부 상태를 변경하도록 해야한다.

 

내부와 외부를 구분하는 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이다. 또한

프로그래머에게 구현의 자유를 제공하기 때문이다.

경계가 명확하고 외부에 필요한것만 노출하는 캡슐화라고 생각한다.

 

자율적인 객체

중요한 사실 두가지

1. 객체는 상태와 행동을 함께 가지는 복합적인 존재이다.

2. 객체가 스스로 판단하고 행동하는 자율적인 존재

 

객체지향 이전에서는 데이터와 기능이라는 독립적인 존재를 엮어서 프로그램 구성

객체지향은 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현

캡슐화이다.

 

캡슐화와 접근제어는 객체를 두부분으로 나눈다.

1. 외부에서 접근 가능한 부분으로 퍼블릭 인터페이스라고 부른다.

2. 외부에서 접근 불가하고 내부에서 가능한 부분으로 구현이라 한다.

따라서 여기서 말하는 인터페이스구현의 분리 원칙은 핵심원칙이다.

 

 

프로그래머의 역할

1. 클래스 작성자.

- 새로운 데이터 타입을 프로그램에 추가

- 필요한 클래스들을 엮어서 어플리케이션을 빠르게 안정적으로 구축

- 필요한 부분만 공개하고 나머진 캡슐화 (구현은닉)

 

2. 클라이언트 프로그래머.

- 클래스 작성자가 추가한 데이터 타입을 사용

 

    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(cusomer, this, calculateFee(audienceCount), audienceCount);
    }

calculateFee라는 메서드는 요금을 계산하기 위해 다시 Movie의 calculateMovieFee 메서드를 호출

calculateMovieFee메서드의 반환은 1명의 예매 요금

 

 

public class Money {
    public static final Money ZERO = Money.wons(0);
    private final BigDecimal amount;

    public Money(BigDecimal amount) {
        this.amount = amount;
    }

    public  Money plus(Money amount){
        return new Money(this.amount.add(amount.amount));
    }


    public Money minus(Money amount){
        return new Money(this.amount.subtract(amount.amount));
    }


    public static Money wons(long amount){
        return new Money(BigDecimal.valueOf(amount))
    }

    public static Money wons(double amount){
        return new Money(BigDecimal.valueOf(amount))
    }

    public Money times(double percent){
        return new Money(this.amount.multiply(BigDecimal.valueOf(percent)))
    }

    public boolean isLessThan(Money other){
        return amount.compareTo(other.amount) < 0;
    }

    public boolean isGreatorThanOrEqual(Money other){
        return amount.compareTo(other.amount) >= 0;
    }

}

 

Long보다는 BigDecimal을 사용해 금액임을 표시

 

public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audientCount;

    public Reservation(Customer customer, Screening screening, Money fee, int audientCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audientCount = audientCount;
    }
}

 

영화를 예매하기 위해서 위의 세 클래스가 서로의 메서드를 호출하며 상호작용을 한다.

객체들끼리의 상호작용을 협력이라고 부른다.

 

 

객체 지향 프로그램에서는 협력의 관점에서 어떤 객체가 필요한지를 결정하고 구현하기 위한 클래스를 작성

 

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청, 자율적인 방법에 따라 처리한후 응답.

 

메세지와 메서드

메세지는 객체가 다른객체와 상호작용하는 방법 (메세지 전송) , 요청이 올때 까지 기다리는것을 (메세지 수신)

메서드는 수신된 메시지를 처리하기 위한 자신만의 방법

 

메세지와 메서드의 구분에서부터 다형성의 개념이 출발한다.

 

지금까지는 Screening이 Movie의 CalculateMovieFee 메서드를 호출한다고 말했지만 사실은 메세지를 전송한다가 적절한 표현이다. (위에서 아직 코드에서 Movie는 나오지 않았다, 위의 협력 그림에서 Screening은 Movie의 CalculateFee 메서드가 존재하는지 를 모른다)

 

내 생각은 메서드는 자신만의 방법인것이고 메세지는 상대의 객체가 무슨 행동을 하지 모르는것이니

메세지를 호출함으로써 동작하는지 혹은 수신하는지를 모르게 메세지를 전송했다가 적합하다는 의미같다.

 

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

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }

    public Money getFee() {
        return fee;
    }

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

여기 메서드 안에서는 이상한점이 있다.

할인정책(DiscountPolicy)를 결정하는 코드가 없고 DiscountPolicy에게 메세지를 전송할 뿐이다.

(이 코드가 어색하다면 객체지향을 모르는 애송이)

 

객체지향의 중요한 개념 중요한것

1. 상속

2. 다형성

3. 추상화

 

할인 정책과 비율할인 정책을 각각

AmountDiscountPolicy, PercentDiscountPolicy라는 클래스로 구현할것이다.

두 클래스는 대부분의 코드가 유사하고 계산하는 방식만 조금 다르다. 따라서 중복코드는 DiscountPolicy

안에 중복 코드를 두고 AmountDiscountPolicy 와 PercentDiscountPolicy가 상속받게 한다.

 

 

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);
            }
        }
        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}

 

이처럼 DiscountPolicy는 DiscountCondition의 리스트인 discountConditions를 인스턴스 변수로 가지기 때문에 하나의 할인정책에는 여러개의 할인 조건을 포함 할 수 있다. calculateDiscountAmount 메서드는 전체 할인 정책에 대해 차례대로isSatisfiedBy 메서드를 호출한다. 만족할경우 true, 아닐경우 false를 반환

 

만족할경우 추상메서드인 getDiscountAmount 메서드를 호출

 

기본적인 알고리즘의 흐름을 구현하고 필요의 처리를 자식들에게 위임하는것을

탬플릿 매서드 패턴이라 부른다.

2019/06/05 - [IT/개발스터디 디자인패턴] - [디자인패턴] 템플릿 메소드 패턴

 

인터페이스로 할인조건을 만든 뒤

public interface DiscountCondition {
    // 할인여부를 판단 하기 위해 사용할 순번을 인스턴스 변수로 전달
    boolean isSatisfiedBy(Screening screening);
}

 

두가지 할인조건인 SequnceCondition 과 PeriodCondition을 만들어보자

 

public class PeriodCondition implements DiscountCondition {

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
        this.dayOfWeek = dayOfWeek;
        this.startTime = startTime;
        this.endTime = endTime;
    }

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

할인 정책을 만족하는지에 대한 코드이다.

 

개인적으로 screnning.getStartTime()은 변수로 빼서 사용했다.

 

 

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);
    }
}

 

 

 

벤다이어 그램을 통해 표현해보면

추상클래스와 인터페이스를 통해 잘 추상화한것을 볼 수 있다.

 

오버로딩과 오버라이딩의 개념은 다 알것이고...

 

 

 

 

할인정책 구성

 

영화에서는 하나의 할인정책만을 설정 가능하고 할인 조건은 여러개를 적용 가능했었다.

Movie와 DiscountPolicy의 생성자를 통해 이런 제약을 강제한다.

 

Movie의 생성자는 오직 하나의 DiscountPolicy 인스턴스만 받도록 선언되어 있다.

반면 DiscountPolicy의 생성자는 여러개의 DiscountCondition 인스턴스를 허용한다.

 

        Movie avatar = 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(11,59)),
                        new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10,0),LocalTime.of(20,59))
                );

이렇게 할인정책은 하나, 할인 조건은 여러개를 만족한다.

 

Movie 클래스에서 어디에도 할인정책이 금액인지 비율인지 판단하지 않는다.

Movie에서는 조건문도 없는데 어떻게 할인정책과 비율정책을 선택할 수 있을까?

 

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

Moive 클래스가 DiscountPolicy와 연결되어있기 때문에 실제로 Movie는 실제화된 클래스와 연결되어있다.

Movie의 인스턴스가 코드 작성 시점에는 '내부 동질성' 때문이다.

 

실제 구현할때 생성한 인스턴스로 의존한다.

 

코드의 의존성 / 실행시점 의존성

코드의 의존성은 실체화해서 객체를 만들어서 코드를 이해하기 쉽다.

실행시점 의존성이 다르면 코드를 이해하기 어렵지만 코드간의 의존성이 낮아 유연하고 확장이 가능해진다.

설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워 진다.

 

상속을 통해 부모클래스와 다른부분만을 추가해서 새로운 클래스를 쉽게 빠르게 만드는 방법을

차이에 의한 프로그래밍이라고 부른다고 한다.

 

용어

슈퍼클래스, 부모클래스, 부모, 직계조상, 직접적인 조상 ,기반클래스 ,슈퍼클래스

서브클래스, 자식클래스, 자식, 직계자손, 직접적인 자손 ,파생클래스 ,서브클래스

 

 

상속 인터페이스

실제 타입과 다르더라도 Movie 부모클래스를 전달하더라도 가능하다.

자식클래스가 부모클래스를 대신하는것을 '업 캐스팅' 이라한다

 

다형성

코드상에서 Movie 클래스는 DiscountPolicy 클래스에 메세지를 전송하지만 실생 시점에 실제로 실행되는 메서드는 Movie와 협력하는 객체의 실제 클래스가 무엇인지에 따라 달라진다. Movie는 동일한 메세지를 전송하지만

실제로 어떤 메서드가 실행될것인지는 메세지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.

이것을 다형성이라 부른다.

 

즉 다형성은 컴파일 시간의 의존성과 실행시간 의존성이 다를 수 있다는 사실을 기반으로 한다.

 

바인딩

메서드의 실행시점에 바인딩 하는것 : (지연 바인딩/동적 바인딩)

컴파일시점에 실행될 함수나 프로시저를 결정하는것을 (초기 바인딩/정적 바인딩)

 

 

인터페이스와 다형성

순수하게 내부구현 함수가 아닌 인터페이스만 공유하고 싶을 때 인터페이스라는 것을 통해 제공한다.

실체화된것에 인터페이스를 공유하며 다형적인 협력에 참여 가능

 

 

추상화의 힘

프로그래밍에서는 인터페이스에 초점을 맞추면 추상적여진다. 인터페이스를 정의하여 구현의 일부 또는 전체를 자식클래스가 결정할 수 있도록 결정권을 위임한다.

 

추상화를 통한 두가지 장점

1. 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은수준에서 서술할 수 있다.

2. 설계가 좀 더 유연해진다.

 

1번의 장점을 알아보자.

하나의 할인정책과 다수의 할인조건을 이용해 계산할 수 있다로 표현할 수 있다.

두개의 할인정책과 두개의 순서조건, 한개의 기간조건을 이용해 계산할 수 있다라는 문장을 포괄 가능하다.

즉 정책과 조건들을 추상적인 개념을 통해 추상화했기 떄문에 가능한 것이다.

 

추상화를 통해 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현 할 수 있다.

 

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

빼먹은 동작 구현했지만 이 조건문은 협력의 설계 측면에서 좋지 않은 선택이므로

0원의 동작의 계산할 책임을 그대로 DiscountPolicy 계층에 유지시켜라

 

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

 

요렇게 말이다.

 

따라서 

 

 Movie avatar = new Movie("아바타",
                Duration.ofMinutes(120),
                Money.wons(10000),
                new NoneDiscountPolicy());

이코드를 통해 할인되지 않는 정책을 생산 가능하다.

 

여기서 가장 중요한 포인트는 Movie와 DiscountPolicy는 수정하지 않고 새로운 클래스를 추가하는것 만으로

어플리케이션을 확장했다. (OCP) 컨텍스트 독립성이라는 개념이 유연한 설계를 이루게 했다.

 

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

public interface DiscountPolicy{
    Money calculateDiscountAmount(Screening screening);
}

 추상클래스를 인터페이스로 변경해보자.

public abstract class DefaultDiscountPolicy implements DiscountPolicy {
}

이렇게 구현하도록 변경하면 개념적인 혼란과 결합을 제거 가능하다.

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

 

 

인터페이스를 사용하면 훨씬 더 좋은 설계가 나온다.

현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들지만

이전의 클래스역시 0원이라는 사실을 효과적으로 전달하기 때문이다.

 

여기서 말하는 내용은 모든것들이 트레이트오프가 대상이 될 수 있다.

 

 

합성(구성)

흔히 알고있는 구성 책에서는 합성이라는 단어를 통해 얘기하고 있다.

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

Movie가 DiscountPolicy의 코드를 재사용하는 방법은 합성(구성)이다. 이 설계를 상속을 하도록 변경할 수도 있다.

상속보다 합성(구성)을 선호하는 이유는 무엇일까?

 

 

상속

상속의 큰 문제점은

1. 캡슐화를 위반한다는 것이다.

- 부모클래스의 구현이 자식클래스에게 노출되기 때문에 캡슐화가 약화된다.

2. 설계를 유연하지 못하게 만드는것

- 부모와 자식클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체 종류를 변경하지 못한다.

위에서 말한 정적 바인딩 , 그래서 유연하지 않다.

 

ChangeDiscountPolicy 메서드를 통해 변경 가능

합성(구성)

합성을 통하면 

1. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 가능

2. 상속은 클래스를 통해 강하게 결합되는 데 비해 합성(구성)은 메시지를 통해 느슨하게 결합된다.

 

즉 상속을 하지 말라는 경우가 아니라는 내용은 아니다. 즉 상황에 맞게 사용하고 객체의 책임과 협력에 맞게

객체를 지향하자