#02장 객체지향 프로그래밍
조영호 저 "오브젝트: 코드로 이해하는 객체지향 설계, 2019"를 읽고 정리한 내용입니다.
1 영화 예매 시스템 만들기
온라인 영화 예매 시스템 시스템을 만들기 위해 알아야 되는 성격들을 알아보자.
1. "영화"와 "상영" 용어 구분 필요
영화: 영화가 가지고 있는 기본적인 정보
예, 제목, 상영시간, 가격 정보 등
상영: 관람하는 사건을 표현
예, 상영 일자, 시간, 순번 등
2. 할인액을 결정하는 두 가지 규칙
할인 조건(discount condition): 가격의 할인 여부를 결정
순서 조건: 상영 순번을 이용해 할인 여부를 결정하는 규칙 (sequence condition)
예, 10번째 상영 영화를 예매한 사용자들에게 할인 혜택을 제공
기간 조건: 상영 시작 시간을 이용해 할인 여부를 결정 (period condition)
예,월요일 시작 시간이 오전 10시, 종료 시간이 오후 1시인 기간 조건을 사용하면,
매주 월요일 오전 10시부터 오후 1시 사이에 상영되는 모든 영화에 대해 할인 혜택 적용
할인 정책(discount policy): 할인 요금을 결정
금액 할인 정책: 예매 요금에서 일정 금액을 할인해주는 방식 (amount discount policy)
비율 할인 정책: 정가에서 일정 비율을 할인해 주는 방식 (percent discount policy)
추가적 풀이
영화별로 하나의 할인 정책만 할당 가능하며, 할인 정책을 지정하지 않는 것도 가능하다.
할인 조건은 다수의 할인 조건을 함께 지정 가능하며, 순서 조건과 기간 조건을 섞는 것도 가능하다.

아바타를 예매

2 객체지향 프로그래밍을 향해
C++, 자바, C#과 같이 클래스 기반의 객체지향 언어에 익숙한 사람이라면, 가장 먼저 어떤 클래스가 필요한지 고민하고 어떤 속성과 메서드가 필요한지 고민한다. 안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라. (클래스 != 객체)
클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
따라서 클래스의 윤곽을 잡기 위해서는, 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
객체를 협력하는 공동체의 일원보자.
특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라.
도메인의 구조를 따르는 프로그램
객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다. 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에, 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다. (도메인 - 사용자가 프로그램을 사용하는 분야)
다음 타입들의 구조를 보고 관계를 유추해보자.

클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다

Screening
주목할 점
인스턴스 변수의 가시성은 private이고, 메서드의 가시성은 public이라는 것이다.
다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계 를 구분 짓는 것이다.
클래스는 내부와 외부로 구분되며, 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다.
직접 접근할 수 없도록 막고, 적절한 Public 메서드를 통해서만 내부 상태를 변경 할 수 있게 하였다.
자율적인 객체
객체가 상태(state)와 행동(behavior)을 함 께 가지는 복합적인 존재이다.
객체지향 이전에 데이터와 기능이라는 독립적인 존재를 서로 엮어 프로그램을 구성했다.
객체지향에서는 데이터와 기능을 묶은 캡슐화를 사용한다.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. 하나는 외부에서 접근 가능한 부분으로 이를 퍼블릭 인터페이스(public interface)라고 부른다. 다른 하나는 외부에서는 접근 불가능하고 오직 내부에서 만 접근 가능한 부분으로 이를 구현(implementation)이라고 부른다.
객체의 상태는 숨기고 행동만 외부에 공개해야 한다.
프로그래머의 자유: 접근 제어 메커니즘으로 얻을 수 있는 효과
역할 두가지
클래스 작성자 - 새로운 데이터 타입을 프로그램에 추가
클라이언트 프로그래머 - 추가한 데이터 타입을 사용
구현 은닉 (implementation hiding)
클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지
클라 이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경
클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.
접근 제어 - 객체의 변 경을 관리할 수 있는 기법 중에서 가장 대표적인 것
Reservation
Reservation의 reserve 메소드는 private 메서드(calculateFee)를 호출하였다.

뒤에서 살펴보겠지만, 메시지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발한다.
Movie
예매 요금을 계산하기 위해서는 현재 영화에 적용돼 있는 할인 정책의 종류를 판단할 수 있어야 한다. 하지만 코드 어디에도 할인 정책을 판단하는 코드는 존재하지 않는다. 단지 discountPolicy에게 메시지를 전송할 뿐이다. 이것이 어색하다면, 객체지향 패러다임에 익숙하지 않은 것이다. 이코드 속에는 상속(inheritance)과 다형성 그리고 그 기반에는 추상화(abstraction)라는 원리가 숨겨져 있다.
Discountpolicy - AmountDiscountPolicy와 PercentDiscountPolicy (부모-자식)
이 두개를 부모클래스 Discountpolicy는 인스턴스를 생성할 필요가 없기 때문에 추상 클래스(abstract)로 구현했다.
할인 조건을 만족하는 Discountcondition이 하나라도 존재하는 경우에는 추상 메서드(abstract method)인 getDiscountAmount 메서드를 호출해 할인 요금을 계산한다.
Discountpolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만 실제로 요금을 계산하는 부분은 추상 메서드인 getDiscountAmount 메서드에게 위임한다.
부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.
영화 가격 계산에 참여하는 모든 클래스 사이의 관계

상속과 다형성
Movie가 DiscountPolicy를 인스턴스로 가졌지만, 그안에 있는 AmountDiscountPolicy와 PercentDiscountPolicy가 존재여부는 모른다.
확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다. 이것이 다르면 다를 수록 코드가 어려워진다는 트레이드오프가 있다. 설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다.
Movie 클래스에서 DiscountPolicy 클래스로의 의존성이 어떻게 실행 시점에는 AmountDiscountPolicy나 PercentDiscountPolicy 인스턴스에 대한 의존성으로 바뀔 수 있을까?
상속 개념을 짚어보자.
상속은 클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사할 경우 재사용을 가능하게 해준다. 이로써 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다. 또한, 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다. (행동이 다르다=오버라이딩해 수정할 경우). 용어적으로, 차이에 의한 프로그래밍 (programming by difference)이라 한다.
C++의 창시자인 비야네 스트롭스트룹(Bjarne Stroustrup)은 슈퍼클래스와 서브 클래스라는 용어가 뜻이 모호하고 오해의 소지가 있다고 보고 C++에는 기반 클래스(base class)와 파생 클래스(derived class)라는 새로운 용어를 도입하였다.
대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각하지만, 무엇보다도 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다. 결과적으로, 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
Movie는 협력 객체가 calculateDiscountAmount라는 메시지를 이해할 수만 있다면 그 객체가 어떤 클래스의 인스턴스인지는 상관없다. 따라서 calculateDiscountAmount 메시지를 수신할 수 있는 AmountDiscountPolicy와 PercentDiscountPolicy 모두 DiscountPolicy를 대신해서 Movie와 협력 할 수 있다. 부모 클래스의 인터페이스를 물려받았기 때문이다.
그리고 Movie의 생성자에서 자식 클래스가 부모 클래스를 대신하는데 이것을 업캐스팅(upcasting)이라고 부른다. 부모 클래스를 자식 클래스의 위에 위치시키기 때문에 업캐스팅 이름이 붙혀졌다.
다형성
강조하지만 메시지와 메서드는 다른 개념이다. Movie는 DiscountPolicy의 인스턴스에게calculateDiscountAmount 메시지를 전송한다.
Movie 클래스는 DiscountPolicy 클래스에게 메시지를 전송하지만, 실행 시점에 실제로 실행되는 메서드는 Movie와 협력하는 객체의 실제 클래스(예, PercentDiscountPolicy)가 무엇인지에 따라 달라진다. 다시 말해서 Movie 는 동일한 메시지를 전송하지만 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라고 부른다.
다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
지연 바인딩(= 동적 바인딩)이라 부른다. lazy binding, dynamic binding
컴파일 시점은 초기바인딩(=정적바인딩) early binding, static binding
상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다. 이런 이유로 대부분의 사람들은 다형성을 이야기할 때 상속을 함께 언급한다. 그러나 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다. 책을 읽고 나면 다형성이란 추상적인 개념이며 이를 구현할 수 있는 방법이 꽤나 다양하다는 사실을 알게 될 것이다.
상속 2가지
구현상속 (= 서브클래싱) implementation inheritance
순수하게 코드 재사용 목적
인터페이스상속 (= 서브타이핑) interface inheritance
다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것
인터페이스와 다형성
Discountpolicy를 추상 클래스로 구현함으로써 자식 클래스들이 인터페이스와 내부 구현을 함께 상속받도록 만들었다.
할인 조건은 구현을 공유할 필요가 없기 때문에, 자바의 인터페이스를 이용해 타입 계층을 구현했다.
Discountcondition 인터페이스를 실체화하고 있는 Sequencecondition과 Periodcondition은 동일한 인터페이스를 공유하며 다형적인 협력에 참여 할수 있다.
Sequencecondition과 Periodcondition은 isSatisfiedBy 메시지를 이해할 수 있기 때문에 클라이언트인 DiscountPolicy의 입장에서 이 둘은 DiscountCondition과 아무 차이가 없다. 이것도 업캐스팅 ..
추상 클래스와 인터페이스 트레이드오프
getDiscountAmount() 메서드가 어떤 값을 반 환하더라도 상관이 없다는 사실을 알 수 있다. 부모 클래스인 Discountpolicy에서 할인 조건이 없을 경 우에는 getDiscountAmount() 메서드를 호출하지 않기 때문이다. 이 문제를 해결하기 위해 DiscountPolicy를 인터페이스로 바꾸고 NoneDiscountPolicy가 DiscountPolicy 의 getDiscountAmount() 메서드가 아닌 calculateDiscountAmount() 오퍼레이션을 오버라이딩하도록 변경한다.
현실적으로 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과할 수 있다.
책에서는 인터페이스를 사용하지 않는 원래의 설계에 기 반해서 설명을 이어갈 것이다.
구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다는 사실이다.
코드 재사용
합성: 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법.
기존 코드인 Movie가 DiscountPolicy의 코드를 재사용하는 방법이 바로 합성이다.
상속으로 구현한다면?

상속 대신 합성을 선호하는 이유는 무엇일까?
상속은 캡슐화를 위반한다.
부모 클래스의 내부 구조를 잘 알고 있어야 하기에
결과적으로 부모 클래스의 구현이 자식 클래스에게 노출된다.
다른 하나는 설계를 유연하지 못하게 만든다.
부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
상속은 클래스를 통해 강하게 결합되는데 비해 합성은 메시지를 통해 느슨하게 결합된다.
코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.
Last updated