객체지향 프로그래밍에 대한 주제로 내용이다.
자세한 내용이 궁금하면 오브젝트를 펼쳐보는 걸 추천드립니다.📖
영화 예매 시스템을 구현해보기
시스템을 구현하기 위한 요구사항은 다음과 같다.
- 시간대 별로 영화를 예매한다.
- 요금할인은 할인 정책(discount policy)과 할인 조건(discount condition) 2가지다.
할인 정책은 금액과 비율로 나눠지고 할인 조건은 기간과 순서로 나눠진다.
객체지향 프로그래밍을 향해
진정한 객체지향 패러다임의 전환은 객체에 초점을 맞춰야 얻는다.
첫째, 어떤 객체들이 필요한지 고민한다. 객체 중심의 접근 방법은 설계를 단순하고 깔끔하게 만든다
둘째, 객체를 협력하는 공동체의 일원으로 봐야한다. 훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 클래스를 낳는다.
도메인의 구조를 따르는 프로그램 구조
도메인(domain)이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 뜻한다. 객체지향 패러다임이 강력한 이유는 요구사항과 프로그램을 객체라는 동일한 관점에서 바라보기에 도메인 개념들을 객체와 클래스로 매끄럽게 연결할 수 있다.
일반적으로 클래스의 이름은 도메인 개념의 이름과 동일하거나 유사해야 한다.
이제 구현으로 넘어가 보자.
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime localDateTime) {
this.movie = movie;
this.sequence = sequence;
this.localDateTime = localDateTime;
}
public LocalDateTime getStartTime() {
return whenScreened;
}
public boolean isSequence(int sequence) {
return this.sequence == sequence;
}
public Money getMovieFee() {
return movie.getFee();
}
}
public class Movie {
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(Money fee, DiscountPolicy discountPolicy) {
this.fee = fee;
this.discountPolicy = discountPolicy;
}
public Money getFee() {
return fee;
}
public Money calculateDiscountFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
인스턴스 변수의 가시성은 private이고 메서드의 가시성은 public이다. 이는 클래스의 경계를 구분 짓는 것으로 설계 핵심은 어떤 것을 외부에 공개하고 숨길지 결정하는 것이다.
내외부를 구분하는 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이고, 프로그래머에게 구현의 자유를 제공한다.
자율적인 객체
두 가지를 알고 넘어가자.
첫째, 객체가 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재.
둘째, 객체가 스스로 판단하고 행동하는 자율적인 존재
객체지향 언어들은 접근 제어(access control) 메커니즘도 제공한다. 접근 제어를 위해 public, protected, private 같은 접근 수정자(accesee modifier)를 제공한다. 접근을 통제하는 이유는 자율적인 존재로 만들기 위해서다.
캡슐화와 접근 제어는 객체의 두 부분으로 나뉜다. 외부에서 접근 가능한 부분을 퍼블릭 인터페이스(public interface)라 부르고, 외부에서는 접근이 불가능하며 내부에서만 접근이 가능한 것을 구현(implementation)이라고 부른다. 이는 인터페이스와 구현의 분리(separation of interface and implementation)로 설계의 핵심 원칙이다.
프로그래머의 자유
프로그래머의 역할을 클래스 작성자(class creator)와 클라이언트 프로그래머(client programmer)로 구분하는 게 좋다. 작성자는 데이터 타입을 추가하고 클라이언트는 데이터 타입을 사용한다.
작성자는 구현 은닉(implementation hiding)을 이용해 데이터 타입이 클라이언트에게 영향을 준다는 걱정을 안 하고 자유롭게 작업할 수 있다. 클라이언트는 데이터 타입의 내부 구현을 무시하고 인터페이스로 사용만 한다.
협력하는 객체들의 공동체
협력(Collaboration)이란 기능을 구현하기 위해 객체들이 상호작용하는 것을 뜻한다.
객체는 다른 객체의 공개된 인터페이스를 통해 요청(request)할 수 있고, 요청을 받는 객체는 자율적으로 처리 후 응답(response)한다.
이처럼 상호작용하는 유일한 방법은 메시지를 전송(send a message)하는 것이고, 다른 객체에게 요청이 도착하면 메시지를 수신(receive a message)했다고 한다. 메시지를 수신한 객체는 스스로 처리할 방법을 정하는데 이를 메서드(method)라고 부른다.
메시지와 메서드를 구분하는 게 중요하다. 여기서 다형성(polymorphism)의 개념이 출발한다
할인 요금을 구해보자
할인 정책과 할인 조건은 세부 사항으로 나눠진다. 추상 클래스(abstract class)로 구현해보자 아래 코드는 할인 정책이다.
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);
}
코드를 보면 추상 메서드 getDiscountAmount에게 계산을 위임한다. 이처럼 추상 클래스로 기본적인 알고리즘 흐름을 구현하고 필요한 처리를 상속하는 자식 클래스에게 위임하는 디자인 패턴을 템플릿 메서드(TEMPLATE METHOD) 패턴이라고 부른다
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 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 {
...
}
컴파일 시간 의존성과 실행 시간 의존성
확장 가능한 객체지향 설계의 특징은 코드의 의존성과 실행 시점의 의존성이 다르다.
코드의 의존성과 실행 시점의 의존성이 다를수록 코드는 더 유연하고 확장이 가능해진다. 대신 코드를 이해하기 어려워지는 문제가 생긴다. 이런 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 보여준다.
* 상황에 따라 유연성과 가독성 사이에서 고민해야 한다.
상속과 인터페이스
상속은 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문에 외부 객체는 자식을 부모와 동일한 타입으로 간주할 수 있다.
다형성
메시지와 메서드는 다른 개념이라고 언급했었다.
코드상에서 Movie는 DiscountPolicy에게 메시지를 전송하지만 실행 시점에 실제로 실행되는 메서드는 수신하는 객체의 클래스에 따라 달라진다. 인터페이스가 동일할 때 수신하는 메시지에 따라 다르게 응답할 수 있는 능력을 다형성이라 한다.
다형성은 컴파일 시점이 아닌 실행 시점에 결정한다. 이를 지연 바인딩(laze binding) 혹은 동적 바인딩(dynamic binding)이라 하며, 컴파일 시점에 함수나 프로시저를 결정하는 것을 초기 바인딩(early binding) 또는 정적 바인딩(static binding)이라 한다.
추상화와 유연성
추상화의 장점은 2가지가 있다.
첫째, 추상화의 계층만 따로 보면 요구사항의 정책을 높은 수준으로 끌어올릴 수 있다.
둘째, 설계가 유연해진다.
추상화를 사용하면 설계가 구체적인 상황에 결합하는 것을 방지하게 해 주고, 기본적인 애플리케이션의 협력 흐름을 기술하게 해 준다.
재사용 가능한 설계의 기본을 이루는 디자인 패턴(design patter)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하기에 매우 중요한 개념이다.
합성
합성(composition)은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다. 합성은 일반적으로 상속보다 좋은 방법이라 많이들 말한다.
public class Movie {
// 다른 객체를 인스턴스 변수로 할당.
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(Money fee, DiscountPolicy discountPolicy) {
this.fee = fee;
this.discountPolicy = discountPolicy;
}
...
}
상속의 문제점은 2가지로 분류된다.
첫째, 캡슐화를 위반
둘째, 설계가 유연하지 못하다.
자식이 부모의 세부사항을 알아야 해서 캡슐화를 위반한다고 하는 것이고, 상속을 거듭하여 계층이 쌓일수록 부모를 수정할 수 없게 된다. 또한 컴파일 시점에 의존성이 결정된다.
합성은 인터페이스의 메시지를 통해서만 재사용되기에 캡슐화에 문제가 없고 실행 시점에 객체를 선택할 수 있어서 설계에 유연성이 생긴다. 상속의 문제점 2가지를 모두 해결하는 셈이다.
하지만 다형성을 위해 인터페이스를 재사용하기 위해서는 상속과 합성을 함께 사용할 수밖에 없다.
'서적 > 오브젝트: 코드로 이해하는 객체지향 설계' 카테고리의 다른 글
오브젝트 05_ 책임 할당하기 (0) | 2021.06.09 |
---|---|
오브젝트 04_ 설계 품질과 트레이드 오프 (0) | 2021.06.08 |
오브젝트 03_ 역할, 책임, 협력 (0) | 2021.06.08 |
오브젝트 01_ 객체, 설계 (0) | 2021.06.07 |
오브젝트 포스팅을 시작하며 (0) | 2021.06.04 |