반응형
01 (온라인) 영화 예매 시스템
- 요구사항
- 영화: 영화에 대한 기본 정보를 표현 (제목, 상영시간, 가격 정보)
- 상영: 실제로 관객들이 영화를 관람하는 사건 (상영 일자, 시간, 순번 등)
- 두 용어의 차이의 중요성
- 사용자가 실제로 예매하는 대상은 영화가 아니라 상영
- 사람들은 영화를 예매한다고 표현하지만 실제로 특정 시간에 상영되는 영화를 관람할 수 있는 권리를 구매하기 위해 돈을 지불하는 것
- 특정한 조건을 만족하는 예매자는 요금을 할인받을 수 있음
- 할인액을 결정하는 두 가지 규칙
- 할인 조건(discount condition): 가격의 할인 여부를 결정하며 ‘순서 조건’과 ‘기간조건’ 두 종류로 나눌 수 있다.
- 순서 조건(sequence condition): 상영 순번을 이용해 할인 여부를 결정하는 규칙
- 기간 조건(period condition): 영화 상영 시작 시간을 이용해 할인 여부 결정
- 할인 정책(discount policy): 할인 요금을 결정
- 금액 할인 정책(amount discount policy): 일정 금액을 할인해 주는 방식
- 비율 할인 정책(percent discount policy): 정가에서 일정 비율의 요금을 할인해 주는 방식
- 영화별로 하나의 할인 정책만 할당 (할인 정책을 지정하지 않는 것도 가능)
- 할인 조건은 다수의 할인 조건을 함께 지정하고, 순서 조건과 기간 조건을섞는 것도 가능
- 할인 조건(discount condition): 가격의 할인 여부를 결정하며 ‘순서 조건’과 ‘기간조건’ 두 종류로 나눌 수 있다.
- 사용자가 예매를 완료하면 예매 정보를 생성 (제목, 상영정보, 인원, 정가, 결제금액)01 (온라인) 영화 예매 시스템
02 객체지향 프로그래밍을 향해
협력, 객체, 클래스
객체지향 프로그램을 작성할 때 가장 먼저 고려하는 것은 무엇일까?
어떤 클래스(class)가 필요한지 고민- 클래스가 아닌 객체에 초점
- 어떤 객체들이 필요한지 그리고 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 고민
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력한는 공동체의 일원으로 바라보는 시각
- 공통된 특성과 상태를 가진 객체들의 타입으로 분류하고
- 이 타입을 기반으로 클래스를 구현하라
- 훌륭한 협력 -> 훌륭한 객체 -> 훌륭한 클래스
도메인의 구조를 따르는 프로그램 구조
도메인(domain): 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
- 객체지향 패러다임은 강력하다. 왜?
- 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문
- 요구사항 == 프로그램 == 객체
- 요구사항과 프로그램을 객체라는 동일한 관점으로 바라볼 수 있기 때문에, 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
- 요구사항과 프로그램을 객체라는 동일한 관점으로 바라볼 수 있기 때문에, 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
- 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.
- 클래스 이름: 대응되는 도메인 개념의 이름과 동일하거나 유사하게
- 클래스 사이의 관계: 최대한 도메인 개념 사이에 맺어진 관계와 유사하게
클래스 구현하기
/**
* 상영
*/
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened; // 인스턴스 변수의 가시성은 private
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() { // 메서드의 가시성은 public
return movie.getFee();
}
}
- 훌륭한 클래스를 설계하기 위한 핵심
- 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 클래스의 경계를 구분 짓는 것
- 구분해야 하는 이유는?
- 경계의 명확성 -> 객체의 자율성 보장
- 프로그래머에게 구현의 자유를 제공
자율적인 객체
- 객체가 상태(state)와 행동(behavior)을 함께 가진는 복합적인 존재
- 객체가 스스로 판단하고 행동하는 자율적인 존재
- 객체지향 이전, 데이터와 기능이라는 독립적인 존재로 프로그램 구성
- 반면 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩이로 묶음
- 캡슐화: 데이터와 기능을 객체 내부로 함께 묶는 것 (단순 DTO의 인스턴스 변수를 private 처리 한것을 뜻하는게 아님)
- Java에서는 접근 수정자(access modifier)를 통해 접근 제어(access control) 메커니즘 제공
- 외부의 간섭을 최소화 하기 위해 객체 내부에 대한 접근을 통제 -> 객체를 자율적인 존재로 만듬.
- 훌륭한 객체지향 프로그램을 만들기 위한 핵심 원칙: 인터페이스와 구현의 분리(separation of interface and implementation)원칙
- 퍼블릭 인터페이스(public interface): 외부에서 접근 가능한 부분
- 자바의 키워드인 interface가 아님
- public으로 지정된 메서드
- 구현(implementation): 오직 내부에서만 접근 가능한 부분
- private, protected 로 지정된 메서드
- 객체의 상태는 숨기고 행동만 외부에 공개
- 퍼블릭 인터페이스(public interface): 외부에서 접근 가능한 부분
프로그래머의 자유
- 프로그래머의 두 역할
- 클래스 작성자(class creator)
- 새로운 데이터 타입을 프로그램에 추가
- 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨김
- 클라이언트 프로그래머(client programmer)
- 클래스 작성자가 추가한 데이터 타입을 사용
- 필요한 클래스들을 엮어서 애플리케이션을 빠르고 안정적으로 구축
- 클래스 작성자(class creator)
- 구현 은닉(implementation hiding)
- 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 내부 구현을 마음대로 변경할 수 있음
- 클래스 작성자가 내부 구현을 은닉 할 수 있게 해줌으로써 클라이언트 프로그래머가 실수로 숨겨진 부분을 접근하는 것을 방지
- 객체체의 외부와 내부를 구분하면
- 알아야할 지식의 양이 줄어들고
- 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어진다
- 따라서 클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다
- 설계가 필요한 이유는 변경을 관리하기 위해서이다
협력하는 객체들의 공동체
- 영화를 예매하는 기능
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
- 요금을 계산하는 기능
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this).times(audienceCount);
}
- 금액과 관련된 다양한 계산을 구현하는 클래스
public class Money {
public static final Money ZERO = Money.wons(0);
private final BigDecimal 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));
}
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 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;
}
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Money)) {
return false;
}
Money other = (Money)object;
return Objects.equals(amount.doubleValue(), other.amount.doubleValue());
}
public int hashCode() {
return Objects.hashCode(amount);
}
public String toString() {
return amount.toString() + "원";
}
}
- 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면, 그 개념이 비록 하나의 인스턴스 변수만 포함하더라도 개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높이는 첫걸음이다.
- 예약 클래스
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;
}
}
- 영화를 예매하기 위해 Screening, Movie, Reservation 인스턴스들은 서로의 메서드를 호출하며 상호 작용하게 되는데 이를 협력(Collaboration)이라 부른다.
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
협력에 관한 짧은 이야기
- 메시지와 메서드를 구분 (다형성 개념의 출발)
- 인터페이스에 공개된 행동을 수행하도록 요청할 때 상호작용할 수 있는 유일한 방법은 메시지를 전송(send a message)하는 것 뿐
- 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정하는데
- 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다
03 할인 요금 구하기
할인 요금 계산을 위한 협력 시작하기
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에게 calculateDiscountAmount 메시지 전송
}
}
- 어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않는다
- 단지 discountPolicy에게 메시지를 전송할 뿐이다
- 객체지향에서 중요한 개념
- 상속(inheritance)
- 다형성(polymorphism)
- 그 기반에는 추상화(abstraction)라는 원리가 숨겨져 있다
할인 정책과 할인 조건
- 할인 정책 추상 클래스
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 안에 중복 코드를 두고 AmountDiscountPolicy와 PercentDiscountPolicy가 이 클래스를 상속
- 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리르 자식 클래스에게 위임하는 디자인 패턴을 TemplateMethod 패턴이라 부른다.
- 할인 조건 인터페이스
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
- 순서 조건
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
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;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
// 1. 변수가 적을 땐 값을 직접 넘김 -> 의존성 X
// 2. 너무 많으면 객체를 넘김 (근데 새로 만들어서 받을때도..?) -> 의존성 O
}
- 금액 할인 정책
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);
}
}
- 영화 가격 계산에 참여하는 모든 클래스 사이의 관계를 다이어그램으로 표현
04 상속과 다형성
Movie 내부에 할인 정책을 결정하는 조건문이 없는데도 불구하고 어떻게 영화 요금을 계산할 때 할인 정책과 비율 할인 정책을 선택할 수 있을까? -> 상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
- 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.
- Movie의 인스턴스는 실행 시에 AmountDiscountPolicy 또는 PercentDiscountPolicy에 의존해야 한다.
- 하지만 코드 수준에서는 오직 추상 클래스인 DiscountPolicy에만 의존하고 있다.
- 실행 시점에 협력 가능한 이유는 무엇일까?
- Movie의 인스턴스를 생성할 때 인자로 AmountDiscountPolicy 또는 PercentDiscountPolicy의 인스턴스를 전달하면 된다.
- Movie의 인스턴스를 생성할 때 인자로 AmountDiscountPolicy 또는 PercentDiscountPolicy의 인스턴스를 전달하면 된다.
- 즉 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.
- 유연하고, 쉽게 재사용할 수 있으며, 확장 가능
- 그러나 코드를 이해하기 어려워진다는 단점
- 의존성의 양면성(객체지향 설계를 한다는 것은 적절한 균형이 필요하고 그 균형에 따른 결과 즉 트레이드오프의 산물이다.)
- 훌륭한 객체지향 설계자로 성장하기 위해서는 항상 유연성과 가독성 사이에서 고민해야한다.
- 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다.
차이에 의한 프로그래밍
- 클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사하다고 가정해보자
- 좋은 방법은 그 클래스의 코드를 전혀 수정하지 않고도 재사용하는 것인데 이를 가능하게 해주는 방법이 바로 상속이다.
- 상속은 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있는 간편한 방법을 제공한다.
- 또한 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
- 이처럼 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.
상속과 인터페이스
- 상속이 가치 있는 이유는 **부모 클래스가 제공하는 모든 인터페이스(public 메서드)**를 자식 클래스가 물려받을 수 있기 때문이다.
- 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각한다.
- 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것이다.
- 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
- 결과적으로 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모클래스와 동일한 타입으로 간주할 수 있다.
- 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다.
다형성
- 다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
- 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
- 다형성을 구현하는 방법
- 공통점: 메시지에 응답하기 위해 실행 될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다
- 지연 바인딩(lazy binding), 동적 바인딩(dynamic binding)
- 반대 개념: 초기 바인딩(early binding) 또는 정적 바인딩(static binding)
- 다형성 관점에서의 상속
- 구현 상속(implementation inheritance)
- 서브클래싱(subclassing)이라고 부름
- 코드를 재사용하기 위한 목적
- 인터페이스 상속(interface inheritance)
- 서브타이핑(subtyping)이라고 부름
- 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 하기 위한 목적
대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
- 구현 상속(implementation inheritance)
인터페이스와 다형성
- 자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.
- 자바의 인터페이스란 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 정의한 것이다.
05 추상화와 유연성
추상화의 힘
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미
- 추상화를 사용하면 설계가 좀 더 유연해진다.
유연한 설계
- 추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.
추상 클래스와 인터페이스 트레이드 오프
- DiscountPolicy에서 할인 조건이 없을 경우에는 getDiscountAmount() 메서드를 호출하지 않는데
- 할인 조건이 없는 NoneDiscountPolicy 클래스의 getDiscountAmount() 메서드는 쓰지 않는 메서드가 된다.
- 이것은 부모 클래스인 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다.
- 기존 추상 클래스였던 DiscountPolicy를 인터페이스로 바꾸고
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
- 원래 DiscountPolicy를 DefaultDiscountPolicy로 변경하고 인터페이스를 구현하도록 수정
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
// .... 생략
}
- DiscountPolicy 인터페이스를 NoneDiscountPolicy를 구현하도록 하여 개념적인 혼란과 결합을 제거할 수 있다.
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
- 변경된 설계에 대한 두 가지 생각
- 이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋다.
- 현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다.
- 구현과 관련된 모든 것들이 트레이드오프의 대상이며, 작성하는 모든 코드에는 합당한 이유가 있어야 한다.
코드 재사용
- 객체지향 설계에서는 코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법
- 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속
- 코드를 재사용하기 위해 널리 사용해온 상속은
- 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화를 위반한다.
- 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정하기 때문에 실행 시점에 객체의 종류를 변경할 수 없어 설계를 유연하지 못하게 만든다.
합성
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.
- 합성은 상속이 가지는 두 가지 문제점(캡슐화 위반, 유연하지 못한 설계)을 모두 해결한다.
반응형
'프로그래밍 > 객체지향' 카테고리의 다른 글
Chapter 03 역할, 책임, 협력 (0) | 2022.05.12 |
---|---|
오브젝트:코드로 이해하는 객체지향 설계 Ch.01 (0) | 2022.01.22 |