최후통첩 게임은 인간을 바라보는 두 가지 관점의 충돌을 잘 설명한다.
인간이 가지고 있는 본연의 특성이라는 관점에서 인간은 이기적으로 합리적인 존재다.
그러나 타인과 관계를 맺는 과정 속에서 인간은 본연의 특성을 배제하고 자신의 이익을 최소화하는 불합리한 선택을 하게 된다.
결론적으로 인간이 어떤 본질적인 특성을 지니고 있느냐가 아니라 어떤 상황에 처해 있느냐가 인간의 행동을 결정한다.
즉, 각 개인이 처해 있는 정황 또는 문맥이 인간의 행동 방식을 결정한다.
객체의 세계에서도 협렵이라는 컨텍스트가 객체의 행동 방식을 결정한다.
객별적인 객체의 행동이나 상태가 아니라 객체들 간의 협력에 집중하자.
1. 협력
협력의 본질은 요청과 응답으로 연결되는 네트워크다.
협력은 한 객체가 다른 객체에게 도움을 요청할 때 시작된다.
협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성된다.
2. 책임
어떤 객체가 어떤 요청에 대해 대답해 줄 수 있거나, 적절한 행동을 할 의무가 있는 경우 해당 객체가 책임을 가진하고 한다.
객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 객체에 할당하는 것이다.
크레이그 라만의 객체 분류
- 하는 것(doing): 스스로 하는 것. 다른 객체의 행동을 시작시키거나 활동을 제어하고 조절하는 것
- 아는 것(knowing): 개인적인 정보, 관견된 객체, 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
책임은 객체의 외부에 제공해 줄 수 있는 정보(아는 것)와 외부에 제공해 줄 수 있는 서비스(하는 것)의 목록이다.
따라서 책임은 객체의 공용 인터페이스를 구성한다. 공용 인터페이스의 개념은 캡슐화로 이어진다.
메시지
두 객체 간의 협력은 메시지를 통해 이뤄진다. 메시지는 협력을 위해 한 객체가 다른 객체로 접근할 수 있는 유일한 방법이다.
책임: 협력이라는 문맥 속에서 수신자 객체 관점에서 무엇을 할 수 있는지를 나열하는 것
메시지: 협력에 참여하는 두 객체 사이의 관계를 강조한 것
책임과 메시지의 수준이 같지는 않다.
책임은 객체가 협력에 참여하기 위해 수행해야 하는 행위를 상위 수준에서 개략적으로 서술한 것이다.
책임을 결정한 후 실제로 협력을 정제하면서 이를 메시지로 변환할 때 하나의 책임이 여러 메시지로 분할된다.
3. 역할
어떤 객체가 수행하는 책임의 집합은 객체가 협력 안에서 수행하는 역할을 암시한다.
역할은 재사용 가능하고 유연한 객체지향 설계를 낳는다.
역할은 객체지향 설계의 단순성, 유연성, 재사용성을 뒷받침하는 핵심 개념이다.
협력의 추상화
역할의 가장 큰 가치는 하나의 협력 안에 여러 종류의 객체가 참여할 수 있게 함으로써 협력을 추상화할 수 있다는 것이다.
역할을 이용하면 협력을 추상화함으로써 단순화할 수 있다.
동일한 구조의 협력을 다양한 문맥에서 재사용할 수 있는 능력은 객체지향만의 힘이며,
그 힘은 근본적으로 역할의 대체 가능성에서 비롯된다.
대체 가능성
역할은 협력 내에서 다른 객체로 대체할 수 있음을 나타내는 일종의 표식이다.
그렇다면 어떤 객체라도 역할을 대체할 수 있을까? 그렇지는 않다.
객체가 역할을 대체하기 위해서는 행동이 호환돼야 한다.
역할이 수산할 수 있는 메시지를 동일한 방식으로 이해할 수 있어야 한다.
결국 객체는 역할이 암시하는 책임보다 더 많은 책임을 가질 수 있다.
따라서 객체의 타입과 역할 사이에는 일반화/특수화 관계가 성립한다.
역할이 협력을 추상적으로 만들 수 있는 이유는 역할 자체가 객체의 추상화이기 때문이다.
역할의 대체 가능성은 행위 호환성을 의미하고,
행위 호환성은 동일한 책임의 수행을 의미한다.
이상한 나라의 앨리스 - 누가 파이를 훔쳤지?
- 왕이 하얀 토끼에게 목격자를 부르라 명령한다.
- 하얀토끼가 모자 장수를 불러온다.
- 왕이 모자 장수에게 증언하라고 명령한다.
- 모자 장수가 알고 있는 것을 증언한다.
- 왕이 모자 장수에게 증인석에서 내려가라고 명령한다.
재판 속의 협력 관계는 다음과 같다.
- 누군가 -> 왕: 재판을 요청
- 왕 -> 하얀 토끼: 증인을 부를 것을 요청
- 하얀 토끼 -> 모자 장수: 증인석으로 입장할 것을 요청
- 모자 장수 -> 하얀 토끼 -> 왕: 증인석에 입장함으로써 요청에 응답
- 왕 -> 모자 장수: 증언할 것을 요청
- 모자 장수 -> 왕: 증언함으로써 요청에 응답
각 객체들의 책임을 살펴보자.
- 왕
- 하는 것: 재판에 참여하는 다른 객체들의 활동을 제어하고 조율
- 아는 것: 없음
- 하얀 토끼
- 하는 것: 모자 장수 객체의 증언을 시작시키는 것
- 아는 것: 목격자가 모자 장수라는 것에 대해 아는 것
- 모자 장수
- 하는 것: 스스로 증언석에 입장하는 것
- 아는 것: 사실에 대해 아는 것
모자 장수가 증언을 끝내고 퇴장한 후의 이야기를 살펴보자.
- 모자 장수가 떠나고, 왕은 하얀 토끼에게 다음 증인을 부르라고 명령한다.
- 이후 모자 장수 대신 요리사/앨리스가 증인으로 참석하고, 앨리스가 증인으로 참석할 때 왕 대신 여왕이 증언 요청을 한다.
요리사가 증언을 하는 경우에도, 앨리스가 증언을 하고 여왕이 재판을 진행하는 경우에도 재판 과정은 이전과 동일하다.
왕과 여왕은 '판사'의 역할을, 모자 장수, 요리사, 앨리스는 '증인'의 역할을 수행한 것으로 추상화할 수 있다.
- 누군가 -> 판사(왕, 여왕): 재판을 요청
- 판사 -> 하얀 토끼: 증인을 부를 것을 요청
- 하얀 토끼 -> 증인(모자 장수, 요리사, 앨리스): 증인석으로 입장할 것을 요청
- 증인 -> 하얀 토끼 -> 판사: 증인석에 입장함으로써 요청에 응답
- 판사 -> 증인: 증언할 것을 요청
- 증인 -> 판사: 증언함으로써 요청에 응답
interface Witness {
name: string;
testify: () => string | void;
}
interface TrialHelper {
name: string;
setWitness: (witness: Witness) => void;
callWitness: () => Witness | undefined;
}
abstract class Judge {
health = 0;
name: string;
constructor(name: string) {
this.name = name;
}
public isOk() {
return this.health < 2;
}
listenToTestimony(trialHelper: TrialHelper) {
if (!this.isOk()) {
throw new Error("머리가 너무 아파졌다오.");
}
try {
console.log(`${this.name}이 ${trialHelper.name}에게 명령`);
const message = trialHelper.callWitness()?.testify();
this.health++;
return this._listenToTestimony(message || "");
} catch (e: any) {
console.log(e.message);
this.health++;
throw new Error("신경쓸 것 없다!");
}
}
abstract _listenToTestimony(message: string): void;
}
class King extends Judge {
_listenToTestimony(message: string) {
console.log("왕이 받은 증언: ", message);
}
}
class Queen extends Judge {
_listenToTestimony(message: string) {
console.log("여왕이 받은 증언: ", message);
}
}
class HatDealer implements Witness {
name = "모자 장수";
testify() {
return "모자 장수 증언";
}
}
class Cook implements Witness {
name = "요리사";
testify() {
throw new Error("요리사 도망감");
}
}
class Alice implements Witness {
name = "앨리스";
testify() {
return "앨리스 증언";
}
}
class WhiteRabbit implements TrialHelper {
name = "하얀 토끼";
private trialNum = 0;
private witness: Witness | undefined;
private witnessList: Witness[] = [new HatDealer(), new Cook(), new Alice()];
setWitness() {
if (this.trialNum >= this.witnessList.length) {
throw new Error("더 이상 증인이 없습니다!");
}
this.witness = this.witnessList[this.trialNum];
console.log(
`${this.name}가 ${this.witness.name}에게 증인석으로 입장할 것을 요청`
);
this.trialNum++;
}
callWitness() {
this.setWitness();
return this.witness;
}
}
class Trial {
constructor(
private judge: Judge,
private trialHelper: TrialHelper,
private subJudge: Judge = new Queen("여왕"),
witness?: Witness
) {
if (witness) {
this.changeWitness(witness);
}
}
tryTrial() {
while (1) {
this.judge.listenToTestimony(this.trialHelper);
}
}
start() {
try {
this.tryTrial();
} catch (e: any) {
console.log(e.message);
this.changeJudges(this.subJudge);
this.tryTrial();
}
}
changeWitness(witness: Witness) {
console.log(`${this.trialHelper.name}에게 증인을 부를 것을 요청`);
this.trialHelper.setWitness(witness);
}
changeJudges(judges: Judge) {
this.judge = judges;
}
}
class God {
static main() {
const trialHelper: TrialHelper = new WhiteRabbit();
const mainJudge: Judge = new King("왕");
const trial = new Trial(mainJudge, trialHelper);
try {
trial.start();
} catch (e) {}
}
}
God.main();
실행 결과
왕이 하얀 토끼에게 명령
하얀 토끼가 모자 장수에게 증인석으로 입장할 것을 요청
왕이 받은 증언: 모자 장수 증언
왕이 하얀 토끼에게 명령
하얀 토끼가 요리사에게 증인석으로 입장할 것을 요청
요리사 도망감
신경쓸 것 없다!
여왕이 하얀 토끼에게 명령
하얀 토끼가 앨리스에게 증인석으로 입장할 것을 요청
여왕이 받은 증언: 앨리스 증언
여왕이 하얀 토끼에게 명령
더 이상 증인이 없습니다!
객체지향에서의 흔한 오류
1. 시스템에 필요한 데이터를 저장하기 위해 객체가 존재한다는 선입견
물론 객체가 상태의 일부로 데이터를 포함하는 것은 사실이지만 데이터는 단지 객체가 행위를 수행하는 데 필요한 재료일 뿐이다.
객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서다. 실제로 중요한 것은 객체의 행동과 책임이다.
2. 객체지향이 클래스와 클래스 간의 관계를 표현하는 시스템의 정적인 측면에 중점을 둔다는 선입견
클래스는 단지 객체를 표현하기 위한 구현 메커니즘이다.
중요한 것은 정적인 클래스가 아니라 협력에 참여하는 동적인 객체다.
이러한 선입견의 원인은 협력이라는 컨텍스트를 고려하지 않고 각 객체를 독립적으로 바라보기 때문이다.
중요한 것은 객체의 겉모습이 아닌, 객체의 역할, 책임, 협력이다.
올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 한다.
일단 객체에게 책임을 할당하고 나면 책임은 객체가 외부에 제공하게 될 행동이 된다.
책임, 즉 행동을 결정한 후에 그 행동을 수행하는 데 필요한 데이터를 고민해야 한다.
그리고 데이터와 행동이 어느 정도 결정된 후에 클래스의 구현 방법을 결정해야 한다.
객체지향 설계 기법
책임-주도 설계
협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 애플리케이션을 설계한다.
개별적인 객체의 상태가 아닌 객체의 책임과 상호작용에 집중한다.
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 중에 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다.
디자인 패턴
전문가들이 식별해 놓은 역할, 책임, 협력의 설계 템플릿 모음이다. (패턴 = 모범이 되는 설계)
가장 유명한 책은 GOF의 디자인 패턴이다.
테스트-주도 개발
테스트를 먼저 작성하고 테스트를 통과하는 구체적인 코드를 추가하면서 애플리케이션을 완성해간다.
애자일 방법론의 한 종류인 XP의 기본 프랙티스다.
응집도가 높고 결합도가 낮은 클래스로 구성된 시스템을 개발할 때 최상의 프랙티스다.
다양한 설계 경험과 패턴에 대한 지식이 있어야만 이 기법을 적용했을 때의 온전한 혜택을 누릴 수 있다.
테스트를 작성하기 위해 객체의 메서드를 호출하고 반환값을 검증하는 것은 객체의 책임에 관해 생각하는 것이다.