본문 바로가기

기술 서적

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

1. 영화 예매 시스템 설명

 

- 영화와 상영의 의미

영화 : 제목, 상영시간, 가격정보처럼 영화가 가진 기본적인 정보

상영 : 상영일자, 시간, 순번 등

=> 사용자가 실제로 예매하는 대상은 영화가 아니라 상영

 

- 할인 조건이란?

할인조건  
순서 조건 상영 순번을 이용해 할인 여부 결정
ex) 조조 할인 -매번 첫번째 상영
기간 조건 - 영화 상영 시작시간을 이용해 할인여부 결정
- 요일, 시작시간, 종료시간
ex) 월요일 오전 10시 -오후 1시 기간 동안 할인 혜택

 

-할인 정책이란?

할인 정책 설명
금액 할인 일정 금액을 할인하는 정책
ex) 800원 할인
비율 할인 일정 비율을 할인하는 정책
ex) 10% 할인

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

 

1) 어떤 클래스가 필요한지 고민하기 전에 어떤 객체가 필요한지 고민하라

- 클래스 윤곽을 위해선 어떤 객체가 어떤 상태와 행동을 가지는지 먼저 결정해야 한다

 

2) 객체를 독립적 존재가 아니라 협력하는 공동체의 일원으로 보아라

- 협력의 문맥을 기반으로 객체를 생각하라

- 객체를 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라

 

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

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

ex) 영화 예매시스템의 도메인 == 영화를 좀더 쉽게 빠르게 예매하려는 사용자의 문제

 

클래스 구현하기

영화 : Movie 

상영 : Screening

 

할인 정책 : DiscountPolicy

 - 금액 할인 : AmountDiscountPolicy

 - 비율 할인 : PercentDiscountPolicy

 

할인 조건 : DiscountCondition

 - 순번 조건 : SequenceCondition

 - 기간 조건 : PeriodCondition

 

상영 클래스

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() {
        return whenScreened;
    }

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

    public Money getMovieFee() {
        return movie.getFee();
    }
}
[필드 설명]
movie : 상영할 영화
sequence : 상영 순서
whenScreened : 상영 시작 시간

[메서드 설명]
isSequence : 매개변수 순번과 같은 순번인지 확인
getMovieFee() : 영화 비용

 

인스턴스 변수의 가시성은 private이고, 메서드의 가시성은 public이다

== 경계의 명확성 => 객체의 자율성 + 구현의 자유

 

자율적인 객체

- 1) 객체는 상태와 행동을 지닌다

- 2) 객체는 스스로 판단하고 행동하는 자율적인 존재이다

- 객체는 접근 제어자를 통해 인터페이스와 구현을 분리했다

- 즉, 상태를 숨기고 행동만을 공개해야 한다

 

프로그래머의 자유

프로그래머의 역할은 클래스 작성자와 클라이언트 프로그래머로 나뉜다

 

클래스 작성자 : 필요한 부분만을 공개하고 나머지는 꽁꽁숨겨야 한다 

=> 구현 은닉

 

구현 은닉의 기대효과

- 클라이언트 프로그래머 : 공개된 인터페이스만 알면 됨(알아야할 지식이 줌)

- 클래스 작성자 : 변경의 폭이 넓어짐


협력하는 객체들의 공동체

 

Screnning 객체

더보기

> reserve(Customer customer, int audeinceCount) : 영화 예매 기능

- customer : 예매자 정보

- audienceCount : 인원 수

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

 

> calculateFee(int audienceCount) : 요금 계산 기능

- caulateMovieFee를 통해 1인당 예매 요금 산출

- 인원수만큼 곱하여 반환

public class Screening {
    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
    }
}

 

Money 클래스 : 간단한 VO

더보기
public class Money {
    public static final Money ZERO = Money.wons(0);

    private final BigDecimal amount;


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

 

=> Long으로 사용가능한데 왜 Money 객체를 만들어주었나?

- 이유1. 저장하는 값이 금액과 관련있다는 의미를 전달

- 이유2. 금액 관련 로직이 서로 다른 곳에 중복되어 구현되는 것을 막음

=> 도메인 의미를 더 명시적으로 표현(유연성과 명확성의 증가)

Reservation 클래스 

더보기

고객, 상영정보, 예매 요금, 인원수 속성으로 포함

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

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

 

 

협력에 관한 짧은 이야기

 

메시지와 메서드는 다르다

- 객체가 다른 객체와 상호작용하는 유일한 방법은 메세지 전송이다.

- 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신했다고 표현한다

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

 

ex) Screening이 Movie의 calculateMovieFee를 호출

= Screening이 Movie에게 calculateMovieFee 메시지를 전송

= Movie는 Screening으로부터 calculateMovieFee 메시지 수신

= Movie는 각자만의 방법(메서드)으로 calculateMovieFee에 응답


3. 할인 요금 구하기

 

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

 

- Movie의 calculateMovieFee는 할인 정책으로부터 할인요금을 전달받음

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 calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening)); //할인 요금만큼 금액을 뺌
    }
}

 

 

할인 정책 : DiscountPolicy

- 추상 클래스로 구현

- TEMPLATE METHOD 패턴

: 부모 클래스에 기본적인 알고리즘 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴

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; // 아무 조건도 만족하지 못했다면 0원 반환
    }

    abstract protected Money getDiscountAmount(Screening Screening);
}

 

구현된 두 할인정책

더보기

> AmountDiscountPolicy(금액 할인 정책)

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; // 일정 금액만큼 할인
    }
}

 

> PercentDiscountPolicy(비율 할인 정책)

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); // 영화금액의 일정 비율만큼 할인
    }
}

 

할인 조건 : DiscountCondition

- 인터페이스로 구현

- isSatisfiedBy(Screening screening)으로 조건이 만족하는지 확인

 

public interface DiscountCondition {
    boolean isSatisfiedBy(Screening screening);
}

 

구현된 두 할인 조건

더보기

> SequenceCondition (순번 조건)

public class SequenceCondition implements DiscountCondition {
    private int sequence;

    public SequenceCondition(int sequence) {
        this.sequence = sequence;
    }

    public boolean isSatisfiedBy(Screening screening) {
        return screening.isSequence(sequence); // 순번이 일치하는지 확인
    }
}

 

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

    public boolean isSatisfiedBy(Screening screening) { //기간이 일치하는지 확인
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
                startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

 

> 영화 계산에 참여하는 클래스 다이어그램

 

ex) 생성자 파라미터 목록을 통해 초기화에 필요한 정보를 전달하도록 강제할 수 있다

- Movie에서는 오직 하나의 할인 정책만을

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

 

4. 상속과 다형성

 

컴파일 시간 의존성과 런타임 의존성

- 컴파일 타임 코드 의존성과 런타임 의존성이 서로 다를 수 있다

- Movie는 컴파일 타임에 DiscountPolicy에만 의존한다.

- 그러나, 런타임에는AmountDiscountPolicy 혹은 PercentDiscountPolicy에 의존한다

 

=> 이러한 차이는 설계를 유연하게 해주나, 가독성을 해친다(연결지점을 정확히 파악해야 함)

 

차이에 의한 프로그래밍

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

 

상속과 인터페이스

: 상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문

== 자식 클래스는 부모 클래스가 수신가능한 모든 메시지를 수신가능하다

 

ex)

public class Movie {
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening)); //할인 요금만큼 금액을 뺌
    }
}

 

- Movie는 할인 정책의 calculateDiscountAmount 메시지를 전송한다.

- Movie 입장에서는 자신과 협력하는 객체가 어떤 클래스의 인스턴스인지가 중요하지 않다.

- 오직, calculateDiscountAmount를 수신가능한지가 중요하다

=> 따라서 서브 클래스인 AmountDiscountPolicy와 PercentDiscountPolicy는 DiscountPolicy를 대신할 수 있다 (업캐스팅)

 

 

다형성

: 동일한 메시지를 수신했을 때 객체 타입에 따라 다르게 응답할 수 있는 능력

: 런타임에 메시지에 대한 메서드를 선택하는 것

 

>> 동적 바인딩  vs 정적 바인딩

동적 바인딩 => 메시지와 메서드를 런타임에 바인딩

정적 바인딩 =>  메시지와 메서드를 컴파일 시점에 바인딩

 


 

5. 추상화와 유연성

 

추상화의 장점

 

1. 일반화된 요구사항 서술

- 높은 수준의 서술 : 영화 예매 요금은 최대 하나의 할인정책과 다수의 할인 조건을 이용해 계산한다

- 낮은 수준의 서술 : 영화의 예매 요금은 금액 할인 정책과 두개의 순서조건, 한개의 기간 조건을 이용해 계산한다

=> 필요에 따라 표현의 수준을 조정 가능함

 

2. 유연한 설계

기존 구조를 수정하지 않고도 쉽게 새로운 기능 추가 가능

 

ex) 할인 정책이 없는 경우 추가

 

방법1. 예외처리 => 일관성이 없어짐

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

 

방법2. NoneDiscountPolicy 확장

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

=> Movie와 DiscountPolicy를 수정하지 않고 확장 가능

=> 컨텍스트 독립성

 

 

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

 

NoneDiscountPolicy의 문제점

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

 

- DiscountPolicy에서 List<DiscountCondition>이 없으면 바로 Money.ZERO를 반환함

=> 따라서 getDiscountAmount에서 아무 값이나 반환해도 상관이 없어짐

 

대안 : DiscountPolicy를 인터페이스화 하고, 기존 추상 클래스를 DefaultDiscountPolicy로 구현

- 개념적인 혼란과 결합을 제거할 수 있음

 

 

=> 모든 코드에는 합당한 이유가 있어야 한다

=> 비록 아주 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다


상속보다 Composition

 

> 상속의 두 가지 단점

 

1. 캡슐화 위반

: 하위 클래스가 상위 클래스를 잘 알고 있어야 한다

 

2. 유연한 설계 불가

: 컴파일 시점에 부모 클래스와 자식 클래스의 관계를 결정

 

ex) 실행 시점에 할인 정책을 변경해야 할 때

- 상속은 인스턴스 자체를 바꾸어 주어야 한다

- 그러나 조합은 필드만 바꾸어 껴주면 된다

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

 

> 조합의 보완점

- 유연한 설계가 가능하다

- 상속은 부모와 자식이 컴파일 타임에 강하게 결합하는데 반해

- 조합은 인터페이스를 통해 약하게 결합된다

 

but, 다형성을 위해 인터페이스를 재사용해야 할 때는 상속+조합을 함께 사용할 수 밖에 없다