[책 스터디][오브젝트] 2장. 객체지향 프로그래밍
본 글을 오브젝트 책을 읽고, 그 내용을 정리한 글입니다.
본 글에 언급되는 실습 코드는 실습 링크를 참고해 주시기 바랍니다.
영화 예매 시스템
영화와 상영의 차이
객체지향 프로그래밍의 강력함을 보여주기 위한 예시로 “영화 예매 시스템”을 제시하고 있다. 먼저, “영화”와 “상영” 개념의 차이를 짚고 넘어간다.
- 영화
- 영화에 대한 기본 정보 (제목, 상영시간, 가격 등..)
- 상영
- 실제로 관객들이 영화를 관람하는 사건
- 한 영화는 하루에도 여러 번 “상영”될 수 있다.
실제로 관객이 예매하는 대상은 영화가 아닌 상영임을 기억하자.
할인 조건과 할인 정책
영화 예매 시스템은 할인에 대한 두 가지 요구사항을 제시한다.
- 할인 조건
- 가격의 할인 여부를 결정하는 개념이다. 두 가지 할인 조건이 존재한다.
- 순서 조건
- 상영 순번을 이용해 할인 여부를 결정하는 할인 조건이다. ex) 매일 10번째로 상영되는 영화를 예매한 사용자들에게 할인 혜택을 제공한다.
- 기간 조건
- 영화 상영 시작 시간을 이용해 할인 여부를 결정하는 할인 조건이다.
- 요일, 시작 시간, 종료 시간 세 가지 요소가 제시된다. ex) 월요일 오전 10시부터 오후 1시 사이에 상영되는 모든 영화에 대해 할인 혜택이 제공된다.
- 할인 정책
- 할인 요금을 결정하는 개념이다. 두 가지 할인 정책이 존재한다.
- 금액 할인 정책
- 예매 금액에서 일정 금액을 할인해주는 정책이다. ex) 예매 금액이 9000원이고 금액 할인 정책이 800원이라면, 할인된 금액은 8200원이다.
- 비율 할인 정책
- 예매 금액의 일정 비율의 요금을 할인해주는 정책이다. ex) 예매 금액이 9000원이고 비율 할인 정책이 10%라면, 할인된 금액은 8100원이다.
- 금액 할인 정책
- 할인 요금을 결정하는 개념이다. 두 가지 할인 정책이 존재한다.
영화별로 할인 정책은 최대 하나만 적용 가능하다.
영화별로 여러 개의 할인 조건을 가질 수 있다.
영화 예매 시스템의 요구사항을 살펴봤으니, 이를 객체지향 프로그래밍 언어를 이용해 구현해보자.
객체지향 프로그래밍을 향해
협력, 객체, 클래스
본격적으로 객체지향 프로그래밍을 해보기 전, 책에서 주의사항을 안내하고 있다.
- 객체지향은 말 그대로 “객체”에 집중해야 한다.
흔히 객체지향 프로그래밍을 하면서 클래스에 집중하는 실수를 범한다.
클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다. 즉, 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야, 이를 토대로 올바른 클래스를 만들 수 있다. - 어떤 기능을 수행하기 위해선 객체 간 협력이 반드시 필요하다.
객체는 고립된 존재가 아닌, 공동체의 일원으로서 서로 협력하는 존재이다.
객체를 협력자로 바라보면, 하나의 객체가 맡는 책임이 줄어들고, 이는 곧 유연하고 확장 가능한 설계로 이어진다.
도메인의 구조를 따르는 프로그램 구조
도메인(domain)이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 일컫는 말이다.
객체지향 프로그래밍의 강력함은 요구사항을 분석하는 초기 단계부터, 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있는 데에서 나온다.
요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에, 도메인을 구성하는 개념들이 프로그램 객체와 클래스로 매끄럽게 연결될 수 있다.
클래스 구현하기
실습 링크 - movie-reservation/without-none-discount-policy branch
인터페이스, 구현 분리
객체의 내부와 외부를 명확히 구분할수록 객체의 자율성이 높아지고, 프로그래머의 구현 자유도가 높아진다.
캡슐화와 접근 제어를 통해 객체의 내부, 외부를 명확히 구분할 수 있다.
- 캡슐화 (encapsulation)
- 데이터와 기능(프로세스)을 객체 내부로 함께 묶는 것 데이터 = 객체의 상태(status) 기능 = 객체의 행동(behavior)
- 접근 제어 (access control)
- 외부에서의 접근을 통제하는 기능
- 접근 수정자(access modifier) **를 통해 실현할 수 있다.
캡슐화와 접근 제어를 통해 객체는 두 부분으로 나뉜다.
- 인터페이스
- 외부에서 접근 가능한 부분
- 구현
- 외부에서 접근 불가능한 부분
이처럼 객체의 내부와 외부를 분리하면 다음과 같은 이점이 있다.
- 외부 객체는 이 객체의 내부 구현을 몰라도 된다. 단지 메세지를 전달하고, 원하는 응답을 받으면 된다.
- 외부 객체에서 실수로 이 객체의 데이터를 조정하는 일을 방지할 수 있다.
- 이 객체의 내부 구현을 변경할 일이 있어도, 인터페이스만 유지한다면 그것이 외부 객체에 영향을 주지 않는다.
객체의 협력이란
절차지향에서는 함수를 “호출” 한다고 표현하지만, 객체지향에서는 “메세지를 전달” 한다고 표현한다.
객체가 다른 객체와 상호작용하기 위해 메세지를 전송하면, 그 메세지를 수신한 객체는 메세지를 처리할 방법을 자율적으로 결정한다. 객체가 메세지를 처리하는 자신만의 방법을 메서드라고 한다.
메세지와 메서드는 분명히 다른 개념이다.
메세지는 처리가 요구되는 작업 자체를 의미하고, 메서드는 메세지의 처리 방법을 의미한다.
메세지와 메서드의 구분에서 다형성 (polymorphism) 의 개념이 출발한다.
자바와 같은 정적 타입 언어는 컴파일 시 메서드가 고정되므로, 객체가 메세지 처리 방법을 자율적으로 결정한다는 말이 어색하게 보일 수 있다.
하지만 Ruby나 Smalltak와 같은 동적 타입 언어에서는 런타임 시 다양한 메서드로 메세지를 처리할 수 있다는 점을 알고 넘어가자.
상속과 다형성
Movie 클래스를 만들 때, 생성자로 DiscountPolicy 타입의 매개변수를 받았다. DiscountPolicy는 추상클래스일 뿐 구현체가 아니다. 그렇다면 프로그램 실행 시 Movie 객체의 할인 정책은 어떻게 결정되는 걸까? 여기서 객체지향의 특징인 상속과 다형성의 개념이 활용된다.
컴파일 시간 의존성과 실행 시간 의존성
어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 갖거나, 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.
코드 수준에서 보면, Movie 클래스는 DiscountPolicy에만 의존하고 있다.
그러나 런타임 시점에서 보면, Movie 객체가 생성될 때 생성자에 AmountDiscountPolicy나 PercentDiscoutPolicy 객체를 전달하고 있다.
이렇게 코드 수준에서의 의존성과 런타임 수준에서의 의존성이 서로 다를 수 있다.
(= 클래스 의존성과 객체 의존성이 다를 수 있다.)
상속을 통해 자식 클래스는 부모 클래스가 갖고 있는 모든 인터페이스를 소유한다. 즉, 자식 클래스는 부모 클래스를 대신할 수 있다.
Movie 객체는 DiscountPolicy 객체에게 “할인할 금액을 알려줘 (calcDiscountAmount)” 라는 메세지를 보낼 뿐, 런타임 시 어떤 객체가 이 메세지를 처리하는지는 관심이 없다. 그저 그 메세지를 정상적으로 처리하고, 올바른 응답을 돌려주면 된다.
영화별 할인 정책에 따라 금액 할인 정책을 사용하는 영화는 AmountDiscountPolicy 객체가 메세지를 처리할 것이고, 비율 할인 정책을 사용하는 영화는 PercentDiscountPolicy 객체가 메세지를 처리할 것이다.
이렇듯 Movie 객체는 동일한 메세지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메세지를 수신하는 객체의 클래스가 무엇인지에 따라 달라진다. 즉, 동일한 메세지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 다형성이라고 한다.
다형성은 메세지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 런타임 시점에 결정한다. 이를 지연 바인딩 (lazy binding) 혹은 동적 바인딩 (dynamic binding) 이라 한다.
반대로 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 (early binding) 혹은 정적 바인딩 (static binding) 이라 한다.
추상화와 유연성
추상화를 사용하면 두 가지 이점을 얻을 수 있다.
- 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 유연한 설계가 가능하다.
NoneDiscountPolicy의 추가
실습 링크 - movie-reservation/just-add-none-discount-policy branch
영화 예매 시스템의 요구사항에는 “할인 정책을 적용하지 않을 수도 있다.” 라는 항목도 있었다. 이를 위해 DiscountPolicy를 상속한 NoneDiscountPolicy를 만들었다.
이 코드에는 한 가지 문제점이 있는데, NoneDiscountPolicy에서 오버라이딩한 getDiscountAmount에 메세지가 도달하지 못한다. 애초에 DiscountPolicy의 calcDiscountAmount에서 할인 조건이 없다면 할인금액을 0원으로 책정해 버렸기 때문이다.
DiscountPolicy가 NoneDiscountPolicy의 책임까지 도맡아 버린 상황이다. 이는 잘못된 설계이며, 개선된 설계안이 필요하다. 그 개선안은 아래와 같다.
코드 재사용 by 상속, 합성
- 상속
부모 클래스의 구현 코드를 재사용하기 위해 상속을 활용하는 것을 구현 상속 또는 서브클래싱이라 한다.
서브클래싱의 문제에는 두 가지가 있다.- 캡슐화 위반
- 자식 클래스가 부모 클래스의 내용을 자세히 알아야 된다.
- 이 때문에 자식 클래스가 부모 클래스에 강하게 결합되고, 부모 클래스의 변경이 자식 클래스에 영향을 미칠 확률이 높아진다.
- 유연하지 못한 설계
- 상속은 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다. 즉, 런타임 시점의 동적 바인딩이 불가능하다.
- Movie 클래스를 상속한 AmountDiscountMovie와 PercentDiscountMovie 클래스를 사용하는 설계에서는, 런타임 시점에서 영화의 금액 할인 정책과 비율 할인 정책을 자유롭게 변경하기 어렵다.
- 캡슐화 위반
- 합성
인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법을 말한다.
Movie 클래스는 DiscountPolicy 인터페이스를 멤버 변수로 갖는다. 런타임 시점에 DiscountPolicy에 어떤 인스턴스가 담겨 있던, 그 인스턴스가 calcDiscountAmount 메세지를 정상적으로 처리할 수 있다면 Movie는 아무런 상관이 없다. 합성은 서브클래싱의 단점 두 가지를 모두 보완할 수 있다.- 재사용이 필요한 코드를 독립적으로 분리하므로, 캡슐화를 위반하지 않는다.
- 의존 인스턴스를 비교적 쉽게 교체할 수 있으므로, 유연한 설계라고 볼 수 있다.
상속은 클래스를 통해 강하게 결합되지만, 합성은 메세지를 통해 느슨하게 결합된다.