📖 2.1 영화 예매 시스템
🔖 2.1.1 요구사항 살펴보기
-
영화
- 영화에 대한 기본적인 정보 표현
- 제목, 상영시간, 가격 정보
-
상영
- 실제로 관객들이 영화를 관람하는 사건을 표현
- 상영일자, 시간, 순번
- 실제로 예매하는 대상은 영화가 아니라 상영임
-
할인조건
- 가격의 할인 여부 결정
- 순서 조건은 상영 순번을 이용해 할인 여부를 결정
- 기간 조건은 영화 상영 시작 시간을 이용해 할인 여부를 결정
- 영화에 다수의 할인 조건을 지정할 수 있다.
-
할인 정책
- 할인 요금 결정
- 금액 할인 정책은 예매 요금에서 일정 금액을 할인
- 비율 할인 정책은 정가에서 일정 비율의 요금을 할인
- 영화별로 하나 이하의 할인 정책만 할당할 수 있다.
📖 2.2 객체지향 프로그래밍을 향해
🔖 2.2.1 협력, 객체, 클래스
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
- 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것
- 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지**를 먼저 결정
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 보자.
- 객체는 홀로 존재하지 않으며, 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재
- 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이를 기반으로 클래스를 구현
🔖 2.2.2 도메인의 구조를 따르는 프로그램 구조
- 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 한다.
❗️ 객체지향 패러다임이 강력한 이유
- 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 동일한 추상화 기법을 사용할 수 있음
- 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있음
🔖 2.2.3 클래스 구현하기
/**
* 상영 Class
*/
@AllArgsConstructor
public class Screening {
/**
* 영화
*/
private Movie movie;
/**
* 순번
*/
private int sequence;
/**
* 상영 시작 시간
*/
@Getter
private LocalDateTime startTime;
/**
* 순번 일치 여부 검사
*/
public boolean isSequence(int sequence) {
return this.sequence == sequence;
}
/**
* 기본 요금 반환
*/
public Money getMovieFee() {
return movie.getFee();
}
}
- 클래스는 내부와 외부로 구분
- 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정해야 함
- 경계의 명확성이 객체의 자율성을 보장하기 때문
- 프로그래머에게 구현의 자유를 제공하기 때문
🎈 2.2.3.1 자율적인 객체
- 객체는 **상태(state) 와 행동(behavior) 을 함께 가지는 복합적인 존재
- 객체는 스스로 판단하고 행동하는 자율적인 존재
- 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 한다.
- 접근 제어 메커니즘 과 접근 수정자
- 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다.
- 캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. (인터페이스와 구현의 분리 원칙)
- public interface
- 외부에서 접근 가능한 부분
- implimentation
- 내부에서만 접근 가능한 부분
- public interface
🎈 2.2.3.2 프로그래머의 자유
프로그래머의 역할을 클래스 작성자(class creator) 와 클라이언트 프로그래머(client programmer) 로 구분하는 것이 유용하다.
- 클래스 작성자
- 새로운 데이터 타입을 프로그램에 추가
- 구현 은닉(implementation hiding): 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨기고 접근을 방지함으로써 내부 구현을 마음대로 변경할 수 있다.
- 클라이언트 프로그래머
- 클래스 작성자가 추가한 데이터 타입을 사용
- 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스를 사용할 수 있기 때문에 머릿속에 담아둬야 하는 지식의 양을 줄일 수 있다.
🔖 2.2.4 협력하는 객체들의 공동체
public class Screening {
/**
* 영화 예매 기능
*
* @param customer 예매자 정보
* @param audienceCount 인원수
* @return 예매 정보
*/
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
/**
* 전체 예매 요금 계산
*
* @param audienceCount 인원수
* @return 전체 예매 요금
*/
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this).times(audienceCount);
}
}
/**
* 예매 요금 Class
*/
@RequiredArgsConstructor
public class Money {
private final BigDecimal amount;
public static final Money ZERO = Money.wons(0);
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 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 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 isGreaterThanOrEqual(Money other) {
return amount.compareTo(other.amount) >= 0;
}
}
- 객체지향의 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다.
- 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현
- 전체적인 설계의 명확성과 유연성을 높이는 첫걸음
/**
* 예매 정보 Class
*/
@AllArgsConstructor
public class Reservation {
/**
* 고객
*/
private Customer customer;
/**
* 상영
*/
private Screening screening;
/**
* 예매 요금
*/
private Money fee;
/**
* 인원 수
*/
private int audienceCount;
}
- 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration) 이라 한다.
🔖 2.2.5 협력에 관한 짧은 이야기
- 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request) 할 수 있다.
- 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response) 한다.
- 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송 하는 것뿐이다.
- 다른 객체에게 요청이 도찰할 때 해당 객체가 메시지를 수신 했다고 이야기한다.
- 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드 라고 부른다.
- 메시지와 메서드의 구분에서부터 다형성(polymorphism) 의 개념이 나온다.
📖 2.3 할인 요금 구하기
🔖 2.3.1 할인 요금 계산을 위한 협력 시작하기
/**
* 영화 Class
*/
@AllArgsConstructor
public class Movie {
/**
* 제목
*/
private String title;
/**
* 상영시간
*/
private Duration runningTime;
/**
* 기본요금
*/
@Getter
private Money fee;
/**
* 할인 정책
*/
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
calculateMovieFee
에는 상속 과 다형성 의 개념이 숨겨져 있다.
🔖 2.3.2 할인 정책과 할인 조건
/**
* 할인 정책 class
*/
public abstract class DiscountPolicy {
private final List<DiscountCondition> conditions;
protected 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;
}
protected abstract Money getDiscountAmount(Screening screening);
}
- 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 Template Method 패턴 이라고 한다.
/**
* 할인 조건 interface
*/
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
@AllArgsConstructor
public class SequenceCondition implements DiscountCondition {
/**
* 순번
*/
private int sequence;
/**
* 할인 여부 판단 Method
* <pre>
* - 상영 순번과 일치할 경우 할인 가능
* </pre>
*
* @param screening 상영
* @return 할인 여부
*/
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
@AllArgsConstructor
public class PeriodCondition implements DiscountCondition {
/**
* 조건에 사용할 요일
*/
private DayOfWeek dayOfWeek;
/**
* 시작 시간
*/
private LocalTime startTime;
/**
* 종료 시간
*/
private LocalTime endTime;
/**
* 할인 여부 판단 Method
* <pre>
* - 상영 요일이 같고, 상영 시작 시간이 startTime과 endTime 사이에 있을 경우 할인
* </pre>
* @param screening 상영
* @return 할인 여부
*/
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
!startTime.isAfter(screening.getStartTime().toLocalTime()) &&
!endTime.isBefore(screening.getStartTime().toLocalTime());
}
}
/**
* 금액 할인 정책 class
*/
public class AmountDiscountPolicy extends DiscountPolicy {
/**
* 할인 요금
*/
private final Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
/**
* 비율 할인 정책 class
*/
public class PercentDiscountPolicy extends DiscountPolicy {
/**
* 할인 비율
*/
private final double percent;
public PercentDiscountPolicy(double percent, DiscountCondition... conditions) {
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}
🔖 2.3.3 할인 정책 구성하기
- 하나의 영화에 대한 단 하나의 할인 정책만 설정할 수 있지만 여러 개의 할인 조건을 적용할 수 있다.
- Movie와 DiscountPolicy의 생성자는 이런 제약을 강제한다.
📖 2.4 상속과 다형성
🔖 2.4.1 컴파일 시간 의존성과 실행 시간 의존성
어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 한다.
Movie
의 인스턴스는 실행 시에AmountDiscountPolicy
나PercentDiscountPolicy
의 인스턴스에 의존해야 한다.- 하지만 코드 수준에서 Movie 클래스는 오직 추상 클래스인
DiscountPolicy
에만 의존한다.
❗️ 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다
- 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것
- 코드를 이해하기 어려워짐
- 코드 유연성 ⬆️
- 코드 확장성 ⬆️
- 결국 trade-off의 산물이다.
🔖 2.4.2 차이에 의한 프로그래밍
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
- 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
- 차이에 의한 프로그래밍(programming by difference)
🔖 2.4.3 상속과 인터페이스
상속이 가치 있는 이유
- 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있음.
- 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
- 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
- 업캐스팅(upcasting)
- 컴파일로는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용
🔖 2.4.4 다형성
- 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
- 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력
- 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
- 메시지와 메서드를 실행 시점에 바인딩한다.
- 지연 바인딩(lazy binding), 동적 바인딩(dynamic binding)
- 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다.
🔖 2.4.5 인터페이스와 다형성
- 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때 인터페이스라는 프로그래밍 요소를 쓰면 된다.
📖 2.5 추상화와 유연성
🔖 2.5.1 추상화의 힘
- 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
- 상위 개념만으로 도메인의 중요한 개념을 설명할 수 있다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것
- 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의
- 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
🔖 2.5.2 유연한 설계
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
return fee;
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
- 이 방식은 일관성 있던 협력 방식이 무너지게 된다.
- 할인 금액이 0원이라는 사실을 결정하는 책임이
DiscountPolicy
가 아닌Movie
에 있음.
/**
* 0원 할인 정책 class
*/
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
- 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능을 확장함
유연성이 필요한 곳에 추상화를 사용하라.
🔖 2.5.3 추상 클래스와 인터페이스 트레이드오프
- 구현과 관련된 모든 것들은 trade-off의 대상이 될 수 있다.
DiscountPolicy
를 interface로 변환
🔖 2.5.4 코드 재사용
- 코드 재사용을 위해서는 상속보다는 **합성(composition)**이 더 좋은 방법
- 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용
🔖 2.5.5 상속
상속은 두 가지 관점에서 설계에 안 좋은 영향을 미친다.
-
캡슐화 위반
- 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화 약화
- 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다.
-
설계를 유연하지 못하게 만듦
- 상속은 부모 클래스와 자식 클래스 사이의 관게를 컴파일 시점에 결정
- 실행 시점에 객체의 종류를 변경하는 것이 불가능
public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
- 위와 같이 인스턴스 변수로 연결한 방법을 사용하면 실행 시점에 할인 정책을 간단하게 변경할 수 있다.
🔖 2.5.6 합성
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
- 구현을 효과적으로 캡슐화할 수 있다.
- 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
- 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.