[1장] 협력하는 객체들의 공동체
객체지향이란 실세계를 직접적이고 직관적으로 모델링할 수 있는 패러다임
객체지향 소프트웨어는 실세계의 투영이며, 현실세계에 존재하는 사물에 대한 추상화
위와 같은 설명은 실용적인 관점에서 객체지향을 설명하기 적합하지 않다. 객체지향의 목표는 실세계의 모방이 아니라 새로운 세계를 창조하는 것이다. 그럼에도 위와 같은 설명이 자주 사용되는 이유는 초심자가 객체지향의 다양한 측면을 이해하는데 효과적이기 때문이다.
- 객체를 스스로 생각하고 결정하는 생명체에 비유하는 것은 상태와 행위를 ‘캡슐화’ 하는 소프트웨어 객체의 ‘자율성’ 을 설명하는데 효과적
- 사람들이 협력하며 목표를 달성해 나가는 과정은 ‘메시지’ 를 주고받으며 ‘협력’ 하는 객체들의 관계를 설명하는데 적합
- 실세계의 사물을 기반으로 소프트웨어를 식별하고 구현한다는 개념은 ‘연결 완결성’ 을 설명
1장에서는 객체지향의 기본적인 내용을 설명하기 위해서 실세계의 모방 이라는 개념을 차용한다.
1-1. 협력하는 사람들
- 손님이 커피를 주문한다.
- 캐시어가 주문을 받는다.
- 바리스타가 커피를 제조한다.
- 바리스타가 커피를 옮긴다.
- 캐시어가 제조 완료를 알린다.
- 손님이 테이블 위에 커피를 가져간다.
커피를 주문하고 제조하는 과정은 역할, 책임, 협력 이라는 세가지 개념이 조화를 이루어 만들어낸다.
- 모든 과정 속에는 손님, 캐시어, 바리스타 사이의 암묵적인 협력 관계가 존재한다.
- 주문을 하는 손님, 주문을 받는 캐시어, 커피를 제조하는 바리스타라는 역할 이 존재한다.
- 손님, 캐시어, 바리스타는 주문한 커피를 제공하기 위해 협력하는 과정에서 책임 을 다한다.
협력은 요청과 응답으로 구성된다.
대부분의 문제는 복잡하기 때문에 다른 사람의 도움을 받아 문제를 해결한다. 일반적으로 요청은 연쇄적으로 발생하며, 협력은 역할을 맡은 개인이 얼마나 요청을 성실히 이행하는가에 달려있다.
역할은 협력에 참여하는 특정한 사람이 협력 안에서 차지하는 책임이나 의무를 의미한다. 사람들이 특정한 역할을 맡고 적합한 책임을 수행한다는 사실은 다음과 같은 개념을 제시한다.
- 여러 사람이 동일한 역할을 수행할 수 있다.
- 역할은 대체 가능하다.
- 책임을 수행하는 방법은 자율적으로 선택할 수 있다.
- 한 사람이 동시에 여러 역할을 수행할 수 있다.
1-2. 역할, 책임, 협력
- 사람: 객체
- 요청: 메시지
- 요청을 처리하는 방법: 메서드
협력의 핵심은 특정한 책임을 수행하는 역할들 간의 연쇄적인 요청과 응답을 통해 목표를 달성한다는 것이다. 어플리케이션의 기능은 더 작은 책임으로 분할되고 책임은 적절한 역할을 수행할 수 있는 객체에 의해 수행된다. 객체는 책임을 수행하는 도중에 다른 객체에게 도움을 청할 수 있다.
적절하게 책임을 할당하는 것이 객체지향 설계의 가장 중요한 요소이다.
1-3. 협력 속에 사는 객체
협력의 주체는 객체이기 때문에 협력의 품질을 결정하는 것은 객체의 품질이다. 객체의 품질이 좋다라고 판단할 수 있는 기준은 다음 두 가지이다.
- 객체는 충분히 협력적이어야 한다.
- 객체는 충분히 자율적이어야 한다.
객체는 공동의 목표를 달성하기 위해 협력하지만, 스스로의 결정과 판단에 따라서 행동하는 자율적인 존재이다. 즉, 객체는 스스로 판단하고 결정하기 위해 필요한 행동과 상태를 함께 지니고 있으며, 다른 객체가 알 필요가 없는 부분은 외부에서 간섭할 수 없도록 차단하며 외부에서의 접근이 허락된 수단을 통해서만 의사소통 해야한다.
객체지향의 세계에서는 오직 한가지 의사소통 수단인 ‘메시지’ 만 존재한다. 그리고 객체가 받은 메시지를 처리하는 방법은 ‘메서드’ 라고 한다. 메시지를 받은 객체가 실행시간에 메서드를 선택할 수 있다는 점은 다른 프로그래밍 언어와 객체지향 프로그래밍 언어를 구분짓는 핵심 특징 중 하나이다.
메시지와 메서드의 분리는 객체의 협력에 참여하는 객체들 간의 자율성을 증진시킨다. 외부의 요청이 무엇인지를 표현하는 메시지와 요청을 처리하기 위한 구체적인 방법인 메서드를 분리하는 것이 객체의 자율성을 높이는 핵심 매커니즘이다.
1-4. 객체지향의 본질
- 객체지향은 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법이다.
- 자율적인 객체란 상태와 행위를 함께 지니며 스스로 책임을 지는 객체이다.
- 객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다. 각 객체는 협력 내에세 정해진 역할을 수행하며, 역할은 관련된 책임의 집합이다.
- 객체는 다른 객체와 협력하기 위해서 메시지를 전송하고 메시지를 수신한 객체는 메시지를 처리하는데 적합한 메서드를 자율적으로 선택한다.
객체지향의 세계에서 클래스가 지나치게 강조되어 있다. 훌륭한 객체지향 설계자가 되기 위해서는 클래스 관점에서 메시지를 주고받는 객체의 관점으로 사고의 중심을 전환하는 것이 필요하다. 클래스의 구조와 메서드가 아니라 객체의 역할, 책임, 협력에 집중해야 한다.
1-alpha. 연습
- 손님 - 캐시어 - 바리스타 클래스 pseudo code
class Customer { orderCoffee() {} takeCoffee() {} } class Barista { receiveOrder() {} makeCoffee() {} relayCoffee() {} } class Casher { takeOrder() {} relayOrder() {} serveCoffee() {} }
-
손님 - 캐시어 - 바리스타의 상호작용
// 역할: 주문 class Order { menu: string; customer: Customer; constructor(customer: Customer, menu: string) { this.customer = customer; this.menu = menu; } getMenu() { return this.menu; } } // 역할: 커피 class Coffee { menu: string; constructor(menu: string) { this.menu = menu; } } // 역할: 고객 class Customer { coffee: Coffee | null; constructor() { this.coffee = null; } // 책임: 커피 주문 orderCoffee(casher: Casher, menu: string) {} // 책임: 커피를 받는다. takeCoffee(cofee: Cofee) {} } // 역할: 바리스타 class Barista { // 책임: 주문을 전해받는다. receiveOrder(caher: Caher, order: Order) {} // 책임: 커피를 만든다. makeCoffee(order: Order) { return new Coffee(order.getMenu()); } // 책임: 커피를 캐셔에게 전달한다. relayCoffee(casher: Casher, coffee: Coffee) {} } // 역할: 캐셔 class Casher { order: Order | null; barista: Barista; constructor(barista: Barista) { this.order = null; this.barista = barista; } // 책임: 주문을 받는다. takeOrder(customer: Customer, menu: string) {} // 책임: 주문을 바리스타에게 전달한다. relayOrder(barista: Barista, order: Order) {} // 책임: 커피를 바리스타로부터 받는다. receiveCoffee(coffee: Coffee) {} // 책임: 커피를 서빙한다. serveCoffee(customer: Customer, coffee: Coffee) {} } // Main Function const customr = new Customer(); const barista = new Barista(); const casher = new Casher(barista); // 고객이 커피를 주문한다. customer.orderCoffee(casher, '핸드드립 커피'); // 캐시어가 주문을 받는다. casher.takeOrder(customer, '핸드드립 커피'); // 캐시어가 주문을 바리스타에게 전달한다. casher.relayOrder(barista, new Order(customer, '핸드드립 커피')); // 바리스타가 주문을 전해받는다. barista.receiveOrder(casher, new Order(customer, '핸드드립 커피')); // 바리스타가 커피를 만든다. barista.makeCoffee(new Order(customer, '핸드드립 커피')); // 바리스타가 커피를 캐시어에게 전달한다. barista.relayCoffee(casher, new Coffee('핸드드립 커피')); // 캐시어가 커피를 받는다. casher.receiveCoffee(new Coffee('핸드드립 커피')); // 캐시어가 커피를 서빙한다. casher.serveCoffee(customer, new Coffee('핸드드립 커피')); // 고객이 커피를 받는다. customer.takeCoffee(new Coffee('핸드드립 커피'));
- 바리스타가 대체 가능한 구조로 변경해보기
- 바리스타 클래스가 다른 클래스가 바뀌더라도 정상 동작하도록
- 캐셔가 바리스타를 호출하는 코드는 그대로 유지되도록
상속 을 이용해서 해결해보기
// 역할: 바리스타 abstract class Barista { // 책임: 주문을 전해받는다. receiveOrder(casher: Casher, order: Order) {} // 책임: 커피를 만든다. abstract makeCoffee(order: Order): Coffee; // 책임: 커피를 캐시어에게 전달한다. relayCoffee(casher: Casher, coffee: Coffee) {} } // 역할: 바리스타 class HandDripBarista extends Barista { // 책임: 커피를 만든다. makeCoffee(order: Order) { // - 커피를 만든다 : '핸드드립 방식' return new Coffee('핸드드립'); } } // 역할: 캐시어 class Casher { order: Order | null; barista: Barista; constructor(barista: Barista) { this.order = null; this.barista = barista; } // 책임: 주문을 받는다. takeOrder(customer: Customer, menu: string) {} // 책임: 주문을 바리스타에게 전달한다. relayOrder(barista: Barista, order: Order) {} // 책임: 커피를 받는다. receiveCoffee(coffee: Coffee) {} // 책임: 커피를 서빙한다. serveCoffee(customer: Customer, coffee: Coffee) {} } casher.relayOrder(new HandDripBarista(), new Order(customer, '아메리카노'));
메시지는 외부에서 알 수 있고, 메서드는 메시지를 처리하는 코드라고 볼 수 있을 듯.
makeCoffee
는 메시지이고, 그 내부 구현은 메서드라고 생각하면 되지 않을까?
[2장] 이상한 나라의 객체
객체지향 패러다임은 지식을 추상화하고 추상화한 지식을 객체 안에 캡슐화함으로써 실세계 문제에 내재된 복잡성을 관리하려고 한다. 객체를 발견하고 창조하는 것은 지식과 행동을 구조화하는 문제다.
인간은 본능적으로 세상을 독립적이고 식별 가능한 객체의 집합으로 바라본다. 인간이 직접적으로 지각할 수 있는 대부분의 객체는 물리적인 경계를 지닌 구체적인 사물이지만, 인간의 인지 능력은 물리적인 한계를 넘어 개념적으로 경계 지을 수 있는 추상적인 사물까지도 객체로 인식할 수 있다.
2-1. 객체, 그리고 이상한 나라
이상한 나라의 앨리스 이야기
- 앨리스는 문 뒤로 아름다운 정원이 펼쳐져 있는 것을 발견
- 문의 높이가 40cm, 앨리스의 몸을 작게 만들 방법 찾기 시작
- 병 속의 액체를 마시거나, 케이크를 먹거나, 부채질을 했을 때 키가 변함
위 이야기는 앨리스가 겪고 있는 키의 변화에 초점을 맞추고 있다. 정원으로 향하는 문을 통과하기에 적당한 상태로 자신의 키를 변화시키는 과정에서 특정 시점의 앨리스의 상태란 특정 시점에서의 앨리스의 키를 의미한다. 여기서 앨리스의 키는 시간의 흐름에 따라 계속해서 변하지만, 키를 변화시키는 것은 앨리스의 행동이다.
즉, 행동에 따라 상태가 변한다.
문을 통과한다는 행동처럼 어떤 행공의 성공 여부는 이전에 어떤 행동들이 발생했는지 영향을 받는다. 이는 행동 간의 순서가 중요하다는 것을 의미한다. 또한 행동에 의해 앨리스의 상태가 변경되더라도 앨리스라는 사실은 변하지 않는다. 앨리스는 상태 변경과 무관하게 유일한 존재로 식별 가능하다.
앨리스의 특징을 요약하자면
- 상태를 가지며 변경가능함
- 상태를 변경시키는 것은 행동이다.
- 행동의 결과는 상태에 의존적이며 상태를 이용해 서술할 수 있다.
- 행동의 순서가 결과에 영향을 미친다.
- 어떤 상태에 있더라도 유일하게 식별 가능하다.
2-2. 객체, 그리고 소프트웨어 나라
객체는 상태, 행동, 식별자라는 개념을 통해 객체가 가진 다양한 특성을 효과적으로 설명할 수 있다. 이 책에서 객체를 다음과 같이 정의한다.
객체란 식별 가능한 개체 또는 사물이다. 객체는 자동차처럼 만질 수 있는 구체적인 사물일 수도 있고, 시간처럼 추상적인 개념일 수도 있다. 객체는 구별 가능한 식별자, 특징적인 행동, 변경 가능한 상태를 가진다. 소프트웨어 안에서 객체는 저장된 상태와 실행 가능한 코드를 통해 구현된다.
왜 상태가 필요한가
객체가 주변 환경과의 상호작용에 어떻게 반응하는가는 그 시점까지 객체에 어떤 일이 발생했느냐에 좌우된다. 행동의 결과를 예상할 수 있는 한 가지 방법은 과거 행동을 모두 기억하고 있어야하기에 결과를 설명하기에 적합하지 않다. 따라서 행동의 과정과 결과를 단순하게 기술하기 위해 상태라는 개념이 나왔다. 상태를 이용하면 과거의 모든 행동 이력을 설명하지 않고도 행동의 결과를 예측하고 설명할 수 있다.
상태와 프로퍼티
모든 것이 객체는 아니다. 분명하게 인식할 수 있음에도 객체의 영역에 포함시킬 수 없는 것들이 존재하는데, 앨리스 이야기를 예로 들면 앨리스의 ‘키’와 ‘위치’처럼 숫자, 문자열, 양, 속도 등의 단순한 값들은 객체가 아니다. 그 자체로 독립적인 의미를 가지기보다는 객체의 상태를 표현하는데 사용된다.
때로는 단순한 값이 아니라 객체를 사용해 다른 객체의 상태를 표현해야할 때가 있다. 앨리스가 현재 음료를 들고 있는 상태인지를 표현하고 싶다면 음료라는 객체를 이용할 수 있다. 객체지향 관점에서 앨리스는 음료 객체에 대해 알고 있는 상태이며, 앨리스 객체와 음료 객체는 서로 연결돼 있다. 이처럼 객체 사이의 의미있는 연결을 링크라고 한다. 즉, 객체의 링크를 통해서만 메시지를 주고받을 수 있다.
결론적으로 모든 객체의 상태는 단순한 값과 객체의 조합으로 표현할 수 있다. 이때 객체의 상태를 구성하는 모든 특징을 통틀어 객체의 프로퍼티라고 한다. 일반적으로 프로퍼티는 변경되지 않고 고정되기 때문에 ‘정적’이다. 반면 프로퍼티 값은 시간이 흐름에 따라 변경되기 때문에 ‘동적’이다.
링크와 달리 객체를 구성하는 단순한 값은 속성이라고 한다. 앨리스의 키와 위치는 단순한 값으로 표현되기 때문에 속성이다. 객체의 프로퍼티는 속성과 링크 두 가지 종류의 조합으로 표현할 수 있다.
객체의 상태를 다음과 같이 정의한다.
상태는 특정 시점에 객체가 가지고 있는 정보의 집합으로 객체의 구조적 특징을 표현한다. 객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다. 객체의 프로퍼티는 단순한 값(속성)과 다른 객체를 참조(링크)로 구분할 수 있다.
객체지향에서 객체는 다른 객체의 상태에 직접 접근, 상태 변경이 불가능하다. 자율적인 객체는 스스로 상태를 책임져야 한다. 외부의 객체가 직접적으로 객체의 상태를 다룰 수 없기에 간접적으로 변경 및 조회할 방법이 필요하다.
이 때 필요한 것이 행동이다. 행동은 다른 객체로 하여금 간접적으로 객체의 상태를 변경하는 것을 가능하게 한다. 객체지향의 기본 사상은 상태와 상태를 조작하기 위한 행동을 하나로 묶는 것이다. 객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 객체의 자율성을 유지한다.
객체의 행동은 자신의 상태를 변경시킨다. 이는 행동이 부수 효과(side effect)를 초래한다는 것을 의미한다.
따라서 상태와 행동 사이의 관계를 서술하면
- 객체의 행동은 상태에 영향을 받는다.
- 객체의 행동은 상태를 변경시킨다.
이것은 상태 개념을 이용해 행동을 두 가지 관점에서 서술할 수 있다.
- 상호작용이 현재의 상태에 어떤 방식으로 의존하는가 (ex. 앨리스의 키가 40cm 이하라면 문을 통과할 수 있다.)
- 상호작용이 어떻게 현재의 상태를 변경시키는가 (ex. 문을 통과한 후에 앨리스의 위치는 아름다운 정원으로 바뀌어야 한다.)
협력과 행동
객체가 다른 객체와 협력하는 유일한 방법은 다른 객체에게 요청을 보내는 것이다. 요청을 수신한 객체는 요청을 처리하기 위해 적절한 방법에 따라 행동한다. 단, 메시지를 통해서만 의사소통 할 수 있다.
객체는 협력에 참여하는 과정에서 자기 자신의 상태뿐만 아니라 다른 객체의 상태 변경을 유발할 수도 있다.
정리하면 객체의 행동으로 인해 발생하는 결과를 두 가지 관점에서 설명하면
- 객체 자신의 상태 변경
- 행동 내에서 협력하는 다른 객체에 대한 메시지 전송
행동을 정의하면
행동이란 외부의 요청 또는 수신된 메시지에 응답하기 위해 동작하고 반응하는 활동이다. 행동의 결과로 객체는 자신의 상태를 변경하거나 다른 객체에게 메시지를 전달할 수 있다. 객체는 행동을 통해 다른 객체와의 협력에 참여하므로 행동은 외부에 가시적이어야 한다.
상태 캡슐화
객체지향의 세계에서 모든 객체는 자신의 상태를 스스로 관리하는 자율적인 존재다. 앨리스 객체의 키를 작게 만드는 것이 앨리스 자신인 것처럼 음료 객체의 양을 줄이는 것은 음료 자신이어야 한다. 앨리스는 음료에게 자신이 음료를 마셨다는 메시지를 전달할 수 있을 뿐이다.
앨리스가 음료를 마시는 과정에서 이뤄지는 앨리스와 음료 사이의 협력 관계
- others send
drinkBeverage()
message to Alice- Alice sends
drunken(quantity)
message to Beverage
위 예시에서 메시지만으로는 앨리스의 키가 줄어든다거나 음료의 양이 줄어든다는 상태 변경을 예상하기 어렵다. 메시지를 보낸 객체는 수신자의 상태 변경에 대해서 전혀 알지 못한다.
캡슐화가 의미하는 것이 이런 것이다. 객체는 상태를 캡슐 안에 감춰둔 채 외부로 노출하지 않는다. 객체가 외부에 노출하는 것은 행동뿐이며, 외부에서 객체에 접근할 수 있는 유일한 방법 역시 행동뿐이다.
상태를 외부에 노출시키지 않고 행동을 경계로 캡슐화하는 것은 결과적으로 객체의 자율성을 높인다. 협력에 참여하는 객체들의 자율성이 높아질수록 협력은 유연하고 간결해진다. 정리하면, 상태를 잘 정의된 행동 집합 뒤로 캡슐화하는 것은 객체의 자율성을 높이고 협력을 단순하고 유연하게 만든다.
식별자
객체 간 서로 구별할 수 있는 특정한 프로퍼티를 식별자라고 한다. 객체가 가지는 프로퍼티의 타입은 객체나 단순한 값 중 하나가 될 수 있다. 값과 객체의 가장 큰 차이점은 객체와 달리 값은 식별자를 지니지 않는다는 점이다.
값의 상태는 변하지 않기에 불변 상태(immutable state)를 가진다고 말한다. 두 개의 1이라는 숫자가 있을 때 동일하다고 판단하며, 두 개의 숫자를 구별하지 않는다.
값이 같은지 여부는 상태가 같은지를 이용해 판단한다. 값의 상태가 같으면 두 인스턴스는 동일하다고 판단한다. 이처럼 상태를 이용해 두 값이 같은지 판단할 수 있는 성질을 동등성(equality) 이라고 한다. 상태를 이용해 동등성을 판단할 수 있는 이유는 값의 상태가 변하지 않기 때문이다.
객체는 시간에 따라 변경되는 상태를 포함하며, 행동을 통해 상태를 변경한다. 따라서 객체는 가변 상태(mutable state)를 가진다. 타입이 같은 두 객체의 상태가 완전히 똑같더라도 두 객체는 독립적인 객체로 다뤄야 한다. 하지만 상태와 무관하게 식별자가 같다면 두 객체를 같은 객체로 판단할 수 있는데, 이러한 성질을 동일성(identical) 이라고 한다.
상태를 기반으로 객체의 동일성을 판단할 수 없는 이유는 시간이 흐름에 따라 객체의 상태가 변하기 때문이다.
식별자의 정의는 다음과 같다.
식별자란 어떤 객체를 다른 객체와 구분하는데 사용하는 객체의 프로퍼티다. 값은 식별자를 가지지 않기 때문에 상태를 이용한 동등성 검사를 통해 두 인스턴스를 비교해야 한다. 객체는 상태가 변경될 수 있기 때문에 식별자를 이용한 동일성 검사를 통해 두 인스턴스를 비교할 수 있다.
값과 객체의 차이점이 혼란스러운 이유는 대부분의 객체지향 프로그래밍 언어에서 두 개념 모두 클래스를 이용해 구현되기 때문이다. 이런 오해의 소지를 줄이기 위해 객체와 값을 지칭하는 별도의 용어를 사용하기도 한다. 참조 객체(reference object) 또는 엔티티(entity) 는 식별자를 지닌 객체, 값 객체(value object) 는 식별자를 가지지 않는 값을 가리킨다.
2-3. 기계로서의 객체
일반적으로 객체의 상태롤 조회하는 작업을 쿼리(query), 상태를 변경하는 작업을 명령(command)라고 한다. 객체가 외부에 제공하는 행동의 대부분은 쿼리와 명령으로 구성된다.
버트란트 마이어는 객체를 기계에 비유해서 설명하는데, 정리하면 객체에 접근할 수 있는 유일한 방법은 객체가 제공하는 행동뿐이라는 것이다.
행동이 상태를 결정한다
객체지향에 입문한 사람들이 쉽게 빠지는 함정은 상태를 중심으로 객체를 바라보는 것이다. 초보자들은 필요한 상태가 무엇인지 결정하고 그 상태에 필요한 행동을 결정한다. 이는 설계에 나쁜 영향을 끼친다.
- 상태를 먼저 결정할 경우 캡슐화가 저해된다.
- 객체를 협력자가 아닌 고립된 섬으로 만든다.
- 객체의 재사용성의 떨어진다.
협력에 중요한 것은 상태가 아니라 행동에 초점을 맞추는 것이다.
설계자로서 우리는 협력의 문맥에 맞는 적절한 행동을 수행하는 객체를 발견하거나 창조해야 한다.
협력 안에서 객체의 행동은 결국 객체가 협력에 참여하면서 완수해야하는 책임을 의미한다. 따라서 어떤 책임이 필요한가를 결정하는 과정이 전체 설계를 주도해야 한다. 책임-주도 설계(Responsibility-Driven Design, RDD) 는 협력이라는 문맥 안에서 객체의 행동을 생각하도록 도움으로써 응집도 높고 재사용 가능한 객체를 만들 수 있게 한다.
2-alpha. 연습
- 앨리스와 음료
class Beverage { private quantity: number; constructor(capacity: number) { this.quantity = capacity; } drunken(quantity: number) { if(this.quantity >= quantity) { this.quantity -= quantity; } } } class Alice { private height: number; private location: string; private beverage: Beverage; constructor(height: number, location: string) { this.height = height; this.location = location; this.beverage = new Beverage(1000); } drinkBeverage() { this.beverage.drunken(500); this.isAbleToPassTheDoor() && this.updateLocation('garden'); } isAbleToPassTheDoor() { return this.height < 40; } updateLocation(location: string) { this.location = location; } }
일단 컴퓨터를 조작하는 것이 추상화를 구축하고, 조작하고, 추론하는 것에 관한 모든 것이라는 것을 깨닫고 나면 훌륭한 컴퓨터 프로그램을 작성하기 위한 중요한 전제 조건은 추상화를 정확하게 다루는 능력이라는 것이 명확해진다.
[3장] 타입과 추상화
3-1. 챕터의 시작: 런던 지하철
지하철은 도시의 팽창과 산업화에 비례해 복잡성이 늘어나는 특징이 있다.
승객이 바라는 것은 역에서 또 다른 역으로의 이동에 필요한 정보 습득이다. 따라서 지하철 노선도는 현실의 정보를 얼마나 정확하게 담아냈는지가 아니라, 역과 역 사이의 연결성을 직관적으로 표현했는지가 중요하다.
지도를 바라보는 관점을 ‘정확성’ 대신 ‘목적’으로 바꾼 것은 그에 적합한 정보가 명확하게 드러나도록 추상화 한 것이다.
3-2. 추상화를 통한 복잡성 극복
추상화란 불필요한 정보를 걷어내고 사물의 본질을 드러나게 하는 과정이다.
추상화의 수준, 이익, 가치는 목적에 의존적이며, 복잡성을 다루기 위해 두 차원에서 추상화가 이뤄진다.
- 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순화
- 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거함으로써 단순화
객체지향 패러다임은 객체라는 추상화를 통해 복잡성을 극복한다. 그리고 객체지향 패러다임을 이용해 좋은 프로그램을 만들기 위해선 추상화의 두 차원을 올바르게 이해하고 적용하는 것이다.
3-3. 객체지향과 추상화
앨리스와 하트 여왕 이야기 등장 인물(?): 앨리스, 정원사, 클로버 병사, 신하, 공주와 왕자, 왕과 왕비, 하트 왕과 하트 여왕, 그리고 하얀 토끼
이야기에 등장하는 객체들은 각기 다른 모습과 행동 양식을 지니고 있다.
‘기껏해야 트럼프에 불과해’
하지만 앨리스는 차이점은 과감하게 무시한 채 공통점만을 취해 ‘트럼프’로 단순화해서 바라보고 있다.
- 그룹으로 나누어 단순화하기
- 명확한 경계를 가지고 서로 구별할 수 있는 무언가를 객체지향 패러다임에서는 객체라고 한다.
- 다양한 인물들의 차이점을 무시하면 공통점을 찾을 수 있다. (예시, ‘트럼프’)
- 각 객체별로 그룹을 나눌 수 있다. (예시, 트럼프 그룹과 트럼프가 아닌 그룹)
- 개념
- 차이점을 무시하고 공통점만을 취해 트럼프라는 개념으로 단순화한 것은 추상화의 일종이다.
- 공통점을 기반으로 객체들을 묶기 위한 그릇을 개념(concept) 이라고 한다.
- 개념을 이용하면 객체를 여러 그룹으로 분류(classification) 할 수 있다.
- 각 객체는 특정한 개념을 표현하는 그룹의 일원으로 포함된다. 객체에 어떤 개념을 적용하는게 가능해서 그룹의 일원으로 분류되었다면 그 객체를 해당 개념의 인스턴스(instance) 라고 한다.
- 즉, 객체란 특정한 개념을 적용할 수 있는 구체적인 사물을 의미한다. 개념이 객체에 적용됐을 때 객체를 개념의 인스턴스라고 한다.
- 개념의 세 가지 관점
- 심볼(symbol) : 개념을 가리키는 간략한 이름이나 명칭
- 트럼프
- 내연(intension) : 개념의 완전한 정의를 나타내며 내연의 의미를 이용해 객체가 개념에 속하는지 여부를 확인할 수 있다.
- 몸이 납작하고 두 손과 두 발은 네모 귀퉁이에 달려 있는 등장인물
- 외연(extension) : 개념에 속하는 모든 객체의 집합(set)
- 정원사, 병사, 신하, 왕자와 공주, etc
- 심볼(symbol) : 개념을 가리키는 간략한 이름이나 명칭
- 객체를 분류하기 위한 틀
- 외연의 관점에서 어떤 객체에 어떤 개념을 적용할 수 있다는 것은 동일한 개념으로 구성된 객체 집합에 해당 객체를 포함시키는 것을 의미한다.
- 분류란 객체에 특정한 개념을 적용하는 작업이다. 객체에 개념을 적용하기로 결심했을 때 우리는 그 객체를 집합의 멤버로 분류하고 있는 것이다.
- 어떤 객체를 어떤 개념으로 분류할지가 객체지향의 품질을 결정한다.
3-4. 타입
타입은 개념이다. 따라서 타입이란 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 의미한다. 어떤 객체에 타입을 적용할 수 있을 때 그 객체를 타입의 인스턴스라고 한다. 타입의 인스턴스는 타입을 구성하는 외연인 객체 집합의 일원이 된다.
- 데이터 타입
무질서한 메모리 안의 데이터에 특정한 의미를 부여 및 분류하기 시작하면서 프로그래밍 언어에 타입 시스템이 생기기 시작했다.
타입 시스템의 목적은 데이터가 잘못 사용되지 않도록 제약사항을 부과하는 것이다.
정리하면- 타입은 데이터가 어떻게 사용되느냐에 관한 것이다. 연산자의 종류가 아니라 어떤 데이터에 어떤 연산자를 적용할 수 있느냐가 그 데이터의 타입을 결정한다.
- 타입에 속한 데이터를 메모리에 어떻게 표현하는지는 외부로부터 철저하게 감춰진다. 데이터 타입의 표현 방식을 몰라도 개발자는 데이터를 사용할 수 있다.
데이터 타입은 메모리 안에 저장된 데이터의 종류를 분류하는데 사용하는 메모리 집합에 관한 메타데이터다. 데이터에 대한 분류는 암시적으로 어떤 종류의 연산이 해당 데이터에 대해 수행될 수 있는지를 결정한다.
- 객체와 타입
데이터 타입에서의 타입과 객체지향의 타입은 연관성이 많다.
객체는 행위에 따라 변할 수 있는 상태를 가지고 있다. 하지만 객체는 데이터가 아니다. 객체에서 중요한 것은 객체의 행동이고, 행동의 결과로 초래된 부수효과를 표현하기 위해 도입된 추상적 개념이 상태일 뿐이다.
객체를 만들 때 다른 객체와 협력하기 위해 어떤 행동(책임)을 지녀야 하는지 결정하는 것이 객체지향 설계의 핵심이다. 따라서 데이터 타입에 대해 언급했던 두 가지 조언은 객체의 타입을 이야기 할 때도 동일하게 적용된다.
- 어떤 객체가 어떤 타입에 속하는지를 결정하는 것은 객체가 수행하는 행동이다.
- 객체의 내부적인 표현은 외부로부터 철저하게 감춰진다.
- 객체를 결정하는 것은 행동이다
위의 두 가지 조언에 따르면, 객체의 내부 표현 방식이 다르더라도 어떤 객체들이 동일하게 행동한다면 그 객체들은 동일한 타입에 속한다고 말할 수 있다. 즉, 객체 분류의 기준은 객체가 타입에 속한 다른 객체와 동일한 행동을 하기만 하면 된다. 객체가 지닌 데이터는 관심사가 아니다.
타입이 데이터가 아니라 행동에 의해 결정된다는 사실은 객체지향 패러다임을 특징 짓는 중요한 몇 가지 원리와 원칙에 의미를 부여한다.- 다형성: 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력을 의미. 동일한 행동은 동일한 책임을 의미하며, 동일한 책임은 동일한 메시지 수신을 의미한다. 동일한 타입에 속한 객체는 내부의 데이터 표현 방식이 다르더라도 동일한 메시지를 수신하고 처리할 수 있다.
- 캡슐화: 외부에 행동만을 제공하고 데이터는 행동 뒤로 감추는 것. 행동만이 고려 대상이라는 사실은 외부에 데이터를 감춰야 한다는 것을 의미한다. 데이터가 외부로 노출되어 객체 인터페이스를 오염시키는 순간 객체의 분류 체계는 붕괴될 가능성이 생겨 유연하지 못한 설계를 초래한다.
- 책임 주도 설계(Responsibility-Driven Design): 객체가 외부에 제공해야할 책임을 먼저 결정하고난 뒤, 책임을 수행하는데 적합한 데이터를 결정하고 캡슐화하는 것. 데이터를 먼저 생각하는 데이터 주도 설계(Data-Driven Design) 방식의 단점을 개선하기 위해 고안되었다.
3-5. 타입의 계층
앨리스 이야기에 등장 인물들은 ‘트럼프’라기 보다는 ‘트럼프 인간’이라고 분류하는게 더 정확하다.
외양은 트럼프와 비슷하나, 행동 자체는 완벽하게 동일하지 않다.
다만, 트럼프 인간 타입의 객체는 트럼프 타입의 객체가 할 수 있는 모든 행동이 가능하고, 추가적으로 걸어다니고 말할 수 있다.
따라서 트럼프 인간 타입은 트럼프 타입의 부분 집합으로 표현할 수 있다. 바꿔 말하면, 트럼프 인간은 트럼프보다 조금 더 특화된 행동을 하는 특수한 개념이다. 이 두 개념 사이의 관계를 일반화/특수화(generalization/specialization) 관계 라고 한다.
객체지향에서 일반화/특수화 관계를 결정하는 것은 객체의 행동이다. 행동의 관점에서 일반적인 타입이란 특수한 타입이 가진 모든 행동들 중에서 일부 행동만을 가지는 타입을 의미한다. 특수한 타입이란 일반적인 타입이 가진 모든 타입을 포함하지만 거기에 더해 자신만의 행동을 추가하는 타입을 가리킨다.
일반적인 타입을 슈퍼타입(Supertype) , 특수한 타입을 서브타입(Subtype) 이라고 한다. 일반적으로 서브타입은 슈퍼타입의 행위와 호환되기 때문에 서브타입은 슈퍼타입을 대체할 수 있어야 한다. 표기법은 슈퍼타입을 상단에, 서브타입을 하단에 두고 속이 빈 삼각형으로 연결해서 표현한다. 슈퍼타입의 행동은 서브타입에 상속된다.
일반화/특수화 계층은 객체지향 패러다임에서 중요한 부분을 강조하기 위해 불필요한 세부사항을 제거한다는 추상화의 두 번째 차원을 적절하게 활용하는 대표적 예다.
앨리스 이야기에는 추상화 두 가지 기법 모두 사용되었다.
- 등장인물들의 차이점을 배제하고 공통점만을 강조함으로써 이들을 공통의 타입은 트럼프 인간으로 분류
- 트럼프 인간을 좀 더 단순한 관점에서 바라보기 위해 불필요한 특성을 배제하고 포괄적 의미를 지닌 트럼프로 일반화
3-6. 정적 모델
타입의 목적은 객체의 복잡성을 단순화하는 것이다. 타입은 시간에 따라 동적으로 변하는 객체의 상태를 시간과 무관한 정적인 모습으로 다룰 수 있게 해준다.
그래서 결국 타입은 추상화다. 타입은 시간과 상태 변화라는 요소를 제거하고 철저하게 정적인 관점에서 객체를 묘사하는 것이 가능하게 만든다.
객체를 생각할 때 우리는 두 가지 모델을 동시에 고려한다.
- 동적 모델: *스냅샷 처럼 계속해서 상태 변화를 포착하는 것 *객체가 특정 시점에 어떤 상태를 가지는가를 의미
- 타입 모델: 객체가 가질 수 있는 모든 상태와 행동을 시간에 독립적으로 표현하는 것. 정적 모델이라고도 한다.
객체지향 애플리케이션을 설계하고 구현하기 위해서는 객체 관점의 동적 모델과 객체를 추상화한 타입 관점의 정적 모델을 적절히 혼용해야 한다. 코드를 작성할 때는 정적인 관점에서, 디버깅을 할 때는 동적인 관점에서 객체를 바라보는 것.
객체지향 프로그래밍 언어에서 정적인 모델은 클래스를 이용해 구현된다. 따라서 타입을 구현하는 가장 보편적인 방법은 클래스를 이용하는 것이다. ‘타입을 구현한다’라고 표현했는데, 이는 클래스와 타입이 동일하지 않다는 것을 강조하기 위함이다. 타입은 객체를 분류하기 위한 개념이고, 클래스는 타입을 구현할 수 있는 여러 구현 메커니즘 중 하나이다. 클래스와 타입을 구분하는 것은 설계를 유연하게 유지하기 위한 바탕이 된다.
3-alpha. 연습
- 트럼프 카드 등장인물 및 하얀토끼
// 심볼: 트럼프 class TrumpCard { // 내연: 몸이 납작하고 두 손과 두 발은 네모 귀퉁이에 달려 있는 것 private shape: string; private hands: number; private feet: number; constructor(shape: string, hands: number, feet: number) { this.shape = shape; this.hands = hands; this.feet = feet; } } // 심볼: 트럼프 인간 class TrumpCardPerson extends TrumpCard { private role: string; constructor(role: string) { super('square', 2, 2); this.role = role; } walk() {} speak() {} } // 심볼: 토끼 class Rabbit { // 내연: 귀가 길고 앞발은 짧고 뒷발은 길어 깡충깡충 뛰어다니는 것 } // 심볼: 하얀 토끼 class WhiteRabbit extends Rabbit { } // 외연: 트럼프 집합 const TrumpCardList: TrumpCard[] = [ new TrumpCardPerson('Gardener'), new TrumpCardPerson('CloverSoldier'), new TrumpCardPerson('Courtier'), new TrumpCardPerson('Prince'), new TrumpCardPerson('Princess'), new TrumpCardPerson('King'), new TrumpCardPerson('Queen'), new TrumpCardPerson('HeartKing'), new TrumpCardPerson('HeartQueen'), ]; // 외연: 토끼 집합 const RabbitList: Rabbit[] = [ new WhiteRabbit() ];
[4장] 역할, 책임, 협력
객체지향에 갓 입문한 사람들의 가장 흔한 실수는 협력이라는 문맥을 고려하지 않은 채 객체가 가져야할 상태와 행동부터 고민하기 시작한다는 것이다.
중요한 것은 개별 객체가 아니라 객체들 사이에 이뤄지는 협력이다. 객체지향 설계의 전체적인 품질을 결정하는 것은 개별 객체의 품질이 아니라 여러 객체들이 모여 이뤄내는 협력의 품질이다.
협력이 자리를 잡으면 저절로 객체의 행동이 드러나고 뒤이어 적절한 객체의 상태가 결정된다.
4-1. 협력
협력은 한 사람이 다른 사람에게 도움을 요청할 때 시작된다. 스스로 해결하기 어려운 문제를 해결하기 위해 필요한 지식을 알고있거나 도움을 받을 수 있는 누군가에게 도움을 요청하게 된다. 요청을 받은 사람은 일을 처리한 후 요청한 사람에게 필요한 지식이나 서비스를 제공하는 것으로 요청에 응답한다. 결과적으로 협력은 다수의 요청과 응답으로 구성되며 연쇄적으로 발생한다.
객체를 잘 설계하기 위해선 협력에 내포된 다양한 특성을 알아야한다.
앨리스 이야기
- 누군가가 왕에게 재판을 요청함으로써 재판 시작
- 왕이 하얀 토끼에게 증인을 부를 것을 요청
- 왕의 요청을 받은 토끼는 모자 장수에게 증인석으로 입장할 것을 요청
- 모자 장수는 증인석에 입장함으로써 토끼의 요청에 응답
- 모자 장수의 입장은 왕이 토끼에게 요청했던 증인 호출에 대한 응답이기도 함
- 왕은 모자 장수에게 증언할 것을 요청
- 모자 장수는 자신이 알고 있는 내용을 증언함으로써 왕의 요청에 응답
협력 안의 요청과 응답에 초점을 맞춰보면 누군가에게 요청을 보냈다는 말은 그것을 수행할 의무가 있으며 필요한 지식을 가지고 있음을 의미한다. 이는 협력에 참여하는 객체가 수행할 책임을 정의한다.
4-2. 책임
객체지향에서 요청에 대해 응답할 수 있거나, 적절한 행동을 할 의무가 있다면 해당 객체가 책임을 가진다고 말한다. 책임을 어떻게 구현할지에 대한 고민은 객체와 책임이 자리잡은 후에 진행해도 늦지 않는다.
객체의 책임은 두 가지로 구성된다.
- 객체가 무엇을 알고 있는가(knowing)
- 개인적인 정보에 대해 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
- 객체가 무엇을 할 수 있는가(doing)
- 객체를 생성하거나 계산을 하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
즉, 책임은 객체의 외부에 제공해 줄 수 있는 정보(아는 것의 측면)와 외부에 제공해 줄 수 있는 서비스(하는 것의 측면)의 집합이다. 따라서 책임은 객체의 공용 인터페이스(public interface) 를 구성한다.
협력 안에서 객체는 다른 객체로부터 요청이 전송되었을 경우에만 주어진 책임을 수행하는데, 이렇게 요청을 보내는 것을 메시지 전송 이라고 하고, 객체 간의 협력은 메시지를 통해 이뤄진다. 책임이 협력이라는 문맥 속에서 요청을 받는 객체의 행동을 나열한 것이라면, 메시지는 협력에 참여하는 두 객체 사이의 관계를 강조한 것이다.
4-3. 역할
어떤 객체가 수행하는 책임의 집합은 객체가 협력 안에서 수행하는 역할을 암시한다. 굳이 왕을 판사라고, 모자 장수를 증인이라고 불러서 상황을 복잡하게 만드는 이유는 역할이 재사용 가능하고 유연한 객체지향 설계를 낳는 매우 중요한 구성요소이기 때문이다.
판사: 왕, 여왕
증인: 모자 장수, 요리사, 앨리스
진행자: 하얀 토끼
역할의 개념을 사용하면 유사한 협력을 추상화해서 인지 과부화를 줄일 수 있다. 역할없이 객체별로 관계를 형성했을 때는 재판 과정이 바뀐다면 협력 과정을 모두 수정해야하지만, 역할로 추상화할 경우 하나의 협력으로 해결할 수 있다.
다시 말해 역할은 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있다는 것을 의미하며, 대체 가능한 객체는 동일한 메시지 를 받아 책임을 수행할 수 있어야 한다.
4-4. 객체의 모양을 결정하는 협력
사람들은 시스템에 필요한 데이터를 저장하기 위해 객체가 존재한다는 흔한 오류를 범한다. 데이터는 단지 객체가 행위를 수행하는데 필요한 재료이며, 객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서이다.
또한 객체지향이 클래스와 클래스 간의 관계를 표현하는 시스템의 정적인 측면에 중점을 두는 오류를 범하기도 한다. 중요한 것은 클래스가 아니라 협력에 참여하는 동적인 객체이며, 객체가 협력 안에서 어떤 책임과 역할을 수행할 것인지 결정하는 것이다. 클래스는 단지 구현 메커니즘일 뿐.
협력은 객체들이 주고받는 요청과 응답의 흐름을 결정하여 설계하고, 이렇게 결정된 흐름은 객체가 협력에 참여하기 위해 수행할 책임이 된다. 객체의 책임은 객체가 외부에 제공하게 될 행동으로 이어지며, 객체지향 시스템에서 가장 중요한 것은 충분히 자율적 인 동시에 충분히 협력적 인 객체를 창조하는 것이다.
4-5. 객체지향 설계 기법
- 책임-주도 설계(Responsibility-Driven Design)
- 객체의 책임을 중심으로 시스템을 구축하는 설계 방법을 말한다.
- 시스템의 기능을 더 작은 규모의 책임으로 분할하고 각 책임을 수행할 적절한 객체에 할당한다.
- 객체가 책임을 수행하는 중에 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 다른 객체에게 책임을 할당함으로써 객체 간의 협력 관계가 만들어진다.
- 객체의 개별적인 상태보다 책임과 상호작용에 집중한다.
- 디자인 패턴(Design Pattern)
- 책임-주도 설계는 객체의 역할, 책임, 협력을 고안하기 위한 방법과 절차를 제시하지만 디자인 패턴은 책임-주도 설계의 결과를 표현한다.
- 반복적으로 발생하는 문제와 그 문제에 대한 해법의 쌍으로 정의된다.
- 패턴은 해결하려는 문제를 명확하게 서술하고, 패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 함께 설명한다.
- 디자인 패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿이다.(COMPOSITE 패턴으로 설명)
- Component 역할: 개별 객체와 복합 객체가 공통적으로 수행하는 작업에 대한 책임을 가지고 객체들 간의 일관성을 유지한다.
- Leaf 역할: 공용 인터페이스에 대한 특정 작업(오퍼레이션)에 대한 책임을 수행한다.
- Composite 역할: 자식 객체들을 관리하고 조작하는 책임을 가진다. Composite 객체는 자식 객체들에게 적절한 작업을 위임하고, 자식들의 결과를 취합하여 통합된 결과를 제공하는 역할을 수행한다.
- 테스트-주도 개발(Test-Driven Development)
- 테스트-주도 개발은 책임-주도 설계를 통해 도달해야 하는 목적지를 테스트라는 안정장치를 통해 좀 더 빠르고 견고한 방법으로 도달할 수 있도록 해주는 최상의 설계 프랙티스다.
- 테스트-주도 개발은 테스트를 작성하는 것이 아니라 책임을 수행할 객체 또는 클라이언트가 기대하는 객체의 역할이 메시지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것인지에 대한 기대를 코드의 형태로 작성하는 것이다.
- 테스트를 작성하기 위해 객체의 메서드를 호출하고 반환값을 검증하는 것은 순간적으로 객체가 수행해야하는 책임에 관해 생각한 것이다.
- 테스트에 필요한 간접 입력 값을 제공하기 위해 스텁(stub)을 추가하거나 간접 출력 값을 검증하기 위해 목객체(mock object)를 사용하는 것은 객체와 협력해야 하는 협력자에 관해 고민한 결과를 코드로 표현한 것이다.
[5장] 책임과 메시지
의도는 "메세징"이다. 훌륭하고 성장 가능한 시스템을 만들기 위한 핵심은 모듈 내부의 속성과 행동이 어떤가보다는 모듈이 어떻게 커뮤니케이션하는가에 달려있다.
5-1. 챕터의 시작
1 모집된 사람들이 각자 격리된 방에서 마이크를 통해 한사람씩 이야기를 하고, 모든 사람은 그 내용을 들을 수 있다고 고지함
2 실제실험에서는 참가자 본인 제외하고는 전부 녹음된 내용 재생
3 첫번째 발표한 학생이 발표(녹음)증 간질로 쓰러졌음
4 대다수 아무런 행동을 취하지 않음. 31프로만 행동
5 참가자들은 자신 제외하고도 도와줄 사람이 한명 이상 있을것이라고 판단
6 간질로 쓰러진 사람과 자신만 있다고 판단한 사람은 85퍼센트가 주저하지 않고 도움 청함
달리와 라타네의 실험으로 인해 인간의 책임감 분산에 대해서 알 수 있다.
- 사건에 대한 목적자가 많을 수록, 개인이 느끼는 책임감은 적어진다
- 대부분의 사람들은 명확한 책임이 없을 경우 타인의 책임으로 간주한다
- 책임이 명확할 경우 해결하려고 노력한다
객체의 세계도 마찬가지이다.
훌륭한 객체지향의 세계는 명확하게 정의된 역할과 책임을 지닌 객체들이 상호 협력 하는 세계이며,
역할과 책임이 흐릿할 경우 협력자 찾기 어렵다.
5-2. 자율적인 책임
객체지향 공동체를 구성하는 기본 단위는 ‘자율적’인 객체다. 자율성의 사전적 의미는 ‘자기 스스로의 원칙에 따라 어떤 일을 하거나 자신을 통제해서 절제하는 성질이나 특성’이다. 따라서 자율적인 객체란 스스로 정한 원칙에 따라 판단하고 스스로의 의지를 기반으로 행동하는 객체다. 그리고 객체가 어떤 행동을 하는 유일한 이유는 다른 객체로부터 요청을 수신했기 때문이다.
앨리스 이야기에서 재판을 예시로 자율적인 책임에 대해 설명해보면 다음과 같다.
1. 자율적인 협력
누군가 =======> 왕 ======> 모자장수
재판하라 증언하라
2. 수동적인 협력
누군가 ======> 왕 =================> 모자장수
재판하라 목격했던 장면을 떠올려라
=================> 모자장수
떠오르는 기억을 시간 순서로 재구성해라
=================> 모자장수
말로 간결하게 표현해라
첫 번째 협력의 경우 모자 장수는 왕에게 증언할 책임은 있지만 증언을 위한 구체적인 방법이나 절차에 대해서는 최대한의 자유를 누린다. 왕의 입장에서 모자 장수가 어떤 방법으로 증언하는지는 중요하지 않다는 것이 핵심이다.
두 번째 협력의 경우는 요청의 수준이 상세하다. 모자 장수는 첫 번째 협력에서와 마찬가지로 요청들을 차례대로 처리해야 하는 책임을 지고 있다. 문제는 이 책임들이 모자 장수가 증언하기 위해 선택할 수 있는 자유의 범위를 지나치게 제한한다는 점이다. 상세한 수준의 책임은 증언이라는 협력의 최종 목표는 만족시킬지 몰라도 모자 장수가 누려야 하는 선택의 자유를 크게 훼손하고 만다. 결과적으로 모자 장수는 자율적으로 책임을 수행할 수 없다.
즉, 객체가 자율적이기 위해서는 객체에게 할당되는 책임의 수준 역시 자율적이어야 한다. 그렇다고 너무 자율적이어야 한다는 것에 매몰되어서도 좋지 않다. 협력의 의도를 명확하게 표현하지 못할 정도로 추상적인 것 역시 문제이기 때문이다.
모자 장수가 왕과 협력하기 위해 ‘설명하라’ 라는 책임을 수행해야할 경우 모자 장수는 무엇을 설명해야할지 난감하다. 추상적이고 포괄적인 책임은 협력을 좀 더 다양한 환경에서 재사용할 수 있도록 유연성을 얻게되지만, 동시에 협력에 참여하는 의도를 명확하게 설명할 수 있는 수준을 유지해야 한다. 따라서 ‘증언하라’ 라는 책임이 훌륭한 책임인 이유는 모자 장수의 자율성을 보장할 수 있을 정도로 충분히 추상적인 동시에 협력의 의도를 뚜렷하게 표현할 수 있을 정도로 충분히 구체적이기 때문이다.
5-3. 메시지와 메서드
자율적인 책임의 특징은 객체가 ‘어떻게’ 해야 하는가가 아니라 ‘무엇’을 하는가를 설명한다는 것이다. 사실 책임이라는 말 속에는 어떤 행동을 수행한다는 의미가 포함돼 있다. 객체는 다른 객체로 부터 요청을 수신할 때만 어떤 행동을 시작하기 때문이다. 이러한 요청을 메시지라고 부르며 행동을 수행하게 만드는 유일한 방법이다.
모자 장수가 메세지를 처리하기 위해 내부적으로 선택하는 방법을 메서드라고 한다. 메세지는 ‘어떻게’ 수행될 것인지 명시하지 않으며, ‘무엇’이 실행되기만을 명시하며, 어떤 메서드를 선택할지 전적으로 수신자의 결정에 좌우된다.
메시지와 메서드의 차이와 관계를 이애하고 나면 다형성을 쉽게 이해할 수 있다. 다형성이란 서로 다른 유형의 객체가 동일한 메시지에 대해 서로 다르게 반응하는 것을 의미한다. 좀 더 구체적으로 말해 서로 다른 타입에 속하는 객체들이 동일한 메시지를신할 경우 서로 다른 메서드를 이용해 메시지를 처리할 수 있는 매커니즘을 가리킨다.
다형성은 역할, 책임, 협력과 깊은 관련이 있다. 서로 다른 객체들이 다형성을 만족시킨다는 것은 객체들이 동일한 책임을 공유한다는 것을 의미한다. 다형성에서 중요한 것은 메시지 송신자의 관점이다. 수신자들이 동일한 오퍼레이션을 서로 다른 방식으로 처리하더라도 송신자의 관점에서 이 객체들은 동일한 책임을 수행하는 것이다. 즉 송신자의 관점에서 다형적인 수신자들을 구별할 필요가 없다. 이는 결국 동일한 역할을 수행할 수 있는 객체들 사이의 대체 가능함을 의미한다.
유연하고 확장 가능하고 재사용성이 높은 협력의 의미
송신자가 수신자에 대해 매우 작은 정보(메세지)만 알고 있어도 협력이 가능하다는 것은
1 협력이 유연해진다. 2 협력이 수행되는 방식을 확장할 수 있다. 3 협력이 수행되는 방식을 재사용 할 수 있다.
을 의미한다.
5-4. 메시지를 따라라
객체지향 어플리케이션을 클래스의 집합이라고 생각하고, 클래스에 집중하지만 객체지향의 강력함은 클래스가 아니라 객체들이 주고받는 ‘메시지’로 부터 나온다. 클래스를 정의하는 것이 먼저가 아니라, 객체들의 속성과 행위를 식별 하는 것이 먼저다.
메시지가 아니라 데이터를 중심으로 객체를 설계하는 방식은 객체의 내부 구조를 객체 정의의 일부로 만들기 때문에 객체의 자율성을 저해한다.
훌륭한 객체지향 설계는 어떤 객체가 어떤 메세지를 전송할 수 있는가와 어떤 객체가 어떤 메세지를 이해할 수 있는가를 중심으로 객체 사이의 협력관계를 구성하는 것이다.
책임-주도 설계의 핵심은 어떤 행위가 필요한지를 먼저 결정한 후에 이 행위를 수행할 객체를 결정하는 것이다. 이 과정을 흔히 What/Who 사이클이라고 한다. 이 용어가 의미하는 것은 객체 사이의 협력 관계를 설계하기 위해서는 먼저 ‘어떤 행위’를 수행할 것인지를 결정한 후에 ‘누가’ 그 행위를 수행할 것인지를 결정해야 한다는 것이다.
객체의 행위를 결정하는 것은 객체 자체의 속성이 아니라는 점에 주목해야 한다. 이 설계에서는 어떤 객체가 어떤 특성을 가지고 있다고 해서 반드시 그와 관련된 행위를 수행할 것이라고 가정하지 않는다.
5-5. 객체 인터페이스
인터페이스의 특징은 다음과 같다.
- 인터페이스의 사용법만 알고 있으면 대상의 내부 구조나 동작 방법을 몰라도 상호작용이 가능하다
- 인터페이스가 변경되지 않고 단순히 내부 구성이나 작동 방식이 변경되는 것은 인터페이스 사용자에게 아무런 영향도 미치지 않는다.
- 인터페이스가 동일하기만 하다면 어떤 대상과도 상호작용할 수 있다.
공용 인터페이스는 내부에서만 접근 가능한 사적인 인터페이스와 구분하기 위해, 외부에 공개된 인터페이스를 의미하며 객체가 협력에 참여하기 위해 수행하는 메세지가 객체의 공용 인터페이스의 모양을 암시한다.
5-6. 인터페이스와 구현의 분리
맷 와이스펠드의 객체지향적 사고 방식 이해를 위한 3원칙은 아래와 같다.
- 좀 더 추상적인 인터페이스 (너무 구체 X)
- 최소 인터페이스 (필요 없는거 최대한 노출 X)
- 인터페이스의 구현과 차이가 있다
객체지향에서 내부 구조와 작동 방식을 가리키는 용어는 구현(implementation)이다. 객체를 구성하지만 공용 인터페이스에 포함되지 않는 모든 것이 구현에 포함된다. 객체의 외부와 내부를 분리하라는 것은 결국 객체의 공용 인터페이스와 구현을 명확하게 분리하라는 말과 동일하다.
훌륭한 객체란 구현을 모른 채 인터페이스만 알면 쉽게 상호작용할 수 있는 객체를 의미한다. 공용 인터페이스와 내부 구현을 분리해서 고려해야한다는 것을 의미하며, 이를 인터페이스와 구현의 분리 원칙이라고 한다.
이 원칙이 중요한 이유는 다음과 같다.
- 소프트웨어는 항상 변경된다.
- 변경에 대한 파급 효화 (부수 효과) 발생
- 변경에 대한 안전지대가 필요하다.
- 자율적인 객체가 되는데에도 기여
객체의 자율성을 보존하기 위해 구현을 외부로부터 감추는 것을 캡슐화라고 한다. 객체지향의 세계에서 캡슐화는 두가지 관점에서 사용한다.
- 상태와 행위에 캡슐화
- 객체는 스스로 상태를 관리하며 상태를 변경하고 외부에 응답할 수 있는 행동을 내부와 함께 보관한다.
- 데이터 캡슐화라고도 한다.
- 객체가 자율적이기 위해서 자기 자신의 상태를 스스로 관리 할 수 있어야 하기 때문에 데이터 캡슐화는 자율적인 객체를 만들기 위한 전제 조건이다.
- 사적인 비밀의 캡슐화
- 캡슐화를 통해 변경이 빈번하게 일어나는 불안정한 비밀을 안정적인 인터페이스 뒤로 숨길 수 있다.
- 개인적인 비밀을 공용 인터페이스 뒤에 감춤으로써 외부의 불필요한 공격과 간섭으로 부터 내부 상태를 격리 할 수 있다. => 따라서 공용 인터페이스를 경계로 최대한의 자율성을 보장 받는다.
5-7. 책임의 자율성이 협력의 품질을 결정한다
- 자율적인 책임은 협력을 단순하게 만든다
- 자율적인 책임은 모자 장수의 외부와 내부를 명확하게 분리한다
- 책임이 자율적인 경우 책임을 수행하는 내부적인 방법을 변경하더라도 외부에 영향을 미치지 않는다
- 자율적인 책임은 협력의 대상을 다양하게 선택할 수 있는 유연성 제공 한다
- 객체가 수행하는 책임들이 자율적일수록 객체의 역할을 이해하기 쉬워진다
DONE.