Table of contents
Open Table of contents
영화 예매 시스템
요구사항 살펴보기
- 영화: 영화에 대한 기본 정보 (제목, 상영시간, 가격정보 등)
- 상영: 실제로 관객들이 영화를 관람하는 시간 (상영일자, 시간, 순번)
- 할인 조건(discount condition): 가격의 할인 여부 결정
- 순서 조건: 상영 순번을 이용해 할인 여부를 결정
- 기간 조건: 영화 상영 시작 시간을 이용해 할인 여부를 결정
- 할인 정책(discount policy): 할인 요금을 결정, 영화별로 하나의 할인 정책만 할당할 수 있다.
- 금액 할인 정책(amount discount policy): 일정 금액을 할인해주는 정책
- 비율 할인 정책(percent discount policy): 일정 비율을 할인해주는 정책
객체지향 프로그래밍을 향해
협력, 객체, 클래스
객체지향 프로그램을 작성할 때 객체지향 언어에 익숙한 사람이라면 가장 먼저 어떤 클래스가 필요한지 고민할 것이다. 안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때만 얻을 수 있다.
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
도메인의 구조를 따르는 프로그램 구조
- 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
- 객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
- 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.
클래스 구현하기
- 클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분짓는 것이다.
- 클래스는 내부와 외부로 구분되며 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정해야 한다.
자율적인 객체
- 객체는 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재다.
- 객체는 스스로 판단하고 행동하는 자율적인 존재다.
대부분의 객체지향 프로그래밍 언어들은 외부에서의 접근을 통제할 수 있는 접근 제어(access control) 메커니즘을 제공한다. 많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private 과 같은 접근 수정자(access modifier) 를 제공한다.
- 외부에서 접근 가능한 부분을 퍼블릭 인터페이스라고 부른다.
- 내부에서만 접근 가능한 부분을 구현이라고 부른다.
프로그래머의 자유 프로그래머의 역할을 클래스 작성자(class creator) 와 클라이언트 프로그래머(client programmer) 로 구분하는 것이 유용하다.
- 클래스 작성자: 새로운 데이터 타입을 프로그램에 추가하고
- 클라이언트 프로그래머: 클래스 작성자가 추가한 데이터 타입을 사용한다.
협력하는 객체들의 공동체
- 객체지향의 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다는 것이다. 비록 하나의 인트턴스 변수만 포함하더라 개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높이는 첫 걸음이다.
- 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 **협력(collaboration)**이라고 부른다.
협력에 관한 짧은 이야기
- 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송(send a message) 하는 것뿐이다.
- 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신(receive a message) 한다고 말한다.
- 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method) 라고 부른다.
할인 요금 구하기
할인 요금 계산을 위한 협력 시작하기
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
클래스를 만들고DiscountCondition
인터페이스를 만든다. DiscountPolicy
안에 중복 코드를 두고 상속받게 한다.- 실제 애플리케이션에서는
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) {
return conditions.stream()
.filter(condition -> condition.isSatisfiedBy(screening))
.findFirst()
.map(this::getDiscountAmount)
.orElse(Money.ZERO);
}
abstract protected Money getDiscountAmount(Screening screening);
}
이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 **템플릿 메서드 패턴(template method pattern)**이라고 부른다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
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) {
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);
}
}
할인 정책 구성하기
하나에 영화에 대해 단 하나의 할인 정책만 설정할 수 있는 것과 할인 조건의 경우에는 여러 개를 적용할 수 있다는 것은 생성자를 통해 제약을 강제한다.
public class Movie {
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
// ...
this.discountPolicy = discountPolicy;
}
}
상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
- 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.
- 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.
- 코드의 의존성과 실행 시점의 의존성이 다를수록 코드를 이해하기 어려워진다.
- 코드의 의존성과 실행 시점의 의존성이 다를수록 코드는 더 유연하고 확장 가능해진다.
차이에 의한 프로그래밍
- 상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
- 부모 클래스의 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 **차이에 의한 프로그래밍(difference programming)**이라고 부른다.
상속과 인터페이스
- 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
- 자식 클래스가 부모 클래스를 대신하는 것을 **업캐스팅(upcasting)**이라고 부른다.
다형성
-
동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라고 부른다.
-
다형성은 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
-
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다. => 인터페이스가 동일해야 한다. => 인터페이스 통일을 위해 상속 사용
-
메시지와 메서드를 실행 시점에 바인딩 하는 것을 지연 바인딩(late binding) 또는 **동적 바인딩(dynamic binding)**이라고 부른다.
-
전통적인 함수 호출처럼 컴파일 시간에 어떤 메서드가 실행될 것인지 결정하는 것을 초기 바인딩(early binding) 또는 **정적 바인딩(static binding)**이라고 부른다.
상속을 구현 상속과 인터페이스 상속으로 분류할 수 있다. 구현 상속을 **서브클래싱(subclassing)**이라고 부르고 인터페이스 상속을 **서브타이핑(subtyping)**이라고 부른다. 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다. 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.
추상화와 유연성
추상화의 힘
- 추상화 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 추상화를 이용하면 설계가 좀 더 유연해진다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다.
- 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 매커니즘을 활용하고 있다.
- 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다. => 설계를 유연하게 만들 수 있다.
유연한 설계
public class Movie {
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 starWars = new Movie("스타워즈", Duration.ofMinutes(210), Money.wons(10000), new NoneDiscountPolicy());
=> 유연성이 필요한 곳이 추상화를 사용하라.
추상 클래스와 인터페이스 트레이드오프
- 구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다.
- 작성하는 모든 코드에는 합당한 이유가 있어야 한다.
코드 재사용
합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용 하는 방법을 말한다.
상속
- 상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다.
- 상속은 캡슐화를 위반한다. (부모 클래스의 내부 구조를 잘 알고 있어야 한다.)
- 설계를 유연하지 못하게 만든다. (실행 시점에 객체의 종류를 변경하는 것이 불가능하다.)
합성
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.
- 효과적으로 캡슐화할 수 있다.
- 설계를 유연하게 만든다.
코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.