1. 객체지향 프로그래밍을 향해
1.1. 협력, 객체, 클래스
•
클래스 기반의 객체지향 언어에 익숙한 사람이라면 가장 먼저 어떤 클래스가 필요한지 고민할 것이다.
◦
대부분의 사람들은 클래스를 결정 후 속성과 메서드를 고민한다.
◦
이는 객체지향의 본질과는 거리가 멀다.
◦
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
•
프로그래밍을 하는 동안 두 가지에 집중해야 한다.
◦
어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라
▪
클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
▪
클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
▪
객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
◦
객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
▪
훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.
1.2. 도메인의 구조를 따르는 프로그램 구조
•
소프트웨어는 어떤 문제를 해결하기 위해 만들어지고 사용자가 프로그램을 사용하는 분야를 도메인이라 한다.
•
객체지향 패러다임이 강력한 이유는 요구사항 분석부터 구현 단계까지 동일한 추상화 기법을 사용할 수 있기 때문이다.
[오브젝트] p41
•
위 그림은 영화 예매 도메인을 구성하는 개념과 관계를 포함한 것으로 영화는 여러 번 상영될 수 있고 상영은 여러 번 예매될 수 있다는 것을 알 수 있다.
[오브젝트] p42
•
클래스 기반의 객체지향 언어에 익숙하다면 도메인 개념들을 구현하기 위해 클래스를 사용한다는 사실은 낯설지는 않을 것이다.
•
앞서 설명한 도메인 원칙에 따라서 개념과 관계를 구조화해야 하기 때문에 위 그림은 도메인 구조와 유사한 형때를 띄어야 한다.
1.3. 클래스 구현하기
•
클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다.
•
외부에서는 직접적인 객체 속성 priavte로 접근을 막고 적절한 public 메서드를 통해 내부 상태를 변경할 수 있게 해야한다.
•
그렇다면 클래스의 내부와 외부를 구분해야 하는 이유는 무엇일까?
◦
그 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이다.
1.3.1. 자율적인 객체
•
객체에게는 중요한 2가지 사실이 있다.
1.
객체는 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재다
2.
객체는 스스로 판단하고, 행동하는 자율적인 존재다.
•
객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶으으로써 문제 영역의 아이디어를 적절하게 표현할 수 있다.
◦
데이터와 기능을 객체 내부로 함게 묶는 것을 캡슐화라 부른다.
•
객체 지향은 외부에서의 접근을 통제하는 접근제어를 통해 스스로 상태를 관리/판단/행동 하는 자율적인 공동체를 구성한다.
•
외부 객체에서는 객체가 어떤 상태에 놓여 있는지, 생각하는지 알아서는 안되며 결정에 직접적으로 개입하려 해서도 안된다.
•
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.
◦
퍼블릭 인터페이스 = 외부에서 접근 가능한 부분
◦
구현 = 외부에서 접근 불가능하고 내부에서만 접근 가능
•
일반적으로 객체 상태는 숨기고 행동만 외부에 공개해야 한다.
1.3.2. 프로그래머의 자유
•
클래스 작성자 : 새로운 데이터 타입을 프로그램에 추가하는 역할
•
클라이언트 프로그래머 : 클래스 작성자가 추가한 데이터 타입을 사용하는 역할
•
클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만을 공개하고 나머지는 숨긴다.
•
클래스 작성자 입장에서 클라이언트 프로그래머가 숨겨놓은 부분을 맘대로 접근할 수 없게 방지하여 외부 영향에 대한 걱정없이 맘대로 내부구조 변경이 가능
◦
이를 은닉 구현이라 한다.
•
은닉 구현은 public한 영역을 건들이지 않는다면 코드의 수정이 자유로워진다.
•
설계가 필요한 이유는 변경을 관리하기 위한것이란 걸 기억하자!!
1.3.3. 협력하는 객체들의 공동체
•
객체지향 프로그램을 작성할 때는 먼저 협력의 관점에서 어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위 구현을 위해 클래스를 작성한다.
1.3.4. 협력에 관한 짧은 이야기
•
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다.
•
요청을 받은 객체는 자율적인 방법에 따라 요청을 처리 한 후 응답(response)한다.
•
객체가 다른 객체와 상호작용 하는 유일한 방법은 메시지를 전송(send a message)하는 것 뿐이다.
•
메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
•
이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.
2. 할인 요금 구하기
2.1. 할인 정책과 할인 조건
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
public Monet calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screeing screening);
}
Java
복사
•
할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분된다. 두 가지 할인 정책을 각각 AmountDiscountPolicy와 PercentDiscountPolicy라는 클래스로 구현할 것이다.
•
여기서는 부모 부모 클래스인 DiscountPolicy 안에 중복 코드를 두고 AmountDiscountPolicy와 PercentDiscountPolicy가 이 클래스를 상속받게 할 것이다.
•
이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.
3. 상속과 다형성
3.1. 컴파일 시간 의존성과 실행 시간 의존성
[오브젝트] p57
•
그림에서 알 수 있듯 Movie는 DiscountPoliy와 연결돼 있으며, AmountDiscountPolicy와 PercentDiscountPolicy는 추상클래스를 상속받는다.
•
클래스 사이에 의존성이 존재하는 경우
◦
어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지는 경우
◦
해당 클래스의 객체의 메서드를 호출할 경우
•
눈여겨볼 부분은 Movie가 DiscountPoliy와 연결돼 있다는 것으로 영화 요금을 계산하기 위한 추상클래스가 아닌 구현체 인스턴스가 필요하다는 것이다.
•
Movie는 직접 구현체 클래스를 생성하지 않지만 외부에서 이를 주입받아 사용하고 있다.
◦
Movie 인스턴스 생성 시 외부에서 DiscountPoliy구현체를 전달받도록 한다.
•
이를 통해 컴파일 시점이 아닌 런타임 시점에서 실제 어떤 클래스를 의존할 것인지 결정할 수 있다.
◦
확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.
•
코드 레벨의 의존성과 런타임 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다.
◦
의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 보여준다.
•
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하라.
◦
유연성을 억제하는 코드는 이해와 디버깅이 쉬워지지만 재사용성과 확장 가능성은 낮아진다.
◦
무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다.
3.2. 차이에 의한 프로그래밍
•
클래스를 하나 추가하고 싶지만 기존의 어떤 클래스와 매우 흡사하다면?
•
해당 클래스의 코드를 가져와 약간만 추가하거나 수정해서 새로운 클래스를 만들면 더 좋을 것이다.
•
더 좋은 방법은 코드를 전혀 수정하지 않고도 재활용 하는것이고, 이를 가능하게 해주는 방법이 상속이다.
◦
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
◦
상속은 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있는 간편한 방법을 제공한다.
•
부모 클래스와 다른 부분만을 추가해 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.
3.3. 상속과 인터페이스
•
상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
•
인터페이스는 객체가 이애할 수 있는 메시지 목록을 정의한다는 것을 기억하자.
◦
상속을 하면 자식이 부모의 인터페이스를 포함하게 되므로 자식은 부모가 수신할 수 있는 모든 메세지를 수신받을 수 있다.
▪
때문에 외부에서는 동일한 타입으로 간주가능하다.
객체지향의 5원칙 중 하나인 리스코프 치환 법칙
•
외부 객체는 협력 객체가 어떤 인스턴스인지는 상관없이 메시지를 통해서 협력 가능하다면 상관하지 않는다.
•
자식 클래스가 부모 클래스를 대신하는 것을 업케스팅이라 부른다.
[오브젝트] p62
•
자식 클래스가 위에 위치한 부모 클래스로 자동적으로 타입 캐스팅되는 것처럼 보이기 때문에 업캐스팅 이라는 용어를 사용한다
3.4. 다형성
•
메세지 = 객체간 협력을 위해서 외부 객체에서 특정 인스턴스를 향해 전송하는 행위
•
메서드 = 전달받은 메서드를 처리하기 위해 실행되는 행위
◦
어떤 객체가 메세지를 전달 받느냐에 따라 다른 행위가 이뤄진다.
•
즉, 메세지와 메서드는 서로 다른 개념이다.
•
동일한 메세지를 전송하지만 실제로 어떤 메서드가 실행될 것인지 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는 것을 다형성이라 한다.
•
다형성은 컴파일 시간 의존성과 실행시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행할 수 있게 한다.
•
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 말한다.
◦
따라서 다형적인 렵력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야한다.
•
다형성을 구현하는 방법은 매우 다양하지만 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
◦
이를 지연 바인딩(lazy binding) 혹은 동적 바인딩(dynamic binding)이라고 부른다.
•
전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을
◦
초기 바인딩(early binding) 혹은 정적 바인딩(static binding)이라고 부른다.
•
상속을 통해 다양한 클래스를 하나의 타입 계층으로 묶을 수 있다.
3.5. 인터페이스와 다형성
•
종종 구현의 공유 없이 순수하게 인터페이스만 공유하고 싶을 때가 있다.
•
이를 위해 C#과 자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.
•
클라이언트 입장에서는 아무런 차이없이 기존과 동일하게 이용할 수 있다.
4. 추상화와 유연성
4.1. 추상화의 힘
•
추상화를 사용할 경우의 두 가지 장점
◦
추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
◦
추상화를 이용해 설계가 더 유연해진다는 것이다.
[오브젝트] p65
•
그림을 하나의 문장으로 정리하면 “영화 예매 요금은 최대 하나의 ‘할인 정책’과 다수의 ‘할인 조건’을 이용해 계산할 수 있다.”
◦
이 문장은 “영화의 예매 요금은 ‘금액 할인 정책’과 ‘두 개의 순서 조건, 한 개의 기간 조건’을 이용해 계산할 수 있다” 라는 문장을 포괄할 수 있다.
•
추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
◦
추상화를 이용한 설계는 필요에 따라 표현의 수준을 조정하는 것을 가능하게 해준다.
•
할인 정책이나 할인 조건의 새로운 자식 클래스들은 추상화를 이용해 정의한 상위 협력의 흐름을 그대로 따르게 된다.
◦
재사용 가능한 설계의 시본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향 메커니즘을 활용한다.
4.2. 유연한 설계
•
만약 할인 정책이 없는 영화가 등장하면 어찌할 것인가??
◦
걱정할 것 없이 새로운 할인 정책을 정의하는 객체를 추가하여 진행하면 된다.
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
Java
복사
•
이처럼 예외적인 케이스가 등장하더라도 일관성을 유지한 채 새로운 정책을 도입할 수 있다.
•
중요한 것은 기존 객체의 수정 없이 새로운 클래스를 추가하는 것 만으로도 기능을 확장했다는 것이다.
•
추상화가 유연한 설계를 가능케 하는 이유는 설계가 구체적 상황에 결합되는 것을 방지하기 때문이다.
•
결론은 간단하게 유연성이 필요한 곳에 추상화를 사용하라.
4.3. 추상 클래스와 인터페이스 트레이드오프
[오브젝트] p69
•
DiscountPolicy를 인터페이스로 구현하도록 변경 후 기존 추상 클래스를 DefaultDiscountPolicy 클래스로서 해당 추상함수를 구현받는 쪽이 실제 정책을 지니도록 수정한다.
•
이로서 NoneDiscountPolicy와 개념적으로 분리되어 혼란과 결합을 제거할 수 있게되었다.
•
이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋을 것이지만 하나의 정책을 위해서 인터페이스를 추가하는건 과하다고 생각이 들 수도 있다.
•
구현과 관련된 모든 것들이 트레이드오프 대상이 될 수 있다.
•
우리가 작성하는 모든 코드에는 합당한 이유가 있어야 하고 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 아닌것은 차이가 크다.
4.4. 상속
•
상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이지만 설계에 안좋은 영향을 미친다.
◦
캡슐화를 위반하고, 설계를 유연하지 못하게 만든다.
•
부모 클래스의 구현이 자식 클래스에게 노출되기 쉽기 때문에 캡슐화가 약화된다.
◦
캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모를 변경할 때 자식도 함께 변경될 확률이 높다.
•
결과적으로 상속을 과도하게 사용한 코드는 변경하기도 어려워진다.
•
상속은 부모와 자식 클래스의 관계를 컴파일 시점에 결정하기 때문에 실행 시점에 객체의 종류를 변경하는게 불가능해진다.
◦
이는 유연하지 못한 설계에 영향을 미치게된다.
4.5. 합성
•
인터페이스에 정의된 메시지를 통해서만 코드를 재사용 하는 방법을 합성이라고 부른다.
•
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
◦
인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.
◦
의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
•
때문에 일반적으로는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다.
•
그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다.
◦
이전 처럼 코드를 재사용하는 경우에는 상속보다는 합성을 선호하는 것이 옳다
◦
하지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.