# 들어가며
본 글은 객체지향의 사실과 오해: 역할, 책임, 협력 관점에서 본 객체지향 (opens new window)의 챕터 3~4를 읽고 정리한 내용입니다. 이전 글에서 이어집니다.
# 3. 타입과 추상화
# 3-1. 추상화를 통한 복잡성 극복
초기 지하철 노선도는 굳이 지형 정보를 반영하여 역에 대한 정보를 되려 확인하기가 어려운 모습이었다. 지하철 노선도가 개선된 모델은 형태가 아닌 목적에 집중하여 지형 정보를 전혀 반영하지 않은 채로 역과 역 사이 방향과 간단한 정보 표기에 집중했다.
추상화의 목적은 불필요한 부분을 무시함으로써 현실에 존재하는 복잡성을 극복하는 것이다. 지하철 노선도 예시에서 알 수 있듯 훌륭한 추상화는 목적에 부합하는 것이다. 추상화의 수준, 이익, 가치는 목적에 의존적이다. 버릴게 무엇인지 정의해야한다.
복잡성을 다루기 위해 추상화는 두 차원에서 이루어진다.
- 구체적인 사물들 간의 공통점은 취하고 차이점은 버린다.
- 중요한 부분 강조를 위해 불필요한 세부 사항을 제거한다.
# 3-2. 개념
앨리스는 이야기 속에 등장하는 정원사, 병사, 신하, 공주 등의 인물들을 '기껏해야 트럼프에 불과해'
라고 단정짓는다. 트럼프 그룹에 속하는 인물들은 각자 객체로서 분명한 차이점들을 지니지만, 이들을 트럼프 그룹안에 소속시키게 되는데, 이것이 바로 추상화를 시도한 것이다.
이때 공동점을 기반으로 객체들을 묶기 위한 그릇을 개념(concept)이라고 한다. 개념을 이용하면 객체를 여러 그룹으로 분류(classification)할 수 있다.
객체에 어떤 개념이 적용 가능해져서 개념 그룹의 일원이 될때, 객체를 그 개념의 인스턴스(instance)라고 한다.
# 3-3. 개념의 세 가지 관점
어떤 객체에 어떤 개념이 적용됐다고 할 때는 그 개념이 부가하는 의미를 만족시켜 다른 객체와 함께 해당 개념의 일원이 되었다는 것을 의미한다.
객체의 분류 장치로서 개념을 이야기 할 때는 아래 세 가지 관점을 함께 언급한다.
- 심볼(symbol): 개념을 가리키는 간략한 이름 혹은 명칭, 예화에서
트럼프
에 해당 - 내연(intension): 개념의 완전한 정의를 나타냄. 내연의 의미를 이용해 객체가 개념에 속하는지 여부를 판단. 예화에서 트럼프라는 특성에 대한 설명에 해당 (몸이 종이처럼 펄럭거리고 ..)
- 외연(extension): 개념에 속하는 모든 객체의 집합.
여왕
,정원사
등등
개념을 구성하는 요소가 심볼, 내연, 외연이라는 것보다 내연을 통해 객체를 분류한다는 아이디어가 가장 중요하다.
# 3-4. 객체를 분류하기 위한 틀
분류는 객체지향의 가장 중요한 개념 중 하나이며, 어떤 객체를 어떤 개념으로 분류할지가 객체지향의 품질을 결정한다.
개념을 통해 객체를 분류하는 과정은 위에 소개된 추상화의 두 차원을 모두 차용한다. 예화 속 인물들을 트럼프로 분류한 것은 첫 번째 차원을, 트럼프라는 개념이 설명하는 몸의 특징 외에 다른 특징들은 굳이 조명하지 않은 것이 두 번째 차원을 의미한다.
# 3-5. 타입
타입의 정의는 개념의 정의와 완전히 동일하다. 데이터 타입을 논할 때에 정수형 데이터, 불리언 데이터 등을 타입으로 분류하게 된다. 이러한 타입 시스템은 두 가지 중요한 사실을 알려준다.
- 타입은 데이터가 어떻게 사용되느냐에 관한 것이다. 어떤 데이터에 어떤 연산자를 적용할 수 있느냐가 데이터 타입을 결정한다.
- 타입에 속한 데이터를 메모리에 어떻게 표현하는지는 외부로부터 철저히 감춰진다.
객체를 바라볼때는 상태, 즉 데이터 중심으로 보는 것이 아니라 행동 중심으로 봐야하지만 데이터 타입의 두 사실은 객체 정의에 중요한 점을 지적한다.
- 어떤 객체가 어떤 타입에 속하는지를 결정하는 것은 객체가 수행하는 행동이다. 동일한 행동들을 수행하는 객체는 동일한 타입이다. (동일한 객체가 아니라는 사실을 기억!)
- 객체의 내부적인 표현은 외부로부터 감춰진다.
# 3-6. 행동이 우선이다
두 번째 사실에 따라 객체 타입은 내부 표현과는 아무런 상관이 없고 어떤 행동을 하느냐에 따라 객체 타입이 결정된다. 즉 동일한 책임을 수행하는 객체들은 동일한 타입에 속한다고 말할 수 있다.
같은 타입에 속한 객체들은 동일한 행동을 하며, 동일한 책임을 가지며, 따라서 동일한 메세지 수신을 의미한다. 중요한 점은 수신하는 메세지가 동일하더라도 메세지를 처리하는 방법은 객체마다 달라질 수 있다는 것이다. 이것이 다형성에 의미를 부여한다.
다형성은 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력을 뜻한다. 외부에 행동만을 제공하고 데이터는 행동 뒤로 감추는 것을 캡슐화 원칙이라고 한다.
# 3-7. 개념의 일반화 / 특수화
앨리스 예화에서 트럼프라는 개념이 있고 내부에 트럼프 인간이라는 범주로 부분집합을 정의할 수 있다. 트럼프 특성을 만족하면서 걸어다니는 대상을 트럼프 인간이라고 새로 정의한 것이다.
트럼프보다 좀 더 특화된 행동을 하는 특수한 개념을 새로 정의할 수 있는데, 이때 두 개념 사이의 관계를 일반화 / 특수화 관계라고 한다.
이때 중요한 것은 객체지향에서 일반화 / 특수화 관계를 결정하는 것은 객체 상태를 표현하는 데이터가 아니라 행동이다. 어떤 객체가 다른 객체보다 더 일반적이거나 특수한 상태를 표현한다고 해서 두 객체가 속하는 타입 간 일반화 / 특수화 관계가 성립하는 것이 아니다.
한 객체의 타입이 다른 타입의 객체보다 더 특수하게 행동하거나, 더 일반적으로 행동해야 위의 관계가 성립된다. 객체가 외부에 제공하는 행동에 집중해야 한다.
일반적인 타입은 특수한 타입보다 더 적은 수의 행동을 가지고 특수한 타입은 일반적인 타입보다 더 많은 수의 행동을 가진다.
이때 외연을 의미하는 집합 내 개체수는 내연을 의미하는 행동 가짓수에 반대된다.
# 3-8. 타입의 목적
타입을 사용하는 이유는 시간에 따라 동적으로 변하는 객체의 복잡성을 극복하기가 너무 어렵기 때문이다.
앨리스라는 타입을 만들고 속성에 키와 행동을 정의하면 시시각각 변하는 앨리스의 키에 따라 다른 객체들을 추상화 할 수 있게 된다. 즉 시간이라는 요소를 제거하여 시간에 독립적인 정적인 모습으로 앨리스를 생각할 수 있게 된다.
객체를 생각할 때에는 두 가지 모델을 동시에 고려한다.
- 객체가 특정 시점에 구체적으로 어떤 상태를 가지는가? (객체 스냅샷, 동적 모델) - 런타임에 객체 상태 변경을 추적하고 디버깅하는 것. 앨리스의 키 변화를 포착하는 것
- 객체가 가질 수 있는 모든 상태와 모든 행동을 시간에 독립적이게 표현 (타입 모델, 정적 모델) - 클래스를 작성할때
클래스 != 타입
객체지향 프로그래밍 언어에서 정적인 모델은 클래스를 이용해 구현된다. 클래스는 타입과 동일하지 않다. 타입은 객체를 분류하기 위해 사용하는 개념이며 클래스는 타입이라는 개념을 구현하기 위한 메커니즘 중 하나이다.
# 4. 역할, 책임, 협력
객체 세계에서는 협력이라는 문맥이 객체의 행동 방식을 결정한다. 객체지향에 입문한 이들의 흔한 실수는 협력이라는 문맥을 고려하지 않은 채 객체가 가져야 할 상태와 행동부터 고민하기 시작한다는 것이다.
협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성된다. 요청과 응답은 협력에 참여하는 객체가 수행할 책임을 정의한다. 객체지향의 세계에서는 어떤 객체가 어떤 요청에 대해 대답해 줄 수 있거나, 적절한 행동을 할 의무가 있는 경우 해당 객체가 책임을 가진다고 말한다.
책임은 객체지향 설계의 가장 중요한 재료이다. 객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것이라고 말한다.
# 4-1. 책임의 분류
책임은 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 무엇을 알고 있는가
와 객체가 무엇을 할 수 있는가
로 구성된다.
- 하는 것(doing)
- 객체를 생성하거나 계산하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
- 아는 것(knowing)
- 개인적인 정보에 관해 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
책임은 외부에 제공해줄 수 있는 정보(아는 것)와 외부에 제공해줄 수 있는 서비스(하는 것)의 목록이다. 따라서 책임은 객체의 공용 인터페이스를 구성한다.
# 4-2. 책임과 메시지
책임이 요청을 수신하는 한 쪽의 객체 관점에서 무엇을 할 수 있는지를 나열하는 것이라면, 메시지는 협력에 참여하는 두 객체 사이의 관계를 강조한 것이다.
주의할 점은 책임과 메시지의 수준이 같지는 않다는 점이다. 책임은 객체가 협력에 참여하기 위해 수행해야 하는 행위를 상위 수준에서 개략적으로 서술한 것이다. 책임 결정 후 실제로 협력을 정제하면서 메시지로 변환할 때는 하나의 책임이 여러 메시지로 분할되는 것이 일반적이다.
설계 시작 초반에는 어떤 객체가 어떤 책임을 가지고 어떤 방식으로 서로 협력해야 하는지에 대한 개요를 아는 것만으로도 충분하다. 책임과 협력의 구조가 자리를 잡기 전까지는 책임 구현 방법에 대한 고민은 미뤄두는 것이 좋다. (인터페이스 정의가 선행되어야 한다는 의미)
# 4-3. 책임의 집합이 의미하는 것
어떤 객체가 수행하는 책임의 집합은 객체가 협력 안에서 수행하는 역할을 암시한다. 앨리스 예화에서 왕이라는 객체를 판사라고 부르고 모자 장수를 증인이라고 부르는 이유는, 역할이 재사용 가능하고 유연한 객체지향 설계를 낳는 매우 중요한 구성요소이기 때문이다.
예화 속 증인으로 서게 되는 객체들은 서로 다르다. 앨리스 객체, 모자 장수 객체, 요리사 객체는 모두 다른 타입이지만 새로운 역할을 할당받게 된다. 받은 역할이 동일하기 때문에 재판이라는 협력 문맥 속에서는 다른 객체더라도 문맥 속 행동은 유사하게 이루어진다.
어떤 증언을 하게 되는지는 증언마다 달라지므로 캡슐화까지 이루어졌다고 볼 수 있겠다.
역할은 협력 내에서 다른 객체로 대체할 수 있음을 나타내는 일종의 표식이다. 물론 어떤 객체라도 역할을 대체할 수 있는 것은 아니며 동일한 메시지를 이해할 수 있는 객체로 한정된다.
역할의 개념을 사용하면 유사한 협력을 추상화해서 인지 과부하를 줄일 수 있다. 다양한 객체들이 협력에 참여할 수 있기 때문에 협력이 유연해지며 다양한 객체들이 동일한 협력에 참여할 수 있기 때문에 재사용성이 높아진다.
역할은 객체지향 설계의 **단순성(simplicity), 유연성(flexibility), 재사용성(reusability)**을 뒷받침하는 핵심 개념이다. 역할을 이용하면 협력을 추상화함으로써 단순화할 수 있다.
# 4-4. 대체 가능성
역할은 다른 객체에 의해 대체 가능함을 의미하는데, 역할이 대체 가능하기 위해서는 행동이 호환되어야 한다는 점에 주목해야 한다. 어떤 객체가 증인이라는 역할을 대체할 수 있는 이유는 그 객체가 증인석에 입장할 수 있고 증언할 수 있기 때문이다.
결국 객체는 협력 안에서 역할이 수행할 수 있는 행동을 그대로 수행할 수 있어야 한다. 이때 객체는 역할에 주어진 책임 이외에 다른 책임을 수행할 수도 있다는 사실에 또 주목해야 한다.
객체는 역할이 암시하는 책임보다 더 많은 책임을 가질 수 있다. 모자 장수는 증언 외에 모자를 판매하는 본질적인 역할을 갖는 것처럼!
따라서 대부분의 경우 객체의 타입과 역할 사이에는 일반화 / 특수화 관계가 성립하는 것이 일반적이다.
# 4-5. 객체지향 설계 기법
올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 한다. 협력을 설계한다는 것은 협력에 참여하는 객체들이 주고받을 응답의 흐름을 결정한다는 것을 의미한다.
객체에 책임을 할당하고 나면 책임은 객체가 외부에 제공하게 될 행동이 된다. 행동을 결정한 뒤 행동을 수행하는 데 필요한 데이터를 고민하고, 데이터와 행동이 어느정도 결정된 후 클래스 구현 방법을 결정해야 한다.
책임-주도 설계(Responsibility-Driven Design) 방법은 협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 앱을 설계한다.
디자인 패턴(Design Pattern)은 전문가들이 반복적으로 사용하는 해결 방법을 정의해 놓은 설계 템플릿의 모음이다. 특정 문제를 해결하기 위해 이미 식별해 놓은 역할, 책임, 협력의 모음이다.
테스트-주도(Test-Driven Development) 개발은 테스트를 먼저 작성하고 테스트를 통과하는 구체적인 코드를 추가하면서 애플리케이션을 완성해가는 방식을 따른다. TDD의 핵심은 테스트 작성이 아니며, 코드를 작성해가며 역할, 책임, 협력을 식별하고 식별된 역할, 협력이 적합한지를 피드백받는 것이다.
# 4-6. 책임-주도 설계
책임-주도 설계는 말 그대로 객체의 책임을 중심으로 시스템을 구축하는 설계 방법을 말한다. 시스템 기능은 더 작은 규모의 책임으로 분할되고 각 책임은 책임을 수행할 적절한 객체에게 할당된다.
책임 수행 과정에서 스스로 처리할 수 없는 정보나 기능이 필요한 경우 적절한 객체를 찾아 필요한 작업을 요청한다. 요청된 작업을 수행하는 일은 작업을 위임받은 객체의 책임으로 변환된다.
만약 책임을 여러 종류의 객체가 수행할 수 있다면 협력자는 객체가 아니라 추상적인 역할로 대체된다.
# 4-7. 디자인 패턴
책임-주도 설계는 객체의 역할, 책임, 협력을 고안하기 위한 방법과 절차를 제시하는 것이라면, 디자인 패턴은 책임-주도 설계의 결과를 표현한다. 디자인 패턴 중 컴포지트 패턴을 예시로 보자.
컴포지트 패턴은 클라이언트가 복합 객체(group of object) 나 단일 객체를 동일하게 취급하는 것을 목적으로 한다. 여기서 컴포지트의 의도는 트리 구조로 작성하여, 전체-부분(whole-part) 관계를 표현하는 것이다.
- 클라이언트는 컴포넌트에게 메시지를 요청하여 협력한다.
- 컴포넌트는 다른 컴포넌트를 추가, 삭제, 얻어내는 책임을 갖는다.
- 리프(Leaf)는 오퍼레이션 호출에 응답할 수 있는 기본 행위를 구현한다.
컴포지트가 리프와 또 다른 컴포지트를 갖는 형태이다. 디렉토리를 생각하면 된다. 디렉토리는 또 다른 디렉토리를 갖거나 파일을 갖는다.
내부 구성이나 구현이 중요한 것이 아니라, 협력관계가 어떻게 이루어지는지가 중요하다.
# 4-8. 테스트-주도 개발
TDD는 응집도가 높고 결합도가 낮은 클래스로 구성된 시스템을 개발할 수 있게 하는 최상의 프랙티스이지만 테스트 작성이 어렵다. TDD는 객체가 이미 존재한다고 가정하고 객체에게 어떤 메시지를 전송할 것인지에 관해 먼저 생각해야 한다.
TDD는 테스트를 작성하는 것이 아니라 책임을 수행할 객체 또는 클라이언트가 기대하는 객체의 역할이 메시지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것인지에 대한 기대를 코드로 작성하는 것이다.