오브젝트[02 객체지향 프로그래밍]
https://product.kyobobook.co.kr/detail/S000001766367
오브젝트 | 조영호 - 교보문고
오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음
product.kyobobook.co.kr
책 목차:
1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :
이번 챕터는 이 책을 읽으면서 보게 될 다양한 주제를 얃은 수준으로 훑어보는 것이다. 따라서 혹여 현실적이지 않더라도 간단한 예제를 기반으로 진행할 것이다. 이제 가벼운 마음으로 2챕터를 읽어보자.
1. 영화 예매 시스템
이번 채챕터는 영화 예매 시스템으로 예시로 설명 할 것이다.
요구사항 살펴보기
영화 예매 시스템은 크게 4가지 요소가 있다.
- 영화 정보
- 상영 정보
- 할인 조건
- 할인 정책
특정 영화는 여러번 상영될 수 있으며, 이때 다양한 할인 조건(특정 시간대에 시작하는 영화, 또는 상영 번호[예컨데 조조할인])에 따라서 할인을 하게 되며, 이때 할인되는 금액은 할인 정책(10% 또는 1000원)에 따라 결정된다.
2. 객체지향 프로그래밍을 향해
협력, 객체, 클래스
객체지향은 클래스를 지향하는 것이 아니라 객체를 지향하는 것이다. 이를 위해서 프로그래밍 하는동안 2가지에 집중해야 한다.
- 필요한 클래스가 아닌 필요한 객체를 고민하라.
클래스는 상태와 행동을 공유하는 객체를 추상화 한 것이다. 따라서 객체의 상태와 행동을 먼저 결정하고, 클래스의 윤곽을 잡자.
- 객체를 협력하는 공동체의 일원으로 봐라.
객체를 협력하는 공동체의 일원으로 바라보면 설계를 유연하고 확장 가능하게 만든다.
도메인의 구조를 따르는 프로그램 구조
도메인(Domain): 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
객체 지향의 장점은 요구사항 분석부터 마지막 구현까지 객체라는 동일한 추상화 기법을 사용할 수 있다는 것이다.
현실 세계에서 영화 예메 도메인의 개념과 관계를 나타내 보면 아래와 같다.
이제 이 도메인을 그대로 객체로 가져오면 아래와 같다.
이렇듯 도메인 개념의 구조를 따라서 클래스 구조를 만들면 프로그램의 구조를 이해하고 예상하기 쉽게 만들 수 있다.
클래스 구현하기
클래스를 구현할 때 private과 public 접근 제어자를 사용해서 클래스의 내부와 외부를 구분해야한다. 그 이유를 저자는 2가지로 설명한다.
자율적인 객체
객체는 상태와 행동을 함께 가지는 복합적인 존재이다. 이렇듯 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 부른다.
또한 객체는 자율적인 존재이다. 객체지향 언어들은 캡슐화에서 한걸음 더 나아가 접근 제어 메커니즘을 제공한다. 이를 통해서 객체는 외부의 간섭을 최소화할 수 있다. 외부에서는 객체가 어떤 상태인지 알 수 없으며, 직접 개인하려고 해도 안된다. 단지 요청할 뿐이다.
캡슐화의 접근 제어는 객체를 퍼블릭 인터페이스와 구현으로 나눈다. 일반적으로 객체는 상태는 숨기고 행동만 외부에 공개해야한다. 이를 통해서 퍼블릭 인터페이스와 구현을 철저히 구분하는 것 이다.
프로그래머의 자유
프로그래머의 역할은 클래스 작성자와 클라이언트 프로그래머로 구분해보자. 클래스 작성자는 클라이언트 프로그래머가 필요한 인터페이스만 공개하고, 내부 구현은 숨겨 둔다. 이를 구현 은닉(implementation hiding)이라고 부른다.
구현 은닉을 통해서 클라이언트 프로그래머는 내부 구현을 모르고 인터페이스만 알아도 클래스를 사용할 수 있다. 또한 클래스 프로그래머는 인터페이스를 바꾸지 않는 한 외부에 미치는 영향을 걱정하지 않고 내부 구현을 변경할 수 있다.
설계가 필요한 이유는 변경을 관리하기 위해서 이다. 객체의 변경을 관리할 수 있는 기법중에 가장 대표적인 것이 바로 접근 제어다.
협력하는 객체들의 공동체
객체들은 서로에게 Message를 보내서 원하는 정보를 얻어오거나, 서로에게 행동을 요청한다. 여기서 주목할 만한 점은 바로 Money타입인데, 금액정도는 그냥 long 타입으로 지정할 수 도 있을 것을 Money라는 클래스로 만들었다. 저자는 이렇듯 의미를 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현하라고 조언한다. Money클래스를 구현 하므로서 값이 금액과 관련되어 있다는 사실을 명시적으로 클라이언트 프로그래머에게 알릴 수 있었고, 금액과 관련된 로직이 서로 다른 곳에서 중복되어 구현되는 것을 막을 수 있었다. 이렇듯 개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높히는 첫걸음이 된다.
시스템이 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration)이라고 부른다. 앞서 구현한 코드는 수많은 객체들이 서로 메소드를 호출하며 협력했다.
협력에 관한 짧은 이야기
객체가 서로 상호작용 할 수 있는 방법은 메시지를 전송/수신하는 것 뿐이다. 이는 메시지를 처리하는 메소드(Method)와는 다르다. 이 둘은 구분하는 것은 객체지향 패러다임을 유연하고, 확장 가능하며, 재사용 가능할 설계로 만드는데 아주 중요한데, 다형성(polymorphism)의 개념이 메시지와 메소드의 구분에서 출발한다.
우리가 메소드를 호출한다고 믿었던 것은 단지 메시지를 전송하는 것이다. 왜냐면 클라이언트 객체는 메시지를 전송하면서 내부에 어떤 메소드가 있는지 알지 못하기 때문이다. 루비와 같은 언더들은 calculateMovieFee라는 메시지를 다른 메소드가 처리하게 할 수도 있다. 즉, 메시지를 처리하는 방법(Method)를 결정하는 것은 오롯이 객체의 몫인 것이다.
3. 할인 요금 구하기
할인 요금 계산을 위한 협력 시작하기
이번에는 상속과 다형성을 이용해서 discountPolicy를 추상화 해 보자.
할인 정책과 할인 조건
TEMPLATE METHOD 패턴(GOF94) : 부모 클래스에 기본적인 알고리즘을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴
DiscountPolicy를 추상 클래스로 만들고, 정책의 종류에 따라 달라지는 로직을 추상 메서드로 만드므로서, Template Method패턴을 구현했다. DiscountCondiction은 인터페이스로 만들었다. 이렇게 구현을 하므로서 객체는 구현이 아니라 설계에 의존할 수 있게 되었다.
할인 정책 구성하기
하나의 영화에 단 하나의 할인 정책만 설정 할 수 있다는 사실을 생성자에 명시해서 강제했다.반면 DiscountPolicy는 여러 DiscountCondition을 받을 수 있는 생성자를 만들었따.이처럼 생성자의 파라미터를 통해 초기화에 필요한 정보를 전달하도록 강제하면 올바른 상태를 가진 객체의 생성을 보장할 수 있다.
4. 상속과 다형성
Movie Class를 보면 할인 정책과 할인 조건에 대해서 아무런 정보도 가지고 있지않아. 그런데 Movie Class는 어떻게 할인에 대한 결정을 내릴 수 있었을까? 이를 의존성과 상속과 다형성을 이용한 선택적 조건 실행하는 방법으로 나누어서 알아본다.
컴파일 시간 의존성과 실행 시간 의존성
의존성이 있다 : 어떤 클래스가 다른 클래스에 접근 할 수 있는 경로를 가지고 있거나, 해당 클래스의 객체의 메서드를 호출할 경우
우리는 앞서 작성한 코드에서 Movie가 DiscountPolicy에만 의존하고 있는 것을 알고 있다. 이건 코드의 의존성을 의미한다. 하지만 실행시간에선 어떨까? Movie 클래스는 우리가 생성자에서 넣어준 구체 클래스를 의존하게 된다.
여기서 중요한 사실은 코드의 의존성과 실생 서짐의 의존성이 서로 다를 수 있다는 것이다. 그리고 코드의 의존성과 실행 시점의 의존성이 달라서 유연하고, 확장 가능하며, 재사용할 수 있는 객체지향 설계를 할 수 있는 것이다.
하지만 이런식의 코드는 가독성이 떨어지는 측면도 분명히 존재한다. 다시 말해서 이런 코드가 언제나 능사는 아닌 것이다. 우리는 훌륭한 객체지향 설계를 하기 위해서 유연성과 가독성 사이에서 고민해야 하는 것이다.
차이에 의한 프로그래밍
상속은 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속은 슈퍼클래스의 속성과 메서드를 받을 수 있으며, 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있는 간편한 방법을 제공한다. 또한 부모크래스의 구현은 공유하면서도, 행동이 다른 자식클래스를 추가할 수 있다. 예컨데, DiscountPolicy에서 getDiscountAmount를 변경해서 Tamplate Method패턴을 구현한 것 처럼 말이다.
이처럼 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference) 이라고 한다.
상속과 인터페이스
상속이 가치있는 이유는 부모클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 즉, 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에, 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다.
다형성
Movie는 DiscountPolicy Type의 객체에게 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라 부른다.
다형성은 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력이다. 이때 다형적인 협력에 참여하는 객체는 모두 같은 메시지를 이해할 수 있어야 하고, 결국 같은 인터페이스를 가져야 한다. 다형성을 구현하기 위해서 기본적으로 메시지와 메서드를 실행 시점에 바인딩 해야하는데, 이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라고 한다.
상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다. 이런 이유로 다형성을 이야기할 때 상속이 빠지지 않는다. 하지만, 다형성을 구현하는 방법은 이보다 많고 앞으로 설명할 것이다.
인터페이스와 다형성
인터페이스는 구현은 고유할 필요가 없고 외부 인터페이스만 구현하고 싶을 때 사용한다.
5. 추상화와 유연성
추상화의 힘
지금까지의 예시에서 DiscountPolicy와 DiscountCondition은 자식 클래스보다 추상적이다. 이는 구현의 일부 또는 전체를 자식 클래스가 결정 할 수 있도록 결정권을 위임한다.
이는 2가지 장점을 가져다 준다.
추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다는 것
추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 예컨데 "영화 예매 요금은 최대 하나의 할인 정책과 다수의 할인 조건을 이용해 계산할 수 있다."로 표현하게 되는 것이다.
추상화를 이용해상위 정책을 기술한다는 것은 애플리케이션의 협력 흐름을 기술하다는 것이다. 이는 아주 중요한데, 디자인 패턴이나 프레임워크 모두 추상화를 이용해서 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문이다.
추상화를 이용하면 설계가 좀 더 유연해 진다는 것
추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
유연한 설계
여기서 DiscountPolicy에 할인하지 않는 정책을 추가하려면 어떻게 해야할까? 이때 Movie Class에서 DiscountPolicy가 null이면 할인되지 않은 금액을 반환 하는 것은 좋지 않다. 이는 할인 금액이 0원 이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie쪽에 있기 때문이다. 책임의 위치를 결정하지 위해 조건문을 사용하는 것은 일반적으로 나쁜 설계이다.
우린 기존의 Movie와 DiscountPolicy를 수정하지 않고 NoneDiscountPolicy라는 새로운 클래스를 추가해서 기능을 확장했다. 이로서 추상화를 중심으로 코드를 유연하고 확장 가능하도록 했다.
추상 클래스와 인터페이스 트레이드오프
추상 클래스 DiscountPolicy를 상속받는 NoneDiscountPolicy는 사실 getDiscountAmount()라는 메소드가 필요하지 않다. 이는 템플릿 메소드 패턴을 위해서 존재하는 것인데, NoneDiscountPolicy는 그냥 calculateDiscountAmount()를 호출할 때 Money.ZERO를 반환하면 되기 때문이다. 이를 위해서 DiscountPolicy를 interface로 선언하고, 기존 DiscountPolicy의 이름을 DefaultDiscountPolicy로 변경했다. 이는 NoneDiscountPolicy가 필요 이상의 인터페이스를 가지지 않아서 좋아보이지만, 설계가 복잡하고 과해 보이기도 한다.
저자는 이를 통해 구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다는 사실을 보여준다. 더불어 모든 코드에는 합당한 이유가 있어야 한다고 첨언한다.
코드 재사용
코드를 재사용 하는 방법은 상속과 합성이 있다. 여기서 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용 하는 방법이다. 지금 Movie가 DiscountPolicy를 이용하는 방식이 바로 합성이다. 그럼데, 아래와 같이 Movie를 직접 상속 받아서 DiscountPolicy를 구현하면 안되는 걸까?
상속
상속을 코드 재사용을 위해 사용한다면 2가지 관점에서 설계에 안 좋은 영향을 미친다. 하나는 캡슐화를 위반한다는 것이고, 하나는 설계를 유연하지 못하게 만든다는 것이다.
상속을 이용하려면 부모클래스의 내부를 알아야한다. 이는 결합도를 높히고, 결과적으로 상속을 과하게 사용한 코드는 변경에 취약해진다. 상속은 설계가 유연하지 않다. 상속은 부모와 자식 클래스의 관계를 컴파일 시점에 결정한다.
만약 Movie를 상속받은 PercentDiscountMovie를 AmountDiscountMovie 객체로 변경해보자. 그럼 AmountDiscountMovie를 생성하고, PercentDiscountMovie 상태를 복사해야 할 것이다.
이와같이 경직된 상속 보다는 Movie객체가 DiscountMovie를 가지는 형태로 코드를 재사용할 수 있다. 이를 합성이라고 부른다.
합성
Movie는 DiscountPolicy의 코드를 재사용한다. 이는 상속과 다르게 인터페이스를 통해 약하게 결합한다. 이렇듯 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다. 합성은 상속의 2가지 문제를 모두 해결한다.
1. 인터페이스에 정의된 메시지를 통해서 재사용이 가능하기 때문에 구현을 캡슐화 한다.
2. 의존하는 인스턴스를 교체하는 것이 쉽기 때문에, 설계가 유연해진다.
그렇다고 해서 상속을 절대 금지하자는 것이 아니다. 코드를 재사용하는 경우에는 합성을, 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 조합해서사용하는 것이 당연하다.