객체지향의 사실과 오해 - 역할, 책임, 협력 관점에서 본 객체지향
객체지향의 사실과 오해 : 네이버 도서
네이버 도서 상세정보를 제공합니다.
search.shopping.naver.com
책 제목을 보고 내가 객체지향에 대해 얼마나 잘 알고 있나 생각했다. 남들에게 잘 설명할 수 있는가? 객체지향을 잘 쓰고 있다고 자부할 수 있나? 완전 자신감있게 잘 알고 있다고 자부할 수준은 아닌 것 같았다. 그렇다면 내가 뭘 잘못 알고 있는걸까? 어떻게 써야 더 잘 쓰는걸까? 이런 생각에서 이 책을 읽기 시작했다.
사실 객체지향도 디자인패턴도 어느정도 잘 알고는 있지만 업무에서는 적절하게 일부만 응용해서 쓰는 수준이다. 못쓰고 모르는건 아니다. 잘 쓰고 있다고 생각은 하지만 아직도 개발하다가 어떤 식으로 개발해야 더 좋을지 매번 고민한다. 여기서는 상속을 써야할까 인터페이스를 써야할까, 여기서는 팩토리를 쓸 필요가 있을까.. 등등 매번 고민한다.
이 책에서는 사실 이런 내용들을 주로 다루고 있지는 않다. '객체지향을 어떤 방식으로 접근해야 하는가', '어떤 방식으로 이해해야 하는가'와 같은 내용들을 다루고 있다. 그러다보니 내가 원하는 답 자체를 얻지는 못했다. 그럼에도 좋은 인사이트를 얻은 것 같다.
이 책에서 말하는 것은 객체지향을 '객체' 중심으로 바라보지 말라는 것이다. 객체지향은 '객체'가 먼저가 아니라 '책임'이 먼저라고 말한다. 즉, RDD(Responsibility-Driven Design)으로 접근해야 한다는 것이다. 그러면서 RDD와 DDD가 섞여야 한다고 말하기도 한다. 객체지향은 RDD 기반으로 책임을 먼저 디자인하고, 적절한 역할에게 협력을 요청하도록 해야한다고 한다. 그러면서, 그 역할은 DDD 기반으로 도메인 용어와 언어에 맞춰서 디자인하다보면 자연스럽게 나눌 수 있다고 말한다.
DDD에 대해서는 익히 들어봤으나 RDD에 대해서는 생각해보지 않았다. 뭐든 큰 차이 없는 것이라고 생각했었다. 근데 이렇게 책에서 말하는 대로 따라가다보니 RDD와 DDD가 기존에 객체지향을 교육하는 것과 어떤 것이 다른지, 어째서 RDD로 생각을 해야하는지 등을 느낄 수 있었다. 다만, 이 책에서 말했듯이 기존 교육방식은 객체지향을 공부하기에 좋은 방식인 것에는 변함이 없다.
아래는 또 내용 정리한 것이다.
객체지향에는 많은 사람들이 동의하는 공통적인 특징이 있습니다. 추상화, 캡슐화, 상속, 다형성은 객체지향을 다른 패러당미과 구분하는 중요한 특징입니다. SOLID로 대표되는 몇 가지 원칙들을 사용하면 훌륭한 객체지향 코드를 작성할 수 있다는 사실에 반박하는 개발자들은 그다지 많지 않습니다. 반복적으로 발생하는 문제와 해법의 쌍을 일컫는 디자인 패턴 역시 많은 객체지향 프로그래머들이 유용하다고 생각하는 도구 중 하나입니다. 그래서 객체지향이란 무엇일까요? 물론 이 질문에 정답이란 존재하지 않습니다. 중요한 것은 여러분이 이 질문에 대한 자신만의 견해를 가지고 있느냐는 것입니다.
안타깝게도 대부분의 사람들은 여전히 객체지향이랑 무엇인가에 대해 자신있게 대답하지 못하는 것 같습니다. 실제로 상속이 무엇인지는 대답할 수 있지만 일반화가 무엇인지에 대해서는 대답하지 못하는 개발자분들이 꽤 많습니다. 객체지향이 말 그대로 객체를 지향한다는 사실을 잘 알고 있으면서도 많은 분들이 여전히 클래스나 상속을 중심으로 객체지향을 바라보고 있습니다.
이 책은 객체지향이란 무엇인가라는 원론적이면서도 다소 위험스러운 질문에 대한 제 나름의 대답을 말씀드리기 위해 쓰여졌습니다. 객체지향으로 향하는 첫 걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작합니다. 객체지향으로 향하는 두 번째 걸음은 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 존재로 바라보는 것입니다. 세 번째 걸음을 내디딜 수 있는지 여부는 협력에 참여하는 객체들에게 얼마나 적절한 역할과 책임을 부여할 수 있느냐에 달려 있습니다. 객체지향의 마지막 걸음은 앞에서 설명한 개념들을 여러분이 사용하는 프로그래밍 언어라는 틀에 흐트러짐 없이 담아낼 수 있는 기술을 익히는 것입니다.
01. 협력하는 객체들의 공동체
객체지향이라고 불리는 새로운 세상의 문을 연 대부분의 사람들은 "객체지향이란 실세계를 직접적이고 직관적으로 모델링할 수 있는 패러다임"이라는 설명과 마주하게 된다. 이런 식의 설명이 전달하고자 하는 핵심은 객체지향 프로그래밍이란 현실 속에 존재하는 사물을 최대한 유사하게 모방해 소프트웨어 내부로 옮겨오는 작업이기 때문에 그 결과물인 객체지향 소프트웨어는 실세계의 투영이며, 객체란 현실 세계에 존재하는 사물에 대한 추상화라는 것이다.
아쉽게도 실세계의 모방이라는 개념은 객체지향의 기반을 이루는 철학적인 개념을 설명하는 데는 적합하지만 유연하고 실용적인 관점에서 객체지향 분석, 설계를 설명하기에는 적합하지 않다. 애플리케이션을 개발하면서 객체에 직접적으로 대응되는 실세계의 사물을 발견할 확률은 그다지 높지 않다. 비록 그런 객체가 존재한다고 하더라도 객체와 사물 간의 개념적 거리는 유사성을 차직 어려울 정도로 매우 먼 것이 일반적이다.
객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것이다. 소프트웨어 개발자의 역할은 단순히 실세계를 소프트웨어 안으로 옮겨 담는 것이 아니라 고객과 사용자를 만족시킬 수 있는 신세계를 창조하는 것이다. 버트란드 마이어는 "소프트웨어 시스템이 해결하려고 하는 실재는 잘해봐야 먼 친척밖에는 되지 않는다"는 말로 소프트웨어의 세계와 실세계 사이의 거리를 잘 표현하고 있다.
실세계의 모방이라는 개념이 비현실적임에도 여전히 많은 사람들이 실세계 객체와 소프트웨어 객체 간의 대응이라는 과거의 유산을 반복적으로 재생산하는 이유는 뭘까? 그것은 실세계에 대한 비유가 객체지향의 다양한 측면을 이해하고 학습하는 데 매우 효과적이기 때문이다.
객체를 스스로 생각하고 스스로 결정하는 현실 세계의 생명체에 비유하는 것은 상태와 행위를 '캡슐화(encapsulation)'하는 소프트웨어 객체의 '자율성(autonomous)'을 설명하는 데 효과적이다. 현실 세계의 사람들이 암묵적인 약속과 명시적인 계약을 과정은 협력하며 목표를 달성해 나가는 과정은 '메시지(message)'를 주고받으며 공동의 목표를 달성하기 위해 '협력(collaboration)'하는 객체들의 관계를 설명하는 데 적합하다. 실세계의 사물을 기반으로 소프트웨어 객체를 식별하고 구현까지 이어간다는 개념은 객체지향 설계의 핵심 사상인 '연결완전성(seamlessness)'을 설명하는 데 적합한 틀을 제공한다.
협력하는 사람들
카페 예시를 보면 책임, 역할, 협력을 알 수 있다. 카페에서 모든 과정 속에는 손님, 캐시어, 바리스타 사이의 암묵적인 협력 관계가 존재한다.
따뜻한 커피와 함께할 수 있는 소박한 아침 시간의 여유를 누릴 수 있는 이유는 커피를 주문하는 손님, 주문을 받는 캐시어, 커피를 제조하는 바리스타라는 역할이 존재하기 때문이다.
손님, 캐시어, 바리스타는 주문한 커피를 손님에게 제공하기 위해 협력하는 과정에서 자신이 맡은 바 책임을 다한다. 손님은 카페인을 채우기 위해 커피를 주문할 책임을 수행한다. 캐시어는 손님의 주문을 받는 책임을 성실히 수행한다. 바리스타는 주문된 커피를 제조하는 책임을 수행한다.
커피 주문이라는 협력에 참여한느 모든 사람들은 커피가 정확하게 주문되고 주문된 커피가 손님에게 정확하게 전달될 수 있도록 맡은 바 역할과 책임을 다하고 있는 것이다.
앞에서 사람이라는 단어를 객체로, 에이전트의 요청을 메시지로, 에이전트가 요청을 처리하는 방법을 메서드로 바꾸면 마법처럼 대부분의 설명을 객체지향이라는 문맥으로 옮겨올 수 있다. 이것이 바로 많은 사람들이 객체지향을 설명하기 위해 실세계의 모방이라는 은유를 차용하는 이유다.
과거의 전통적인 개발 방법은 데이터와 프로세스를 엄격하게 구분한다. 이에 반해 객체지향에서는 데이터와 프로세스를 객체라는 하나의 틀 안에 함께 묶어 놓음으로써 객체의 자율성을 보장한다. 이것이 전통적인 개발 방법과 객체지향을 구분 짓는 가방 핵심적인 차이다. 자율적인 객체로 구성된 공동체는 유지보수가 쉽고 재사용이 용이한 시스템을 구축할 수 있는 가능성을 제시한다.
어떤 객체에게 메시지를 전송하면 결과적으로 메시지에 대응되는 특정 메서드가 실행된다. 메시지를 수신한 객체가 실행 시간에 메서드를 선택할 수 있다는 점은 다른 프로그래밍 언어와 객체지향 프로그래밍 언어를 구분 짓는 핵심적인 특징 중 하나다. 이것은 프로시저 호출에 대한 실행 코드를 컴파일 시간에 결정하는 절차적인 언어와 확연히 구분되는 특징이다.
메시지와 메서드의 분리는 객체의 협력에 참여하는 객체들 간의 자율성을 증진시킨다.
협력 속에 사는 객체
외부의 요청이 무엇인지를 표현하는 메시지와 요청을 처리하기 위한 구체적인 방법이 메서드를 분리하는 것은 객체의 자율성을 높이는 핵심 메커니즘이다. 이것은 캡슐화(encapsulation)라는 개념과도 깊이 관련돼 있다.
객체지향의 본질
클래스가 객체지향 프로그래밍 언어의 관점에서 매우 중요한 구성요소(construct)인 것은 분명하지만 객체지향의 핵심을 이루는 중심 개념이라고 말하기에는 무리가 있다. 자바스크립트 같은 프로토타입(prototype) 기반의 객체지향 언어에서는 클래스가 존재하지 않으며 오직 객체만이 존재한다. 프로토타입 기반의 객체지향 언어에서는 상속 역시 클래스가 아닌 객체 간의 위임(delegation) 메커니즘을 기반으로 한다. 지나치게 클래스를 강조하는 프로그래밍 언어적인 관점은 객체의 캡슐화를 저해하고 클래스를 서로 강하게 결합시킨다. 애플리케이션을 협력하는 객체들의 공동체가 아닌 클래스로 구성된 설계도로 보는 관점은 유연하고 확장 가능한 애플리케이션의 구축을 방해한다.
훌륭한 객체지향 설계자가 되기 위해 거쳐야 할 첫 번째 도전은 코드를 담는 클래스의 관점에서 메시지를 주고받는 객체의 관점으로 사고의 중심을 전환하는 것이다. 중요한 것은 어떤 클래스가 필요한가가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는가다. 클래스는 객체들의 협력 관계를 코드로ㅜ 옮기는 도구에 불과하다. 클래스는 객체지향 세계의 도시전설이다.
널리 알려진 미신과 달리 객체지향의 핵심은 클래스가 아니다. 핵심은 적절한 책임을 수행하는 역할 간의 유연하고 견고한 협력 관계를 구축하는 것이다. 클래스는 협력에 참여하는 객체를 만드는 데 필요한 구현 메커니즘일 뿐이다. 클래스가 중요하지 않다는 것이 아니라 단지 협력 구조와 책임을 식별하는 것에 비해 상대적으로 덜 중요하다는 것을 말하고 싶은 것이다. 객체지향의 중심에는 클래스가 아니라 객체가 위치하며, 중요한 것은 클래스들의 정적인 관계가 아니라 메시지를 주고받는 객체들의 동적인 관계다.
클래스의 구조와 메서드가 아니라 객체의 역할, 책임, 협력에 집중하라. 객체지향은 객체를 지향하는 것이지 클래스를 지향하는 것이 아니다.
02. 이상한 나라의 객체
객체, 그리고 소프트웨어 나라
객체란 식별 가능한 개체 또는 사물이다. 객체는 자동차처럼 만질 수 있는 구체적인 사물일 수도 있고, 시간처럼 추상적인 개념일 수도 있다. 객체는 구별 가능한 식별자, 특징적인 행동, 변경 가능한 상태를 가진다. 소프트웨어 안에서 객체는 저장된 상태와 실행 가능한 코드를 통해 구현된다.
객체가 주변 환경과의 상호작용에 어떻게 반응하는가는 그 시점까지 객체에 어떤 일이 발생했느냐에 좌우된다. 여행을 위해 비행기를 이용하려면 탑승 전에 항공권을 발권해야 한다. 항공권을 발권해 놓았다면 아늑한 비행기 좌석에 앉을 수 있겠지만 발권하지 않았다면 떠나는 비행기의 모습을 하릴없이 쳐다볼 수 밖에 없을 것이다. 비행기 탑승 여부는 과거에 항공권을 발권하는 행동이 발생했는지 여부에 따라 달라지는 것이다.
일상생활 속에서 좀 더 다양한 예를 찾아볼 수 있다. ... 예로 든 모든 일들의 공통점은 어떤 행동의 결과는 과거에 어떤 행동들이 일어났었느냐에 의존한다는 것이다.
세상에 존재하는 모든 것들이 객체인 것은 아니다. 분명하게 인식할 수 있음에도 객체의 영역에 포함시킬 수 없는 것들도 존재한다. 앨리스의 '키'와 '위치'는 객체가 아니다. 음료와 케이크의 '양'은 객체가 아니다. 문이 열려있는지 '여부'는 객체가 아니다. 토끼가 달려가는 '속도' 역시 객체가 아니다.
숫자, 문자열, 양, 속도, 시간, 날짜, 참/거짓과 같은 단순한 값들은 객체가 아니다. 단순한 값들은 그 자체로 독립적인 의미를 가지기보다는 다른 객체의 특성을 표현하는 데 사용된다. 다시 말해 다른 객체의 상태를 표현하기 위해 사용된다. 앨리스의 상태를 구성하는 키는 단순한 숫자 값으로 표현할 수 있다. 앨리스의 위치는 문자열로 표현할 수 있다. 음료와 케이크의 양은 숫자로, 문이 열려있는지 여부는 참/거짓으로 표현할 수 있을 것이다. 비록 단순한 값은 객체가 아니지만 객체가 아니지만 객체의 상태를 표현하기 위한 중요한 수단이다.
때로는 단순한 값이 아니라 객체를 사용해 다른 객체의 상태를 표현해야 할 때가 있다. 앨리스가 현재 음료를 들고 있는 상태인지를 표현하고 싶다면 어떻게 할 것인가? 가장 간단하고 직관적인 방법은 앨리스의 상태 일부를 음료라는 객체를 이용해 표현하는 것이다. 앨리스가 음료를 들고 있는지 여부는 앨리스라는 객체가 음료라는 객체와 연결돼 있는지 여부로 표현할 수 있다.
결론적으로 모든 객체의 상태는 단순한 값과 객체의 조합으로 표현할 수 있따. 이때 객체의 상태를 구성하는 모든 특징을 통틀어 객체의 프로퍼티(property)라고 한다. 앨리스의 경우 키, 위치, 음료가 앨리스의 프로퍼티가 된다. 일반적으로 프로퍼티는 변경되지 않고 고정되기 떄문에 '정적'이다. 반면 프로퍼티 값(property value)은 시간이 흐름에 따라 변경되기 때문에 '동적'이다. 앨리스의 키는 음료를 마시면 작아질 것이고, 문을 통과하면 위치가 정원으로 바뀔 것이며, 음료를 다 마신 후에는 현재 가지고 있는 음료를 버리게 될 것이다.
객체와 객체 사이의 의미 있는 연결을 링크(link)라고 한다. 객체와 객체 사이에는 링크가 존재해야만 요청을 보내고 받을 수 있다. 즉, 객체의 링크를 통해서만 메시지를 주고받을 수 있다.
링크는 객체가 다른 객체를 참조할 수 있다는 것을 의미하며, 이것은 일반적으로 한 객체가 다른 객체의 식별자를 알고 있는 것으로 표현된다.
객체 간의 선으로 표현되는 링크와 달리 객체를 구성하는 단순한 값은 속성(attribute)이라고 한다. 앨리스의 키와 위치는 단순한 값으로 표현된기 때문에 속성이다. 객체의 프로퍼티는 단순한 값인 속성과 다른 객체를 가리키는 링크라는 두 가지 종류의 조합으로 표현할 수 있다.
상태는 특정 시점에 객체가 가지고 잇는 정보의 집합으로 객체의 구조적 특징을 표현한다. 객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다. 객체의 프로퍼티는 단순한 값과 다른 객체를 참조하는 링크로 구분할 수 있다.
객체는 자율적인 존재라는 점을 명시하라. 객체지향의 세계에서 객체는 다른 객체의 상태에 직접적으로 접근할 수도, 상태를 변경할 수도 없다. 자율적인 객체는 스스로 자신의 상태를 책임져야 한다. 외부의 객체가 직접적으로 객체의 상태를 주무를 수 없다면 간접적으로 객체의 상태를 변경하거나 조회할 수 있는 방법이 필요하다.
이 시점에 행동이 무대 위로 등장한다. 행동은 다른 객체로 하여금 간접적으로 객체의 상태를 변경하는 것을 가능하게 한다. 객체지향의 기본 사상은 상태와 상태를 조작하기 위한 행동을 하나의 단위로 묶는 것이라는 점을 기억하라. 객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 객체의 자율성을 유지한다.
객체의 상태는 저절로 변경되지 않는다. 객체의 상태를 변경하는 것은 객체의 자발적인 행동뿐이다. 앨리스의 키가 작아진 이유는 앨리스가 음료를 마셨기 때문이다. 앨리스의 위치가 통로에서 정원으로 바뀐 이유는 앨리스가 문을 통과했기 때문이다. 앨리스가 특정한 행동을 취할 때마다 앨리스의 키와 위치는 변경된다.
객체가 취하는 행동은 객체 자신의 상태를 변경시킨다. 객체의 행동에 의해 객체의 상태가 변경된다는 것은 행동이 부수 효과(side effect)를 초래한다는 것을 의미한다. 부수효과의 개념을 이용하면 객체의 행동을 상태 변경의 관점에서 쉽게 기술할 수 있다.
상태와 행동 사이에는 다음과 같은 관계가 있다.
- 객체의 행동은 상태에 영향을 받는다
- 객체의 행동은 상태를 변경시킨다
이것은 상태라는 개념을 이용해 행동을 다음의 두 가지 관점에서 서술할 수 있음을 의미한다.
- 상호작용이 현재의 상태에 어떤 방식으로 의존하느가
- 상호작용이 어떻게 현재의 상태를 변경시키는가
객체는 협력에 참여하는 과정에서 자기 자신의 상태뿐만 아니라 다른 객체의 상태 변경을 유발할 수도 있다. 앨리스가 음료를 마시면 앨리스 자신의 키가 작아지는 동시에 앨리스가 먹은 양만큼 음료의 양이 주렁야 한다. 따라서 음료를 마시는 앨리스의 행동은 자기 자신뿐만 아니라 음료의 상태 변경도 유발한다.
객체의 행동으로 인해 발생하는 결과는 두 가지 관점에서 설명할 수 있다. 객체의 행동은 이 두 가지 관점의 부수효과를 명확하게 서술해야 한다.
- 객체 자신의 상태 변경
- 행동 내에서 협력하는 다른 객체에 대한 메시지 전송
행동이란 외부의 요청 또는 수신된 메시지에 응답하기 위해 동작하고 반응하는 활동이다. 행동의 결과로 객체는 자신의 상태를 변경하거나 다른 객체에게 메시지를 전달할 수 있다. 객체는 행동을 통해 다른 객체와의 협력에 참여하므로 행동은 외부에 가시적이어야 한다.
현실 세계의 객체와 객체지향 세계의 객체 사이에는 중요한 차이점이 있다. 현실 속에서 앨리스는 스스로 음료를 마시는 능동적인 존재지만 음료는 스스로 아무것도 할 수 없는 수동적인 존재다. 현실 세계라면 음료의 양을 줄여 상태를 변경시키는 주체는 음료를 목 안으로 밀어 넣은 앨리스가 될 것이다.
그러나 객체지향의 세계에서 모든 객체는 자신의 상태를 스스로 관리하는 자율적인 존재다. 앨리스 객체의 키를 작게 만드는 것이 앨리스 자신인 것처럼 음료 객체의 양을 줄이는 것은 음료 자신이어야 한다. 따라서 앨리스는 직접적으로 음료의 상태를 변경할 수 없다. 단지 음료에게 자신이 음료를 마셨다는 메시지를 전달할 수 있을 뿐이다.
앨리스가 음료를 마시는 행동은 앨리스 자신의 키를 작게 만든다. 따라서 앨리스 자신의 상태를 변경한다. 이 과정에서 앨리스는 자신이 먹은 양만큼 음료의 양을 줄여달라고 메시지를 전송한다. 이것이 앨리스가 음료를 마신다는 행동에 대한 모든 것이다. 음료의 양이 줄어들 것인지는 메시지를 수신한 음료가 결정할 사항이며, 앨리스와는 무관하다. 단지 앨리스는 음료의 양이 줄어들 것이라는 것을 믿고 요청을 전달할 뿐이다.
객체의 행동을 유발하는 것은 외부로부터 전달된 메시지지만 객체의 상태를 변경할지 여부는 객체 스스로 결정한다. 사실 객체에게 메시지를 전달하는 외부의 객체는 메시지를 수신하는 객체의 상태가 변경된다는 사실조차 알지 못한다. 메시지 송신자는 단지 자신의 요구를 메시지로 포장해서 전달할 뿐이다. 메시지를 해석하고 그에 반응해서 상태를 변경할지 여부는 전적으로 메시지 수신자의 자율적인 판단에 따른다. 송신자가 상태 변경을 기대하더라도 수신자가 자신의 상태를 변경하지 않는다면 송신자가 간섭할 수 있는 어떤 여지도 없다.
상태를 외부에 노출시키지 않고 행동을 경계로 캡슐화하는 것은 결과적으로 객체의 자율성을 높인다. 자율적인 객체는 스스로 판단하고 스스로 결정하기 때문에 객체의 자율성이 높아질수록 객체의 지능도 높아진다. 협력에 참여하는 객체들의 지능이 높아질수록 협력은 유연하고 간결해진다.
이것이 상태를 캡슐화해야 하는 이유다.
객체란 인간의 인지 능력을 이용해 식별 가능한 경계를 가진 모든 사물을 의미한다. 객체가 식별 가능하다는 것은 객체를 서로 구별할 수 있는 특정한 프로퍼티가 객체 안에 존재한다는 것을 의미한다. 이 프로퍼티를 식별자라고 한다. 모든 객체는 식별자를 가지며 식별자를 이용해 객체를 구별할 수 있다.
값과 객체의 가장 큰 차이점은 값은 식별자를 가지지 않지만 객체는 식별자를 가진다는 점이다. 그리고 시스템을 설계할 때는 이런 단순한 값과 객체의 차이점을 명확하게 구분하고 명시적으로 표현하는 것이 매우 중요하다.
값은 숫자, 문자열, 날짜, 시간, 금액 등과 같이 변하지 않는 양을 모델링한다. 흔히 값의 상태는 변하지 않기 때문에 불변 상태를 가진다고 말한다. 값의 경우 두 인스턴스의 상태가 같다면 두 인스턴스를 같은 것으로 판단한다. 두 개의 1이라는 숫자가 종이 위에 적혀 있을 때 모든 사람들은 두 숫자가 같은 것으로 간주한다. 두 숫자는 값이 같기 때문에 동일하다고 판단하며 굳이 두 개의 숫자를 구별하려고 하지 않는다.
값이 같은지 여부는 상태가 같은지를 이용해 판단한다. 값의 상태가 같으면 두 인스턴스는 동일한 것으로 판단하고 상태가 다르면 두 인스턴스는 다른 것으로 판단한다. 이처럼 상태를 이용해 두 값이 같은지 판단할 수 있는 성질을 동등성이라고 한다.
객체는 시간에 따라 변경되는 상태를 포함하며, 행동을 통해 상태를 변경한다. 따라서 객체는 가변 상태를 가진다고 말한다. 타입이 같은 두 객체의 상태가 완전히 똑같더라도 두 객체는 독립적인 별개의 객체로 다뤄야 한다. 이름이 앨리스이고 키가 동일한 두 사람이 함께 있다고 하더라도 어떤 누구도 두 사람을 같은 사람이라고 생각하지 않는다. 비록 이름과 키 등의 상태는 완전히 동일하지만 두 사람은 완전히 별개의 인격체다.
어린 시절의 여러분을 떠올려보자. 어린 여러분은 현재의 여러분보다 키도 작고 나이도 적지만 두 사람은 동일한 인물이다. 따라서 여러분은 상태와 무관하게 동일한 사람으로 판단될 수 있다. 이처럼 식별자를 기반으로 객체가 같은지를 판단할 수 있는 성질을 동일성(identical)이라고 한다.
상태를 기반으로 객체의 동일성을 판단할 수 없는 이유는 시간이 흐름에 따라 객체의 상태가 변하기 때문이다. 어느 한 시점에 두 객체의 상태가 동일하더라도 한 객체의 상태가 변하는 순간 두 객체는 서로 다른 상태가 되어 버린다. 따라서 상태가 가변적인 두 객체의 동일성을 판단하기 위해서는 상태 변경에 독립적인 별도의 식별자를 이용할 수밖에 없다.
식별자란 어떤 객체를 다른 객체와 구분하는 데 사용하는 객체의 프로퍼티다. 값은 식별자를 가지지 않기 때문에 상태를 이용한 동등성 검사를 통해 두 인스턴스를 비교해야 한다. 객체는 상태가 변경될 수 있기 때문에 식별자를 이용한 동일성 검사를 통해 두 인스턴스를 비교할 수 있다.
대부분의 사람들은 값과 객체의 차이점을 혼란스러워하는데, 대부분의 객체지향 프로그래밍 언어에서 두 개념 모두 클래스를 이용해 구현되기 때문이다. 객체지향 언어 관점에서 값과 객체 모두 클래스로부터 생성된 객체이기 때문에 문맥에 따라 그 의미가 혼란스러워질 수 있다.
이런 오해의 소지를 줄이기 위해 객체와 값을 지칭하는 별도의 용어를 사용하기도 한다. 참조 객체(reference object), 또는 엔티티(entity)는 식별자를 지닌 전통적인 의미의 객체를 가리키는 용어다. 값 객체(value object)는 식별자를 가지지 않는 값을 가리키는 용어다. 이 책에서 특별한 언급이 없는 한 객체라는 용어는 식별자를 가지는 참조 객체나 엔티티를 가리킨다.
행동이 상태를 결정한다
안타깝게도 상태를 먼저 결정하고 행동을 나중에 결정하는 방법은 설계에 나쁜 영향을 끼친다.
첫째, 상태를 먼저 결정할 경우 캡슐화가 저해된다. 상태에 초점을 맞출 경우 상태가 객체 내부로 깔끔하게 캡슐화되지 못하고 공용 인터페이스에 그대로 노출되버릴 확률이 높아진다.
둘째, 객체를 협력자가 아닌 고립된 섬으로 만든다. 객체가 필요한 이유는 애플리케이션의 문맥 내에서 다른 객체와 협력하기 위해서다. 불행하게도 상태를 먼저 고려하는 방식은 협력이라는 문맥에서 멀리 벗어난 채 객체를 설계하게 함으로써 자연스럽게 협력에 적합하지 못한 객체를 창조하게 된다.
셋째, 객체의 재사용성이 저하된다. 객체의 재사용성은 다양한 협력에 참여할 수 있는 능력에서 나온다. 상태에 초점을 맞춘 객체는 다양한 협력에 참여하기 어렵기 때문에 재사용성이 저하될 수밖에 없다.
협력에 참여하는 훌륭한 객체 시민을 양성하기 위한 가장 중요한 덕목은 상태가 아니라 행동에 초점을 맞추는 것이다. 객체는 다른 객체와 협력하기 위해 존재한다. 객체의 행동은 객체가 협력에 참여하는 유일한 방법이다. 따라서 객체가 적합한지를 결정하는 것은 그 객체의 상태가 아니라 행동이다. 설계자로서 우리는 협력의 문맥에 맞는 적절한 행동을 수행하는 객체를 발견하거나 창조해야 한다. 결과적으로 우리가 애플리케이션 안에서 어떤 행동을 원하느냐가 어떤 객체가 적합한지를 결정한다. 객체의 적합성을 결정하는 것은 상태가 아니라 객체의 행동이다.
협력 안에서 객체의 행동은 결국 객체가 협력에 참여하면서 완수해야 하는 책임을 의미한다. 따라서 어떤 책임이 필요한가를 결정하는 과정이 전체 설계를 주도해야 한다. 책임-주도 설계(Responsibility-Driven Design, RDD)는 협력이라는 문맥 안에서 객체의 행동을 생각하도록 도움으로써 응집도 높고 재사용 가능한 객체를 만들 수 있게 한다.
"행동이 상태를 결정한다"
은유와 객체
안타깝게도 객체지향 세계는 현실 세계의 단순한 모방이 아니다. 소프트웨어 안에 구현된 상품 객체는 실제 세계의 상품과는 전혀 다른 양상을 띤다. 소프트웨어 상품이 실제 세계의 상품을 단순화하거나 추상화한 것이 아니라 특성이 전혀 다른 어떤 것임을 의미한다.
모방과 추상화라는 개념만으로는 현실 객체와 소프트웨어 객체 사이의 관계를 깔끔하게 설명하지 못한다. 최초의 객체지향 언어인 시뮬라가 그 이름에서부터 객체지향이 현실세계를 시뮬레이션한다는 의미를 강하게 표출하고 있다고 하더라도 객체지향이 현실을 오롯이 모방하기만 한다는 것은 오해일 뿐이다.
현실 속의 객체와 소프트웨어 객체 사이의 가장 큰 차이점은 무엇일까? 그것은 현실 속에서는 수동적인 존재가 소프트웨어 객체로 구현될 때는 능동적으로 변한다는 것이다.
레베카 워프스브록은 현실의 객체보다 더 많은 일을 할 수 있는 소프트웨어 객체의 특징을 의인화(anthropomorphism)라고 부른다.
객체지향의 세계와 현실 세계 사이에는 전혀 상관이 없는 것인가? 그렇지는 않다. 다만 모방이나 추상화의 수준이 아닌 다른 관점에서 유사성을 가지고 있을 뿐이다. 현실 세계와 객체지향 세계 사이의 관계를 좀 더 정확하게 설명할 수 있는 단어는 은유(metaphor)다.
03. 타입과 추상화
일단 컴퓨터를 조작하는 것이 추상화를 구축하고, 조작하고, 추론하는 것에 관한 모든 것이라는 것을 깨닫고 나면 (훌륭한)컴퓨터 프로그램을 작성하기 위한 중요한 전제 조건은 추상화를 정확하게 다루는 능력이라는 것이 명확해진다.
따라서 진정한 의미에서 추상화란 현실에서 출발하되 불필요한 부분을 도려내가면서 사물의 놀라운 본질을 드러나게 하는 과정이라고 할 수 있다. 추상화의 목적은 불필요한 부분을 무시함으로써 현실에 존재하는 복잡성을 극복하는 것이다. 추상화는 복잡한 현실을 단순화하기 위해 사용하는 인간의 가장 기본적인 인지 수단이라고 할 수 있다.
추상화를 통한 복잡성 극복
헤리 벡의 지하철 노선도를 통해 알 수 있는 것처럼 훌륭한 추상화는 목적에 부합하는 것이어야 한다. 사실적인 지형 정보를 추가한 초기의 런던 지하철 노선도 역시 훌륭한 추상화라고 할 수 있지만 지하철을 이용하는 승객들의 목적에는 적합하지 않았기 때문에 사람들로부터 외면을 바독 말았다. 반대로 헤리 벡의 지하철 노선도는 런던 곳곳의 정확한 위치와 실제 거리를 알고자 하는 경우에는 적절하지 않다. 지상의 도로를 이용해 지하철 노선도에 적힌 역을 찾으려고 하는 사람에게는 지하철 노선도 위의 왜곡된 위치와 거리는 큰 도움이 되지 않는다.
어떤 추상화도 의도된 목적이 아닌 다른 목적으로 사용된다면 오도될 수 있다. 추상화의 수준, 이익, 가치는 목적에 의존적이다. 리처드 파인만의 말처럼 "현상은 복잡하다. 법칙은 단순하다. 버릴게 무엇인지 알아내라."
추상화
어떤 양상, 세부 사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.
복잡성을 다루기 위해 추상화는 두 차원에서 이뤄진다.
- 첫 번째 차원은 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순하게 만드는 것이다.
- 두 번째 차원은 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거함으로써 단순하게 만드는 것이다.
모든 경우에 추상화의 목적은 복잡성을 이해하기 쉬운 수준으로 단순화하는 것이라는 점을 기억하라.
객체지향과 추상화
공통점을 기반으로 객체들을 묶기 위한 그릇을 개념(concept)이라고 한다. 개념이란 일반적으로 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 뜻한다.
개념을 이용하면 객체를 여러 그룹으로 분류(classification)할 수 있다. 앨리스가 정원에 존재하는 객체를 '트럼프'와 '토끼'라는 두 개의 개념으로 나누고는 두 개념에 적합한 객체가 각 그룹에 포함되도록 분류했다는 사실에 주목하라. 결과적으로 개념은 공통점을 기반으로 객체를 분류할 수 있는 일종의 체라고 할 수 있다.
결국 각 객체는 특정한 개념을 표현하는 그룹의 일원으로 포함된다. 하트 여왕은 '트럼프'라는 개념 그룹의 일원이고 하얀 토끼는 '토끼'라는 개념 그룹의 일원이다. 이처럼 객체에 어떤 개념을 적용하는 것이 가능해서 개념 그룹의 일원이 될 때 객체를 그 개념의 인스턴스(instance)라고 한다. 따라서 객체를 다음과 같이 정의할 수 있다.
객체란 특정한 개념을 적용할 수 있는 구체적인 사물을 의미한다. 개념이 객체에 적용됐을 때 객체를 개념의 인스턴스라고 한다.
개념은 세상의 객체들을 거르는 데 사용하는 정신적인 렌즈를 제공한다. 이 렌즈를 통해 세상을 바라보면 수백 수천 개의 다양한 객체가 존재하는 복잡한 세상을 몇 개의 개념만으로 단순화할 수 있다. 개념은 객체를 분류할 수 있는 틀을 제공한다. 앨리스가 수많은 군상들을 단지 트럼프일 뿐이라고 일축했던 것처럼 주변의 복잡한 객체들은 단지 몇 가지 개념의 인스턴스일 뿐이다.
일반적으로 객체의 분류 장치로서 개념을 이야기할 때는 아래의 세 가지 관점을 함께 언급한다.
- 심볼(symbol): 개념을 가리키는 간략한 이름이나 명칭
- 내연(intension): 개념의 완전한 정의를 나타내며 내연의 의미를 이용해 객체가 개념에 속하는지 여부를 확인할 수 있다.
- 외연(extension): 개념에 속하는 모든 객체의 집합(set)
'심볼'이란 개념을 가리키는 이름이다. 앨리스 이야기에서 개념을 지칭하는 데 사용하는 '트럼프'라는 이름을 개념의 심볼이 된다.
'내연'이란 개념의 의미를 나타낸다. 앨리스의 이야기에서 몸이 납작하고 두 손와 두 발이 네모난 몸 모서리에 달려 있다는 트럼프에 대한 설명이 바로 내연이다. 내연은 개념을 객체에게 적용할 수 있는지 여부를 판단하기 위한 조건이라는 점에 주목하라. 하얀 토끼는 트럼프의 내연을 만족시키지 못하기 때문에 트럼프가 될 수 없다.
'외연'은 개념에 속하는 객체들, 즉 개념의 인스턴스들이 모여 이뤄진 집합을 가리킨다. 앨리스의 이야기에서 정원사, 병사, 신하, 왕자와 공주, 하객으로 참석한 왕과 왕비들, 하트 잭, 하트 왕과 하트 여왕은 모두 트럼프의 외연을 구성하는 객체 짛바에 속한다.
트럼프라는 개념의 심볼, 내연, 외연은 다음과 같이 표현할 수 있다.
- 심볼: 트럼프
- 내연: 몸이 납작하고 두 손과 두 발을 네모 귀퉁이에 달려 있는 등장인물
- 외연: 정원사, 병사, 신하, 왕자와 공주, 하객으로 참석한 왕과 왕비들, 하트 잭, 하트 왕과 하트 여왕
개념을 이용해 공통점을 가진 객체들을 분류할 수 있다는 아이디어는 객체지향 패러다임이 복잡성을 극복하는 데 사용하는 가장 기본적인 인지 수단이기 때문이다.
객체지향의 세계에서 가장 널리 알려진 유명인사가 클래스(class)라는 사실을 감안한다면 분류(classification)라는 개념이 얼마나 중요한지 실감할 수 있을 것이다.
앞에서 추상화가 두 가지 차원에서 이뤄진다고 했던 것을 머릿속에 떠올려 보라. 추상화의 첫 번째 차원은 구체적인 사물 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순화하는 것이다. 추상화의 두 번째 차원은 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거해 단순화하는 것이다. 개념을 통해 객체를 분류하는 과정은 추상화의 두 가지 차원을 모두 사용한다.
개념은 추상화의 첫 번째 차원인 일반화를 적용한 결과다.
추상화의 두 번째 차원에 따라 불필요한 세부 사항을 제거했다고 볼 수 있다.
개념은 객체들의 복잡성을 극복하기 위한 추상화 도구다.
타입
타입은 개념이다.
개념이란 단어 자체는 이미 우리의 일상생활에서 폭넓게 사용되는 일상적인 용어지만 컴퓨터 공학자들은 개념을 좀 더 멋지게 표현할 수 있는 자신들만의 용어를 가지고 싶었던 것 같다. 따라서 공학자들은 개념을 대체할 수 있는 좀 더 세련돼 보이는 용어를 수학으로부터 차용해 왔다. 그것이 바로 타입(type)이다.
타입의 정의는 개념의 정의와 완전히 동일하다. 타입은 공통점을 기반으로 객체들을 묶기 위한 틀이다. 타입은 개념과 마찬가지로 심볼, 내연, 외연을 이용해 서술할 수 있으며 타입에 속하는 객체 역시 타입의 인스턴스라고 한다.
타입은 개념과 동일하다. 따라서 타입이란 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 의미한다. 어떤 객체에 타입을 적용할 수 있을 때 그 객체를 타입의 인스턴스라고 한다. 타입의 인스턴스는 타입을 구성하는 외연인 객체 집합의 일원이 된다.
컴퓨터 안에 살아가는 데이터를 목적에 따라 분류하기 시작하면서 프로그래밍 언어 안에는 서서히 타입 시스템(type system)이 자라나기 시작했다. 타입 시스템의 목적은 메모리 안의 모든 데이터가 비트열로 보임으로써 야기되는 혼란을 방지하는 것이다. 타입 시스템은 메모리 안에 저장된 0과 1에 대해 수행 가능한 작업과 불가능한 작업을 구분함으로써 데이터가 잘못 사용되는 것을 방지한다. 결과적으로 타입 시스템의 목적은 데이터가 잘못 사용되지 않도록 제약사항을 부과하는 것이다.
지금까지 이야기한 내용을 통해 타입에 관련된 두 가지 중요한 사실을 알 수 있다.
첫째, 타입은 데이터가 어떻게 사용되느냐에 관한 것이다. 숫자형 데이터가 숫자형인 이유는 데이터를 더하거나 빼거나 곱하거나 나눌 수 있기 때문이다. 어떤 데이터가 문자열형인 이유는 두 데이터를 연결해 새로운 문자열을 만들 수 있고 데이터에 포함된 문자의 길이를 알 수 있기 때문이다. 중요한 것은 연산자의 종류가 아니라 어떤 데이터에 어떤 연산자를 적용할 수 있느냐가 그 데이터의 타입을 결정한다는 점이다.
둘째, 타입에 속한 데이터를 메모리에 어떻게 표현하는지는 외부로부터 철저하게 감춰진다. 데이터 타입의 표현은 연산 작업을 수행하기에 가장 효과적인 형태가 선택되며, 개발자는 해당 데이터 타입의 표현 방식을 몰라도 데이터를 사용하는 데 지장이 없다. 개발자는 해당 데이터 타입을 사용하기 위해 단지 데이터 타입에 적용할 수 있는 연산자만 알고 있으면 된다.
데이터 타입은 메모리 안에 저장된 데이터의 종류를 분류하는 데 사용하는 메모리 집합에 관한 메타데이터다. 데이터에 대한 분류는 암시적으로 어떤 종류의 연산이 해당 데이터에 대해 수행될 수 있는지를 결정한다.
그렇다면 객체는 데이터인가? 그렇지 않다. 객체에서 중요한 것은 객체의 행동이다. 상태는 행동의 결과로 초래된 부수효과를 쉽게 표현하기 위해 도입한 추상적인 개념일 뿐이다. 객체를 창조할 때 가장 중요하게 고려해야 하는 것은 객체가 이웃하는 객체와 협력하기 위해 어떤 행동을 해야 할지를 결정하는 것이다. 즉, 객체가 협력을 위해 어떤 책임을 지녀야 하는지를 결정하는 것이 객체지향 설계의 핵심이다.
첫째, 어떤 객체가 어떤 타입에 속하는지를 결정하는 것은 객체가 수행하는 행동이다. 어떤 객체들이 동일한 행동을 수행할 수 있다면 그 객체들은 동일한 타입으로 분류될 수 있다.
둘째, 객체의 내부적인 표현은 외부로부터 철저하게 감춰진다. 객체의 행동을 가장 효과적으로 수행할 수만 있다면 객체 내부의 상태를 어떤 방식으로 표현하더라도 무방하다.
행동이 우선이다.
객체가 어떤 행동을 하느냐에 따라 객체의 타입이 결정된다. 객체의 타입은 객체의 내부 표현과는 아무런 상관이 없다. 따라서 객체의 내부 표현 방식이 다르더라도 어떤 객체들이 동일하게 행동한다면 그 객체들은 동일한 타입에 속한다. 결과적으로 동일한 책임을 수행하는 일련의 객체는 동일한 타입에 속한다고 말할 수 있다.
이것은 객체를 타입으로 분류할 때 사용해야 하는 기준을 명확하게 제시한다. 어떤 객체를 다른 객체와 동일한 타입으로 분류하는 기준은 무엇인가? 그 객체가 타입에 속한 다른 객체와 동일한 행동을 하기만 하면 된다. 그 객체가 어떤 데이터를 가지고 있는지는 우리의 관심사가 아니다. 그 객체가 다른 객체와 동일한 데이터를 가지고 있더라도 다른 행동을 한다면 그 객체들은 서로 다른 타입으로 분류돼야 한다.
결론적으로 객체의 타입을 결정하는 것은 객체의 행동뿐이다. 객체가 어떤 데이터를 보유하고 있는지는 타입을 결정하는 데 아무런 영향도 미치지 않는다.
같은 타입에 속한 객체는 행동만 동일하다면 서로 다른 데이터를 가질 수 있다. 여기서 동일한 행동이란 동일한 책임을 의미하며, 동일한 책임이란 동일한 메시지 수신을 의미한다. 따라서 동일한 타입에 속한 객체는 내부의 데이터 표현 방식이 다르더라도 동일한 메시지를 수신하고 이를 처리할 수 있다. 다만 내부의 표현 방식이 다르기 때문에 동일한 메시지를 처리하는 방식은 서로 다를 수밖에 없다. 이것은 다형성에 의미를 부여한다. 다형성이란 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력을 뜻한다. 동일한 메시지를 서로 다른 방식으로 처리하기 위해서는 객체들은 동일한 메시지를 수신할 수 있어야 하기 때문에 결과적으로 다형적인 객체들은 동일한 타입(또는 타입 계층)에 속하게 된다.
훌륭한 객체지향 설계는 외부에 행동만을 제공하고 데이터는 행동 뒤로 감춰야 한다. 이 원칙을 흔히 캡슐화라고 한다. 공용 인터페이스 뒤로 데이터를 캡슐화하라는 오래된 격언은 객체를 행동에 따라 분류하기 위해 지켜야 하는 기본적인 원칙이다.
행동에 따라 객체를 분류하기 위해서는 객체가 내부적으로 관리해야 하는 데이터가 아니라 객체가 외부에 제공해야 하는 행동을 먼저 생각해야 한다. 이를 위해서는 객체가 외부에 제공해야 하는 책임을 먼저 결정하고 그 책임을 수행하는 데 적합한 데이터를 나중에 결정한 후, 데이터를 책임을 수행하는 데 필요한 외부 인터페이스 뒤로 캡슐화해야 한다. 흔히 책임-주도 설계(Responsibility-Driven Design)라고 부르는 객체지향 설계 방법은 데이터를 먼저 생각하는 데이터-주도 설계(Data-Driven Design) 방법의 단점을 개선하기 위해 고안됐다.
타입의 계층
트럼프는 트럼프 인간을 포괄하는 좀 더 일반적인 개념이다. 트럼프 인간은 트럼프보다 좀 더 특화된 행동을 하는 특수한 개념이다. 이 두 개념 사이의 관계를 일반화/특수화(generalization/specialization) 관계라고 한다.
일반화와 특수화는 동시에 일어난다.
객체지향에서 일반화/특수화 관계를 결정하는 것은 객체의 상태를 표현하는 데이터가 아니라 행동이라는 것이다. 어떤 객체가 다른 객체보다 더 일반적인 상태를 표현하거나 더 특수한 상태를 표현한다고 해서 두 객체가 속하는 타입 간에 일반화/특수화 관계가 성립하는 것은 아니다. 두 타입 간에 일반화/특수화 관계가 성립하려면 한 타입이 다른 타입보다 더 특수하게 행동해야 하고 반대로 한 타입은 다른 타입보다 더 일반적으로 행동해야 한다. 결국 객체의 일반화/특수화 관계에 있어서도 중요한 것은 객체가 내부에 보관한 데이터가 아니라 객체가 외부에 제공하는 행동이다.
행동의 관점에서 더 일반적인 타입이란 무엇이고 더 특수한 타입이란 무엇인가? 일반적인 타입이란 특수한 타입이 가진 모든 행동들 중에서 일부 행동만을 가지는 타입을 가리킨다. 특수한 타입이란 일반적인 타입이 가진 모든 행동을 포함하지만 거기에 더해 자신만의 행동을 추가하는 타입을 가리킨다. 따라서 일반적인 타입은 특수한 타입보다 더 적은 수의 행동을 가지고 특수한 타입은 일반적인 타입보다 더 많은 수의 행동을 가진다.
좀 더 일반적인 타입을 슈퍼타입(supertype)이라고 하고 좀 더 특수한 타입을 서브타입(subtype)이라고 한다.
일반적으로 서브타입은 슈퍼타입의 행위와 호환되기 때문에 서브타입은 슈퍼타입을 대체할 수 있어야 한다.
추상화의 두 번째 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거시켜 단순하게 만드는 것이다. 일반화/특수화 계층은 객체지향 패러다임에서 추상화의 두 번째 차원을 적절하게 활용하는 대표적인 예다.
정적 모델
왜 타입을 사용해야 하는가? 객체지향은 객체를 지향하는 것이므로 객체만 다루면 되지 않는가? 타입을 사용하는 이유는 인간의 인지 능력으로는 시간에 따라 동적으로 변하는 객체의 복잡성을 극복하기가 너무 어렵기 때문이다.
앨리스의 키는 앨리스가 어떤 음식을 먹을 때마다, 어떤 행동을 할 때마다 시시각각 변한다. 어떤 때는 앨리스의 몸이 집을 가득 채우고 남을 정도로 거대해지기도 했고, 또 어떤 때는 앨리스의 목이 높다란 천장에 다다를 정도로 커져버리기도 했다. 앨리스의 몸이 작은 강아지보다 더 작아져 버린 경우도 있었다. 앨리스의 키는 계속 변하고 있었지만 모든 경우에 앨리스는 단지 앨리스일 뿐이다.
앨리스라고 하는 객체의 상태는 변하지만 앨리스를 다른 객체와 구별할 수 있는 식별성은 동일하게 유지된다. 따라서 우리는 머릿속에 앨리스가 가질 수 있는 모든 경우의 키 값을 나열하는 대신 앨리스의 키가 임의의 값을 가질 수 있다는 사실만을 생각함으로써 상황을 단순하게 만들 수 있다. 다시 말해 앨리스에 대해 생각할 때 키가 100센티미터, 80센티미터, 3미터가 될 수 있다라는 사실은 뒤로 미루고, 단지 앨리스가 변경되는 키라는 상태를 가진다고 단순화하면 그만이라는 것이다.
타입은 시간에 따라 동적으로 변하는 앨리스의 상태를 시간과 무관한 정적인 모스븡로 다룰 수 있게 해준다. 결국 타입은 시간에 독립적인 정적인 모습으로 앨리스를 생각할 수 있게 해준다.
타입은 추상화다. 타입을 사용하면 객체의 동적인 특성을 추상화할 수 있다. 결국 타입은 시간에 따른 객체의 상태 변경이라는 복잡성을 단순화할 수 있는 효과적인 방법인 것이다.
지금까지의 논의를 통해 객체를 생각할 때 우리는 두 가지 모델을 동시에 고려한다는 사실을 알 수 있다.
하나는 객체가 특정 시점에 구체적으로 어떤 상태를 가지느냐다. 이를 객체의 스냅샷(snapshot)이라고 한다. 객체지향 모델링을 위한 표준 언어인 UML에서 스냅샷은 객체 다이어그램(object diagram)이라고도 불린다. 스냅샷처럼 실제로 객체가 살아 움직이는 동안 상태가 어떻게 변하고 어떻게 행동하는지를 포착하는 것을 동적 모델(dynamnic model)이라고 한다.
다른 하나는 객체가 가질 수 있는 모든 상태와 모든 행동을 시간에 독립적으로 표현하는 것이다. 일반적으로 이런 모델을 타입 모델(type diagram)이라고 한다. 이 모델은 동적으로 변하는 객체의 상태가 아니라 객체가 속한 타입의 정적인 모습을 표현하기 때문에 정적 모델(static model)이라고도 한다.
객체지향 프로그래밍 언어에서 정적인 모델은 클래스를 이용해 구현된다. 타입을 구현하는 가장 보편적인 방법은 클래스를 이용하는 것이다. 클래스와 타입은 동일한 것이 아니다. 타입은 객체를 분류하기 위해 사용하는 개념이다. 반면 클래스는 단지 타입을 구현할 수 있는 여러 구현 메커니즘 중 하나일 뿐이다. 실제로 자바스크립트와 같은 프로토타입 기반의 언어에는 클래스가 존재하지 않는다.
결국 개체지향에서 중요한 것은 동적으로 변하는 객체의 '상태'와 상태를 변경하는 '행위'다. 클래스는 타입을 구현하기 위해 프로그래밍 언어에서 제공하는 구현 메커니즘이라는 사실을 기억하라.
04. 역할, 책임, 협력
객체의 세계에서도 협력이라는 문맥이 객체의 행동 방식을 결정한다. 객체지향에 갓 입문한 사람들의 가장 흔한 실수는 협력이라는 문맥을 고려하지 않은 채 객체가 가져야 할 상태와 행동부터 고민하기 시작한다는 것이다.
중요한 것은 개별 객체가 아니라 객체들 사이에 이뤄지는 협력이다. 객체지향 설계의 전체적인 품질을 결정하는 것은 개별 객체의 품질이 아니라 여러 객체들이 모여 이뤄내는 협력의 품질이다. 훌륭한 객체지향 설계자는 객체들 간의 요청과 응답 속에서 창발하는 협력에 초점을 맞춰 애플리케이션을 설계한다. 협력이 자리를 잡으면 저절로 객체의 행동이 드러나고 뒤이어 적절한 객체의 상태가 결정된다.
협력
협력은 한 사람이 다른 사람에게 도움을 요청할 때 시작된다.
다른 사람으로부터 요청을 받은 사람 역시 자신에게 주어진 일을 처리하던 중에 다른 사람의 도움이 필요한 경우가 있다. 결과적으로 협력은 다수의 요청과 응답으로 구성되며 전체적으로 협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성된다.
책임
크레이그 라만(Craig Larman)은 "객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것"이라고 말한다. 책임을 어떻게 구현할 것인가 하는 문제는 객체와 책임이 제자리를 잡은 후에 고려해도 늦지 않다. 객체와 책임이 이리저리 부유하는 상황에서 성급하게 구현에 뛰어드는 것은 변경에 취약하고 다양한 협력에 참여할 수 없는 비자율적인 객체를 낳게 된다.
객체의 책임은 '객체가 무엇을 알고 있는가(knowing)'와 '무엇을 할 수 있는가(doing)'로 구성된다.
크레이그 라만은 이러한 분류 체계에 따라 객체의 책임을 크게 '하는 것'과 '아는 것'의 두 가지 범주로 자세히 분류하고 있다.
- doing
- 객체를 생성하거나 계산을 하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
- knowing
- 개인적인 정보에 관해 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
한 가지 주의할 점은 책임과 메시지의 수준이 같지는 않다는 점이다. 책임은 객체가 협력에 참여하기 위해 수행해야 하는 행위를 상위 수준에서 개략적으로 서술한 것이다. 책임을 결정한 후 실제로 협력을 정제하면서 이를 메시지로 변환할 때는 하나의 책임이 여러 메시지로 분할되는 것이 일반적이다.
설계를 시작하는 초반에는 어떤 객체가 어떤 책임을 가지고 어떤 방식으로 서로 협력해야 하는지에 대한 개요를 아는 것만으로도 충분하다. 책임과 협력의 구조가 자리를 잡기 전까지는 책임을 구현하는 방법에 대한 고민은 잠시 뒤로 미루는 것이 좋다. 물론 언젠가는 모자 장수가 어떻게 증언할 것인지를 고민해야 하겠지만 재판이라는 협력에 참여하기 위해 왕과 모자 장수가 상호 협력해야 하고, 이를 위해 '증언하라'라는 메시지를 송신하고 수신할 수 있다는 것을 결정하는 것이 더 중요하다.
역할
역할은 협력 내에서 다른 객체로 대체할 수 있음을 나타내는 일종의 표식이다. 협력 안에서 역할은 "이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다"라고 말하는 것과 같다.
역할의 가장 큰 가치는 하나의 협력 안에 여러 종류의 객체가 참여할 수 있게 함으로써 협력을 추상화할 수 있다는 것이다. 협력의 추상화는 설계자가 다뤄야 하는 협력의 개수를 줄이는 동시에 구체적인 객체를 추상적인 역할로 대체함으로써 협력의 양상을 단순화한다. 결과적으로 애플리케이션의 설계를 이해하고 기억하기 쉬워진다.
역할은 협력 안에서 구체적인 객체로 대체될 수 있는 추상적인 협력자다. 따라서 본질적으로 역할은 다른 객체에 의해 대체 가능함을 의미한다.
객체가 역할을 대체하기 위해서는 행동이 호환돼야 한다는 점에 주목하라. 객체지향의 용어를 빌려 설명하면 객체가 역할을 대체 가능하기 위해서는 협력 안에서 역할이 수행하는 모든 책임을 동일하게 수행할 수 있어야 한다.
객체가 역할에 주어진 책임 이외의 다른 책임을 수행할 수도 있다는 사실에 주목하라. 모자 장수는 증인으로서의 역할뿐만 아니라 모자를 판매할 모자 장수로서 본질적인 책임을 가지고 있다. 앨리스는 잠시만 증인의 역할을 할 뿐 이야기 내내 주인공으로서의 역할을 해왔다.
결국 객체는 역할이 암시하는 책임보다 더 많은 책임을 가질 수 있다. 따라서 대부분의 경우에 객체의 타입과 역할 사이에는 일반화/특수화 관계가 성립하는 것이 일반적이다. 일반화/특수화 관점에서 좀 더 일반적인 개념을 의미하는 역할은 일반화이며 좀 더 구체적인 개념을 의미하는 객체의 타입은 특수화다. 역할이 협력에 추상적으로 만들 수 있는 이유는 역할 자체가 객체의 추상화이기 때문이다.
요약하면 역할의 대체 가능성은 행위 호환성을 의미하고, 행위 호환성은 동일한 책임의 수행을 의미한다.
객체의 모양을 결정하는 협력
많은 사람들은 시스템에 필요한 데이터를 저장하기 위해 객체가 존재한다는 선입견을 가지고 있다. 물론 객체가 상태의 일부로 데이터를 포함하는 것은 사실이지만 데이터는 단지 객체가 행위를 수행하는 데 필요한 재료일 뿐이다. 객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서다. 따라서 실제로 중요한 것은 객체의 행동, 즉 책임이다.
객체지향에 대한 두 번째 선입견은 객체지향이 클래스와 클래스 간의 관계를 표현하는 시스템의 정적인 측면에 중점을 둔다는 것이다. 중요한 것은 클래스가 아니라 협력에 참여하는 동적인 객체이며, 클래스는 단지 시스템에 필요한 객체를 표현하고 생성하기 위해 프로그래밍 언어가 제공하는 구현 메커니즘이라는 사실을 기억하라. 객체지향의 핵심은 클래스를 어떻게 구현할 것인가가 아니라 객체가 협력 안에서 어떤 책임과 역할을 수행할 것인지를 결정하는 것이다.
객체지향 입문자들이 데이터나 클래스를 중심으로 애플리케이션을 설계하는 이유는 협력이라는 문맥을 고려하지 않고 각 객체를 독립적으로 바라보기 때문이다. 예를 들어 '왕'의 인스턴스를 모델링할 경우 대부분의 사람들은 왕관을 쓰고 멋진 수염을 기른 채 근엄한 표정으로 왕좌에 앉아 있는 왕의 모습부터 떠올릴 것이다. 그러고는 머릿속에 떠오른 왕의 모습을 기반으로 클래스를 개발하기 시작할 것이다.
처음에는 전형적인 왕의 모습을 빌려 소프트웩어 객체를 창조하는 것이 합리적이고 적절해 보일지 모르지만 실제로 동작하는 애플리케이션을 구축하기 위해서는 왕이 참여하는 협력을 우선적으로 고려해야 한다. 왕관을 쓰고 멋진 수염을 기른 채 근엄한 표정으로 왕좌에 앉아 있는 왕의 모은 앨리스의 이야기에는 어울리지 않는다. 중요한 것은 왕의 겉모습이 아니다. 앨리스의 이야기에서 왕이 중요한 이유는 재판이라는 협력에 '판사'의 역할로 참여해서 죄인의 죄를 판결하는 책임을 수행할 수 있기 때문이다.
올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 한다. 협력을 설계한다는 것은 설계에 참여하는 객체들이 주고받을 요청과 응답의 흐름을 결정한다는 것을 의미한다. 이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 책임이 된다.
일단 객체에게 책임을 할당하고 나면 책임은 객체가 외부에 제공하게 될 행동이 된다. 협력이라는 문맥에서 객체가 수행하게 될 적절한 책임, 즉 행동을 결정한 후에 그 행동을 수행하는 데 필요한 데이터를 고민해야 한다. 그리고 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 어느 정도 결정된 후에 클래스의 구현 방법을 결정해야 한다. 결과적으로 클래스와 데이터는 협력과 책임의 집합이 결정된 후에야 무대 위에 등장할 수 있다.
객체지향이 올바른 객체에 올바른 책임을 할당하는 것과 관련된 모든 것이라면 협력이라는 문맥 안에서 객체를 생각하는 것은 올바른 객체지향 애플리케이션을 구현하는 것과 관련된 모든 것이다. 일단 협력이라는 견고한 문맥이 갖춰지면 우리의 초점은 협력을 위해 필요한 책임의 흐름으로 옮겨진다. 그리고 협력에 필요한 책임을 결정하고 객체에게 책임을 할당하는 과정을 얼마나 합리적이고 적절하게 수행했는지가 객체지향 설계의 품질을 결정한다.
객체의 행위에 초점을 맞추기 위해서는 협력이라는 실행 문맥 안에서 책임을 분배해야 한다. 각 객체가 가져야 하는 상태와 행위에 대해 고민하기 전에 그 객체가 참여할 문맥인 협력을 정의하라. 객체지향 시스템에서 가장 중요한 것은 충분히 자율적인 도잇에 충분히 협력적인 객체를 창조하는 것이다. 이 목표를 달성할 수 있는 가장 쉬운 방법은 객체를 충분히 협력적으로 만든 후에 협력이라는 문맥 안에서 객체를 충분히 자율적으로 만드는 것이다.
객체지향 설계 기법
역할, 책임, 협력의 관점에서 애플리케이션을 설계하는 유용한 세 가지 기법을 살펴보기로 하자.
맨 먼저 살펴볼 책임-주도 설계 방법은 협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 애플리케이션을 설계한다. 책임-주도 설계 방법은 객체지향 패러다임의 전문가들이 애플리케이션을 개발할 때 어떤 방식으로 사고하고 무엇을 기반으로 의사결정을 내리는지 잘 보여준다.
두 번째로 살펴볼 디자인 패턴은 전문가들이 반복적으로 사용하는 해결 방법을 정의해 놓은 설계 템플릿의 모음이다. 패턴은 전문가들이 특정 문제를 해결하기 위해 이미 식별해 놓은 역할, 책임, 협력의 모음이다. 패턴을 알고 있다면 바퀴를 반복적으로 발명할 필요가 없다. 여러분이 필요로 하는 역할, 책임, 협력이 디자인 패턴 안에 이미 존재하기 때문이다.
세 번째 기법은 테스트-주도 개발 방법이다. 테스트-주도 개발은 테스트를 먼저 작성하고 테스트를 통과하는 구체적인 코드를 추가하면서 애플리케이션을 완성해가는 방식을 따른다. 이름에서 풍기는 뉘앙스와 달리 테스트-주도 개발은 테스트가 아니라 설계를 위한 기법이다. 테스트-주도 개발의 핵심은 테스트 작성이 아니다. 테스트는 단지 테스트-주도 개발을 통해 얻을 수 있는 별도의 보너스 같은 것이며, 실제 목적은 구체적인 코드를 작성해나가면서 역할, 책임, 협력을 식별하고 식별된 역할, 책임, 협력이 적합한지를 피드백받는 것이다.
책임-주도 설계
객체지향 설계의 핵심은 올바른 책임과 올바른 객체에게 할당하는 것이다. 프로그래밍 과정에서 객체지향 언어를 사용하거나 UML과 같은 모델링 언어를 이용해 설계의 밑그림을 그린다고 해서 효율적이고 견고한 객체지향 시스템이 보장되는 것은 아니다. 이를 위해서는 전체 개발 단계에 걸쳐 객체의 역할과 책임, 협력을 도드라지게 만드는 기법과 체계를 따르는 것이 중요하다. 인간이 만들어왔던 다른 창조물처럼 객체지향 시스템을 창조하는 작업 역시 지속적인 훈련과 견고한 기술, 안정적인 가이드라인을 필요로 한다.
현재 가장 널리 받아들여지는 객체지향 설계 방법은 레베카 워프스브록이 고안한 책임-주도 설계 방법이다.
책임-주도 설계에서는 시스템의 책임을 객체의 책임으로 변환하고, 각 객체가 책임을 수행하는 중에 필요한 정보나 서비스를 제공해줄 협력자를 찾아 해당 협력자에게 책임을 할당하는 순차적인 방식으로 객체들의 협력 공동체를 구축한다. 책임-주도 설계는 개별적인 객체의 상태가 아니라 객체의 책임과 상호작용에 집중한다. 결과적으로 시스템은 스스로 자신을 책임질 수 있을 정도로 충분히 자율적인 동시에 다른 객체와 우호적으로 협력할 수 있을 정도로 충분히 협조적인 객체들로 이뤄진 생태계를 구성하게 된다.
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 중에 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
디자인 패턴
책임-주도 설계는 객체의 역할, 책임, 협력을 고안하기 위한 방법과 절차를 제시한다. 반면 디자인 패턴은 책임-주도 설계의 결과를 표현한다. 패턴은 모범이 되는 설계다. 패턴은 특정한 상황에서 설계를 돕기 위해 모방하고 수정할 수 있는 과거의 설계 경험이다.
일반적으로 디자인 패턴은 반복적으로 발생하는 문제와 그 문제에 대한 해법의 쌍으로 정의된다. 패턴은 해결하려고 하는 문제가 무엇인지를 명확하게 서술하고, 패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 함께 설명한다. 패턴은 반복해서 일어나는 특정한 상황에서 어떤 설계가 왜 더 효과적인지에 대한 이유를 설명한다.
특정한 상황에 적용 가능한 디자인 패턴을 잘 알고 있다면 책임-주도 설계의 절차를 순차적으로 따르지 않고도 시스템 안에 구현할 객체들의 역할과 책임, 협력 관계를 빠르고 손쉽게 포착할 수 있을 것이다. 디자인 패턴은 책임-주도 설계의 결과물인 동시에 지름길이다.
테스트-주도 개발
테스트-주도 개발은 애자일 방법론의 한 종류인 XP의 기본 프랙티스로 소개되면서 주목받기 시작한 설계 기법이다. 테스트-주도 개발의 기본 흐름은 실패하는 테스트를 작성하고, 테스트를 통과하는 가장 간단한 코드를 작성한 후, 리팩터링을 통해 중복을 제거하는 것이다. 테스트-주도 개발을 통해 '작동하는 깔끔한 코드(clean code that works)'를 얻을 수 있다.
테스트-주도 개발이 응집도가 높고 결합도가 낮은 클래스로 구성된 시스템을 개발할 수 있게 하는 최상의 프랙티스인 것은 맞지만 객체지향에 대한 경험이 적은 초보자들은 개발을 주도하기 위해 어떤 테스트를 어떤 식으로 작성해야 하는지를 결정하는 데 큰 어려움을 느낀다. 테스트-주도 개발은 객체가 이미 존재한다고 가정하고 객체에게 어떤 메시지를 전송할 것인지에 관해 먼저 생각하라고 충고한다. 그러나 이 같은 종류의 충고는 역할, 책임, 협력의 관점에서 객체를 바라보지 않을 경우 무의미하다.
테스트-주도 개발은 책임-주도 설계의 기본 개념을 따른다. 사전 설계 없이 테스트-주도 개발을 진행하는 개발자들은 책임-주도 설계의 단계적인 절차와 기법들을 짧은 시간에 감각적으로 수행하는 사람들이다. 때로는 요구사항으로부터 특정 패턴이 필요하다는 것을 눈치채고 패턴을 목표로 빠르게 테스트를 작성한다. 협력 안에서 객체의 역할과 책임이 무엇이고 이것이 클래스와 같은 프로그래밍 언어 장치로 구현되는 방식에 대한 감각을 갖춰야만 효과적인 테스트를 작성할 수 있다. 테스트-주도 개발은 책임-주도 설계를 통해 도달해야 하는 목적지를 테스트라는 안전장치를 통해 좀 더 빠르고 견고한 방법으로 도달할 수 있도록 해주는 최상의 설계 프랙티스다.
요점은 테스트-주도 개발은 다양한 설계 경험과 패턴에 대한 지식이 없는 사람들의 경우에는 온전한 혜택을 누리기가 어렵다는 것이다. 초보 개발자들이 테스트-주도 개발 기법을 따르지 않는 경우보다 따르는 경우에 더 훌륭한 코드를 작성하는 것은 사실이지만 그렇다고 해서 결코 경험 많은 개발자들이 테스트-주도 개발 없이 작성한 코드보다 더 훌륭한 코드를 작성할 수는 없다.
테스트-주도 개발은 객체지향에 대한 깊이 있는 지식을 요구한다. 테스트를 작성하기 위해 객체의 메서드를 호출하고 반환값을 겁증하는 것은 순간적으로 객체가 수행해야 하는 책임에 관해 생각한 것이다. 테스트에 필요한 간접 입력 값을 제공하기 위해 스텁(stub)을 추가하거나 간접 출력 값을 검증하기 위해 목 객체(mock object)를 사용하는 것은 객체와 협력해야 하는 협력자에 관해 고민한 결과를 코드로 표현한 것이다.
05. 책임과 메시지
자율적인 책임
객체가 어떤 행동을 하는 유일한 이유는 다른 객체로부터 요청을 수신했기 때문이다. 요청을 처리하기 위해 객체가 수행하는 행동을 책임이라고 한다. 따라서 자율적인 객체란 스스로의 의지와 판단에 따라 각자 맡은 책임을 수행하는 객체를 의미한다.
왕이 모자 장수에게 증언하라고 요청하는 협력을 보자. 모자 장수에게 할당된 '증언하라'라는 책임은 그 자체로 모자 장수의 자율성을 충분히 보장할 수 있을 정도로 포괄적이고 추상적이면서도 모자 장수가 해야할 일을 명확하게 지시하고 있다. 반면 모자 장수에게 '목격했던 장면을 떠올려라', '떠오르는 기억을 시간 순서대로 재구성하라', '말로 간결하게 표현하라'라는 요청을 순차적으로 하는 경우, 할당된 좀 더 상세한 수준의 책임들은 모자 장수의 자율성을 제한한다.
객체지향 세계는 자율적인 객체들의 공동체라는 점을 명심하라. 객체가 자율적이기 위해서는 객체에게 할당되는 책임의 수준 역시 자율적이어야 한다.
어떤 책임이 자율적인지를 판단하는 기준은 문맥에 따라 다르다는 사실에 유의하라. 재판이라는 협력 안에서는 '증언하라'라는 책임이 모자 장수의 자율권을 보장하는 가장 적절한 수준의 책임이지만 다른 상황에서는 오히려 '설명하라'라는 책임이 자율권을 보장하는 최선의 선택이 될 수 있다. 어떤 책임이 가장 적절한가는 설계 중인 협력이 무엇인가에 따라 달라진다. 이런 모호함이 객체지향 설계를 난해하면서도 매력적인 예술로 만드는 이유다.
성급한 일반화의 오류를 피하고 현재의 문맥에 가장 적합한 책임을 선택할 수 있는 날카로운 안목을 기르기 바란다.
자율적인 책임의 특징은 객체가 '어떻게' 해야 하는가가 아니라 '무엇'을 해야 하는가를 설명한다는 것이다. '증언한다'라는 책임은 모자 장수가 협력을 위해 '무엇'을 해야 하는지는 결정하지만 '어떻게' 해야 하는지에 대해서는 전혀 언급하지 않는다. 증언할 방법은 모자 장수가 자율적으로 선택할 수 있다.
메시지와 메서드
메시지는 '어떻게' 수행될 것인지는 명시하지 않는다. 메시지는 단지 오퍼레이션을 통해 '무엇'이 실행되기를 바라는지만 명시하며, 어떤 메서드를 선택할 것인지는 전적으로 수신자의 결정에 좌우된다.
다형성
메시지와 메서드의 차이와 관계를 이해하고 나면 객체지향의 핵심 개념인 다형성을 쉽게 이해할 수 있다. 다형성이란 서로 다른 유형의 객체가 동일한 메시지에 대해 서로 다르게 반응하는 것을 의미한다. 좀 더 구체적으로 말해 서로 다른 타입에 속하는 객체들이 동일한 메시지를 수신할 경우 서로 다른 메서드를 이용해 메시지를 처리할 수 있는 메커니즘을 가리킨다.
유연하고 확장 가능하고 재사용성이 높은 협력의 의미
왕은 오직 수신자가 메시지를 이해할 수 있다는 사실만 알고 있는 상태에서 협력에 참여한다.
송신자가 수신자에 대해 매우 적은 정보만 알고 있더라도 상호 협력이 가능하다는 사실은 설계의 품질에 큰 영향을 미친다.
첫째, 협력이 유연해진다. 송신자는 수신자가 메시지를 이해한다면 누구라도 상관하지 않는다. 송신자는 수신자에 대한 어떤 가정도 하지 않기 때문에 수신자를 다른 타입의 객체로 대체하더라도 송신자는 알지 못한다. 따라서 송신자에 대한 파급효과 없이 유연하게 협력을 변경할 수 있다.
둘째, 협력이 수행되는 방식을 확장할 수 있다. 송신자에게 아무런 영향도 미치지 않고서도 수신자를 교체할 수 있기 때문에 협력의 세부적인 수행 방식을 쉽게 수정할 수 있다. 왕과 증인 사이에는 '증언하라'라는 메시지를 기반으로 느슨한 관계만 존재하기 때문에 재판장에 출석하지 않고 동영상만 보내는 객체라도 책임만 완수할 수 있다면 쉽게 수용할 수 있다. 협력을 확장하고 싶은가? 간단하게 새로운 유형의 객체를 협력에 끼워 맞추기만 하면 된다.
셋째, 협력이 수행되는 방식을 재사용할 수 있다. 협력에 영향을 미치지 않고서도 다양한 객체들이 수신자의 자리를 대체할 수 있기 때문에 다양한 문맥에서 협력을 재사용할 수 있다. 재판이라는 협력은 모자 장수가 존재하는 곳에서도, 요리사가 존재하는 곳에서도, 앨리스가 존재하는 곳에서도, 심지어 아직까지 알려지지 않은 미래의 누군가가 존재하는 곳에서도 재사용 가능하다.
송신자와 수신자를 약하게 연결하는 메시지
무엇이 유연하고 확장 가능하고 재사용성이 높은 협력을 가능하게 하는가? 얼핏 보기에는 모든 것이 다형성의 축복처럼 느껴지지만 이 모든 것은 다형성을 지탱하는 메시지가 존재하기 때문에 가능한 것이다. 메시지는 송신자와 수신자 사이의 결합도를 낮춤으로써 설계를 유연하고, 확장 가능하고, 재사용 가능하게 만든다.
메시지를 따라라
객체지향의 핵심, 메시지
클래스 기반의 객체지향 언어를 사용하는 대부분의 사람들은 객체지향 애플리케이션을 클래스의 집합으로 생각한다. 프로그래머 입장에서 클래스는 실제로 볼 수 있고 수정할 수 있는 구체적인 존재다. 많은 객체지향 책에서는 클래스를 선언하고 속성과 메서드를 정의하는 방법에 초점을 맞춘다. 대부분의 입문자들은 클래스 간의 상속 관계가 객체지향 설계를 가치 있게 만드는 핵심적인 메커니즘이라고 배운다. 객체지향 설계에 관한 많은 논의가 클래스에 어떤 책임을 할당하고 클래스 간의 의존성을 어떻게 관리할 것인가에 집중된다. 어쨌든 훌륭한 객체지향 프로그래밍의 목적은 훌륭한 클래스를 창조하는 것이 아니던가?
클래스가 코드를 구현하기 위해 사용할 수 있는 중요한 추상화 도구인 것은 사실이지만 객체지향의 강력함은 클래스가 아니라 객체들이 주고받는 메시지로부터 나온다. 객체지향 애플리케이션은 클래스가 아니라 객체들이 주고받는 메시지로부터 나온다. 객체지향 애플리케이션은 클래스를 이용해 만들어지지만 메시지를 통해 정의된다. 실제로 애플리케이션을 살아있게 만드는 것은 클래스가 아니라 객체다. 그리고 이런 객체들의 윤곽을 결정하는 것이 바로 객체들이 주고받는 메시지다.
클래스는 단지 동적인 객체들의 특성과 행위를 정적인 텍스트로 표현하기 위해 사용할 수 있는 추상화 도구일 뿐이다. 중요한 것은 클래스가 아니라 객체다. 클래스를 정의하는 것이 먼저가 아니라 객체들의 속성과 행위를 식별하는 것이 먼저다.
What/Who 사이클
책임-주도 설계의 핵심은 어떤 행위가 필요한지를 먼저 결정한 후에 이 행위를 수행할 객체를 결정하는 것이다. 이 과정을 흔히 What/Who 사이클이라고 한다. 먼저 '어떤 행위(what)'를 수행할 것인지를 결정한 후에 '누가(who)' 그 행위를 수행할 것인지를 결정해야 한다는 것이다. 여기서 '어떤 행위'가 바로 메시지다.
객체의 행위를 결정하는 것은 객체 자체의 속성이 아니라는 점에 주목하라. 책임-주도 설계의 관점에서는 어떤 객체가 어떤 특성을 가지고 있다고 해서 반드시 그와 관련된 행위를 수행할 것이라고 가정하지 않는다. 반대로 행위를 먼저 식별한 후에 행위를 수행할 적절한 객체를 찾는다.
결론적으로 협력이라는 문맥 안에서 필요한 메시지를 먼저 결정한 후에 메시지를 수신하기에 적합한 객체를 선택한다. 그리고 수신된 메시지가 객체의 책임을 결정한다. 이것은 객체를 고립된 상태로 놓고 어떤 책임이 적절한지를 결정하는 것과는 근본적으로 다른 접근 방법이다. 어떤 객체도 섬이 아니라는 말은 협력이라는 문맥 안에서 객체의 책임과 역할을 결정하라는 의미를 내포하고 있다.
묻지 말고 시켜라
메시지를 먼저 결정하고 객체가 메시지를 따르게 하는 설계 방식은 객체가 외부에 제공하는 인터페이스가 독특한 스타일을 따르게 한다. 이 스타일을 묻지 말고 시켜라(Tell, Don't Ask) 스타일 또는 데메테르 법칙(Law of Demeter)이라고 한다.
책임-주도 설계는 객체가 아니라 객체들이 주고받는 메시지에 초점을 맞추게 함으로써 객체지향의 장점을 극대화한다. What/Who 사이클은 어떤 객체가 필요한지를 생각하지 말고 어떤 메시지가 필요한지를 먼저 고민하라고 조언한다. 메시지를 결정하기 전까지는 객체에 관해 고민하지 말아야 한다. 일단 메시지가 결정된 후에야 이 메시지를 처리할 객체를 선택한다.
메시지를 결정하는 시점에서는 어떤 객체가 메시지를 수신할 것인지를 알 수 없기 때문에 당연히 메시지 송신자는 메시지를 수신할 객체의 내부 상태를 알 수 없기 때문에 당연히 메시지 송신자는 메시지를 수신할 객체의 내부 상태를 볼 수 없다. 따라서 메시지 중심의 설계는 메시지 수신자의 캡슐화를 증진시킨다. 또한 송신자가 수신자의 내부 상태를 미리 알 수 없기 때문에 송신자와 수신자가 느슨하게 결합된다.
메시지를 먼저 결정하고 메시지에 적합한 객체를 선택하는 방식을 따르다 보면 객체 사이의 협력 방식을 특징짓는 한 가지 스타일에 이르게 된다. 송신자는 수신자가 어떤 객체인지는 모르지만 자신이 전송한 메시지를 잘 처리할 것이라는 것을 믿고 메시지를 전송할 수밖에 없다. 이런 스타일의 협력 패턴은 '묻지 말고 시켜라'라는 이름으로 널리 알려져 있다.
'묻지 말고 시켜라' 스타일은 객체지향 애플리케이션이 자율적인 객체들의 공동체라는 사실을 강조한다. 객체는 다른 객체의 결정에 간섭하지 말아야 하며, 모든 객체는 자신의 상태를 기반으로 스스로 결정을 내려야 한다.
객체는 다른 객체의 상태를 묻지 말아야 한다. 객체가 다른 객체의 상태를 묻는다는 것은 메시지를 전송하기 이전에 객체가 가져야 하는 상태에 관해 너무 많이 고민하고 있었다는 증거다. 고민을 연기하라. 단지 필요한 메시지를 전송하기만 하고 메시지를 수신하는 객체가 스스로 메시지의 처리 방법을 결정하게 하라.
결과적으로 '묻지 말고 시켜라' 스타일은 객체를 자율적으로 만들고 캡슐화를 보장하며 결합도를 낮게 유지시켜 주기 때문에 설계를 유연하게 만든다.
샌디 메츠는 '묻지 말고 시켜라' 스타일이란 "메시지가 '어떻게 해야 하는지를 지시하지 말고 '무엇'을 해야 하는지를 요청"하는 것이라고 설명한다. '어떻게'에서 '무엇'으로 전환하는 것은 객체 인터페이스의 크기를 급격하게 감소시킨다. 인터페이스의 크기가 작다는 것은 외부에서 해당 객체에게 의존해야 하는 부분이 적어진다는 것을 의미한다. 결과적으로 메시지 송신자와 수신자 간의 결합도가 낮아지기 때문에 걸계를 좀 더 유연하게 만들 여지가 많아지고 의도 역시 명확해진다.
메시지를 믿어라
객체지향 시스템은 협력하는 객체들의 연결망이다. 전체 시스템은 메시지를 전송하는 객체와 전송된 메시지를 이해할 수 있는 객체를 연결하고 상화 관련짓는 과정을 통해 구축된다.
메시지를 전송하는 객체의 관점에서 자신이 전송하는 메시지를 수신할 수 있다면 협력하는 객체의 종류가 무엇인지는 중요하지 않다. 중요한 것은 메시지를 수신하는 객체가 메시지의 의미를 이해하고 메시지를 전송한 객체가 의도한 대로 요청을 처리할 수 있는지 여부다. 객체의 구체적인 타입과 무관하게 전송된 메시지를 이해할 수 있는 객체들을 서로 연결하고 상호 협력 가능하게 만드는 것은 유연하고 재사용 가능한 설계를 낳는 토양이다.
메시지를 이해할 수만 있다면 다양한 타입의 객체로 협력 대상을 자유롭게 교체할 수 있기 때문에 설계가 좀 더 유연해진다. 메시지를 기반으로 다양한 타입의 객체들이 동일한 협력 과정에 참여할 수 있기 때문에 다양한 상황에서 협력을 재사용할 수 있다. 재사용 가능하고 확장 가능한 객체지향 설계를 구축하기 위한 핵심적인 도구인 다형성은 개별 객체가 아니라 객체들이 주고받는 메시지에 초점을 맞출 대 비로소 그 진가를 발휘하게 된다. 메시지를 중심으로 설계된 구조는 유연하고 확장 가능하며 재사용 가능하다.
메시지를 믿어라. 그러면 자율적인 책임은 저절로 따라올 것이다.
인터페이스와 구현의 분리
객체 관점에서 생각하는 방법
맷 와이스펠드는 객체지향적인 사고 방식을 이해하기 위해서는 다음의 세 가지 원칙이 중요하다고 주장한다. 이것들은 모두 객체의 인터페이스에 관련된 것이다.
- 좀 더 추상적인 인터페이스
- 최소 인터페이스
- 인터페이스와 구현 간에 차이가 있다는 점을 인식
구현
객체지향의 세계에서 내부 구조와 작동 방식을 가리키는 고유의 용어는 구현(implementation)이다. 객체를 구성하지만 공용 인터페이스에 포함되지 않는 모든 것이 구현에 포함된다.
객체는 상태를 가진다. 상태는 어떤 식으로든 객체에 포함되겠지만 객체 외부에 노출되는 공용 인터페이스의 일부는 아니다. 따라서 상태를 어떻게 표현할 것인가는 객체의 구현에 해당한다.
객체는 행동을 가진다. 행동은 메시지를 수신했을 때만 실행되는 일종의 메시지 처리 방법이다. 이 처리 방법을 메서드라고 한다. 메서드를 구성하는 코드 자체는 객체 외부에 노출되는 공용 인터페이스의 일부는 아니기 때문에 객체의 구현 부분에 포함된다.
객체의 외부와 내부를 분리하라는 것은 결국 객체의 공용 인터페이스와 구현을 명확하게 분리하라는 말과 동일하다.
인터페이스와 구현의 분리 원칙
훌륭한 객체란 구현을 모른 채 인터페이스만 알면 쉽게 상호작용할 수 있는 객체를 의미한다. 이것은 객체를 설계할 때 객체 외부에 노출되는 인터페이스와 객체의 내부에 숨겨지는 구현을 명확하게 분리해서 고려해야 한다는 것을 의미한다. 이를 인터페이스와 구현의 분리(separation of interface and implementation) 원칙이라고 한다.
결론적으로 객체 설계의 핵심은 객체를 두 개의 분리된 요소로 분할해 설계하는 것이다. 그것은 바로 외부에 공개되는 인터페이스와 내부에 감춰지는 구현이다.
인터페이스와 구현의 분리 원칙이 왜 중요한가? 그것은 소프트웨어는 항상 변경되기 때문이다. 수많은 객체들이 물고 물리며 돌아가는 객체지향 공동체에서 어떤 객체를 수정했을 때 어떤 객체가 영향을 받는지를 판단하는 것은 거의 곡예에 가깝다.
캡슐화
객체의 자율성을 보존하기 위해 구현을 외부로부터 감추는 것을 캡슐화라고 한다. 객체는 상태와 행위를 함께 캡슐화함으로써 충분히 별벽적이고 만족스러울 정도로 자율적인 존재가 될 수 있다. 캡슐화를 정보 은닉(information hiding)이라고 부르기도 한다.
- 상태와 행위의 캡슐화
- 사적인 비밀의 캡슐화
06. 객체 지도
길을 직접 알려주는 방법이 기능적이고 해결 방법 지향적인 접근법이라면 지도를 이용하는 방법은 '구조적이고 문제 지향적인 접근법(structural, probelm-directed approach)'이다. 지도는 길을 찾는 데 필요한 구체적인 기능이 아니라 길을 찾을 수 있는 '구조'를 제공한다.
길을 묻는 방법은 현재의 마을에서 다른 마을로 이동하는 현재의 요구만을 만족시킬 수 있다. 이에 반해 지도는 현재의 목적뿐만 아니라 다양한 목적을 위해 재사용될 수 있다. 즉, 지도는 범용적이다. 지도를 제작한 사람들이 지도를 만들 때는 지도를 사용할 사람들이 구체적으로 어던 목적으로 지도를 사용할지 알지 못한다. 지도를 사는 사람들은 마을로 가는 길을 찾을 수도 있고, 기차역으로 가는 길을 찾을 수도 있다. 그럼에도 지도는 다른 마을까지 어떤 길을 따라 이동할 것인가라는 현재의 목적뿐만 아니라 집으로 가기 위해 버스 터미널이나 기차역까지 어떤 길을 따라 이동할 것인가라는 새로운 목적까지도 만족시킬 수 있다.
지도가 범용적인 이유는 지도를 사용하려는 사람들이 원하는 '기능'에 비해 지도에 표시된 '구조'가 더 안정적이기 때문이다. 앞에서 살펴본 것처럼 지도를 사용하는 사람들의 요구사항은 계속 바뀐다. 마을까지의 길을 찾을 수도 있고 기차역으로 이동하는 길을 찾을 수도 있다. 기능에 대한 요구사항이 계속 변함에도 지도는 이 모든 요구사항을 수용할 수 있는데, 지도는 기능에 비해 상대적으로 잘 변하지 않는 안정적인 지형 정보를 기반으로 하고 있기 때문이다.
지도 은유의 핵심은 기능이 아니라 구조를 기반으로 모델을 구축하는 편이 좀 더 범용적이고 이해하기 쉬우며 변경에 안정적이라는 것이다. 사람들의 요구사항은 계속 변하기 때문에 모델이 제공해야 하는 기능 역시 이에 따라 지속적으로 변할 수밖에 없다. 따라서 기능을 중심으로 구조를 종속시키는 접근법은 범용적이지 않고 재사용이 불가능하며 변경에 취약한 모델을 낳게 된다. 이와 달리 안정적인 구조를 중심으로 기능을 종속시키는 접근법은 범용적이고 재사용 가능하며 변경에 유연하게 대처할 수 있는 모델을 만든다. 사람들에게 직접 길을 묻는 접근법은 기능에 구조를 종속시키는 방법이다.
전통적인 소프트웨어 개발 방법은 변경이 빈번하게 발생하는 기능에 안정적인 구조를 종속시키는 길을 묻는 방법과 유사하다. 반면에 객체지향 개발 방법은 안정적인 구조에 변경이 빈번하게 발생하는 기능을 종속시키는 지도의 방법과 유사하다. 이것이 객체지향이 과거의 전통적인 방법보다 범용적이고, 재사용성이 높으며, 변경에 안정적인 이유다. 즉, 객체지향은 자주 변경되는 기능이 아니라 안정적인 구조를 기반으로 시스템을 구조화한다.
앞에서 객체지향을 역할과 책임을 수행하며 협력하는 자율적인 객체들의 공동체로 정의했다. 자율적인 객체들로 시스템을 분할하는 객체지향이 강력한 이유는 사람들이 실세계의 현상을 인지하고 이해하는 관점을 그대로 소프트웨어에 투영할 수 있기 때문이다.
자주 변경되는 기능이 아니라 안정적인 구조를 따라 역할, 책임, 협력을 구성하라. 이것이 이번 장의 주제다.
기능 설계 대 구조 설계
모든 소프트웨어 제품의 설계에는 두 가지 측면이 존재한다. 하나는 '기능' 측면의 설계이고, 다른 하나는 '구조' 측면의 설계다. 기능 측면의 설계는 제품이 사용자를 위해 무엇을 할 수 있는지에 초점을 맞춘다. 구조 측면의 설계는 제품의 형태가 어떠해야 하는지에 초점을 맞춘다. 설계의 가장 큰 도전은 기능과 구조라는 두 가지 측면을 함께 녹여 조화를 이루도록 만드는 것이다.
소프트웨어가 사용자에게 가치 있는 이유는 사용자가 필요로 하는 기능을 제공하기 때문이다. 이런 관점에서 소프트웨어를 개발하는 일차적인 이유는 사용자에게 훌륭한 기능을 제공하기 위해서다. 소프트웨어의 기능은 사용자가 금전적인 대가를 지불하고서라도 구매할 수 있을 정도로 매력적이어야 한다. 따라서 소프트웨어를 개발하는 초기 단계에서는 사용자가 무엇을 원하는지, 그리고 사용자가 원하는 것을 만족시키기 위해 시스템이 어떤 기능을 제공해야 하는지에 초점을 맞춰야 한다.
훌륭한 기능이 훌륭한 소프트웨어를 만드는 충분조건이라고 한다면 훌륭한 구조는 훌륭한 소프트웨어를 만들기 위한 필요조건이다. 성공적인 소프트웨어들이 지닌 공통적인 특징은 훌륭한 기능을 제공하는 동시에 사용자가 원하는 새로운 기능을 빠르고 안정적으로 추가할 수 있다는 것이다. 비록 최종 사용자들이 소프트웨 어의 내부 구조를 볼 수는 없지만 깔끔하고 단순하며 유지보수하기 쉬운 설계는 사용자의 변하는 요구사항을 반영할 수 있도록 쉽게 확장 가능한 소프트웨어를 창조할 수 있는 기반이 된다.
요구사항이 변경되지 않는다면 개발자의 삶은 좀 더 단순하고 지루했지도 모른다. 요구사항이 변경되지 않는다면 코드를 어떻게 작성하는지를 가지고 이렇게 골치를 썩을 필요도 없었을 것이다. 개발된 코드를 성공적으로 테스트한 후 운영 환경에 배포하고 나면 더는 그 코드를 볼 일도, 수정할 일도 없기 때문이다. 따라서 사용자가 원하는 기능을 제공할 수 있다면 설계가 어떠한가는 그다지 중요한 문제가 아니다. 물론 요구사항이 절대 변경되지 않는다는 전제하에서 말이다.
미래의 변경에 대비할 수는 있지만 미래의 변경을 예측할 수는 없다. 워런 웨이거의 말처럼 우리는 미래에 대해 얘기할 수 있고, 놀 수 있고, 추측할 수 있고, 깊이 생각해볼 수 있으며, 이론과 모형을 구축하고 이에 영향을 미치는 정량 데이터를 수집할 수 있지만 미래를 전혀 알지 못한다. 불확실한 미래의 변경을 예측하고 이를 성급하게 설계에 반영하는 것은 불필요하게 복잡한 설계를 낳을 뿐이다. 우리는 미래를 예측할 수 없다. 단지 대비할 수 있을 뿐이다.
미래에 대비하는 가장 좋은 방법은 변경을 예측하는 것이 아니라 변경을 수용할 수 있는 선택의 여지를 설계에 마련해 놓는 것이다. 훌륭한 설계자는 미래에 구체적으로 어떤 변경이 발생할 것인지를 예측하지 않는다. 단지 언젠가는 변경이 발생할 것이며 아직까지는 그것이 무엇인지 모른다는 사실을 겸허하게 받아들인다. 좋은 설계는 나중에라도 변경할 수 있는 여지를 남겨 놓는 설계다. 설계를 하는 목적은 나중에 설계하는 것을 허용하는 것이며, 설계의 일차적인 목표는 변경에 소요되는 비용을 낮추는 것이다.
지도 은유를 통해 살펴본 것처럼 변경에 대비하고 변경의 여지를 남겨 놓는 가장 좋은 방법은 자주 변경되는 기능이 아닌 안정적인 구조를 중심으로 설계하는 것이다. 전통적인 기능 분해(functional decomposition)는 자주 변경되는 기능을 중심으로 설계한 후 구조가 기능에 따르게 한다. 이것이 바로 전통적인 기능 분해 방법이 변경에 취약한 이유다. 기능 분해 방법의 경우 시스템 기능은 더 작은 기능으로 분해하고 각 기능은 서로 밀접하게 관련된 하나의 덩어리를 이루기 때문에 기능이 변경될 경우 기느으이 축을 따라 설계된 소프트웨어가 전체적으로 요동치게 된다.
이에 비해 객체지향 접근방법은 자주 변경되지 않는 안정적인 객체 구조를 바탕으로 시스템 기능을 객체 간의 책임으로 분배한다. 객체지향은 객체의 구조에 집중하고 기능이 객체의 구조를 따르게 만든다. 시스템 기능은 더 작은 책임으로 분할되고 적절한 객체에게 분배되기 때문에 기능이 변경되더라도 객체 간의 구조는 그대로 유지된다.
이것이 객체를 기반으로 책임과 역할을 식별하고 메시지를 기반으로 객체들의 협력 관계를 구축하는 이유다. 안정적인 객체 구조는 변경을 수용할 수 있는 유연한 소프트웨어를 만들 수 있는 기반을 제공한다.
두 가지 재료: 기능과 구조
- 구조는 사용자나 이해 관계자들이 도메인에 관해 생각하는 개념과 개념들 간의 관계로 표현한다.
- 기능은 사용자의 목표를 만족시키기 위해 책임을 수행하는 시스템의 행위로 표현한다.
일반적으로 기능을 수집하고 표현하기 위한 기법을 유스케이스 모델링이라고 하고 구조를 수집하고 표현하기 위한 기법을 도메인 모델링이라고 한다. 쉽게 예상할 수 있는 것처럼 두 가지 모델링 활동의 결과물을 각각 유스케이스와 도메인 모델이라고 한다.
안정적인 재료: 구조
소프트웨어를 사용하는 사람들은 자신이 관심을 가지고 있는 특정한 분야의 문제를 해결하기 위해 소프트웨어를 사용한다. 이처럼 사용자가 프로그램을 사용하는 대상 분야를 도메인이라고 한다.
도메인 모델에서 모델이란 대상을 단순화해서 표현한 것이다. 모델은 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태다. 모델은 복잡성의 바다에서 길을 잃지 않고 중요한 문제에 집중할 수 있도록 필요한 지식만 재구성한 것이다. 즉, 대상을 추상화하고 단순화한 것이다. 모델을 사용하면 현재의 문제와 관련된 측면은 추상화하고 그 밖의 관련 없는 세부 사항에 대해서는 무시할 수 있다. 모델은 복잡성을 관리하기 위해 사용하는 기본적인 도구다.
도메인과 모델의 정의를 연결하면 도메인 모델을 쉽게 정의할 수 있다. 도메인 모델이란 사용자가 프로그램을 사용하는 대상 영역에 관한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태다. 도메인 모델은 소프트웨어가 목적하는 영역 내의 개념과 개념 간의 관계, 다양한 규칙이나 제약 등을 주의 깊게 추상화한 것이다. 도메인 모델은 소프트웨어 개발과 관련된 이해관계자들이 도메인에 대해 생각하는 관점이다.
도메인 모델은 단순히 다이어그램이 아니다. 도메인 모델은 이해관계자들이 바라보는 멘탈 모델이다. 멘탈 모델이란 사람들이 자기 자신, 다른 사람, 환경, 자신이 상호작용하는 사물에 대해 갖는 모형이다. 사람들은 세상에 존재하는 현상을 이해하고 현상에 반응하기 위해 자신의 마음 속에 멘탈 모델을 구축한다. 소프트웨어 사용자들 역시 도메인에 존재하는 현상에 반응하기 위해 도메인과 관련된 멘탈 모델을 형성한다.
도널드 노먼은 제품을 설계할 때 제품에 관한 모든 것이 사용자들이 제품에 대해 가지고 있는 멘탈 모델과 정확하게 일치해야 한다고 주장한다. 사용자들은 자신의 멘탈 모델과 유사한 방식으로 제품이 반응하고 움직일 것이라고 기대하기 때문에 훌륭한 디자인이란 사용자가 예상하는 방식에 따라 정확하게 반응하는 제품을 만드는 것이다.
도메인 모델은 도메인에 대한 사용자 모델, 디자인 모델, 시스템 이미지를 포괄하도록 추상화한 소프트웨어 모델이다. 따라서 도메인 모델은 소프트웨어에 대한 멘탈 모델이다.
도메인의 모습을 담을 수 있는 객체지향
도널드 노먼의 주장을 요약하면 최종 제품은 사용자의 관점을 반영해야 한다는 것이다. 이것은 소프트웨어 개발에도 동일하게 적용할 수 있다. 최종 코드는 사용자가 도메인을 바라보는 관점이며, 설계자가 시스템의 구조를 바라보는 관점인 동시에 소프트웨어 안에 구현된 코드의 모습 그 자체이기 때문이다.
따라서 도메인 모델의 세 가지 측면을 모두 모델링할 수 있는 유사한 모델링 패러다임을 사용할수록 소프트웨어 개발이 쉬워질 것이다. 객체지향은 이런 요구사항을 가장 범용적으로 만족시킬 수 있는 거의 유일한 모델링 패러다임이다.
객체지향을 사용하면 사용자들이 이해하고 있는 도메인의 구조와 최대한 유사하게 코드를 구조화할 수 있다. 객체지향은 사람들이 만지고 느끼고 볼 수 있는 실체를 시스템 안의 객체로 재창조할 수 있게 해준다. 동적인 객체가 가진 복잡성을 극복하기 위해 정적인 타입을 이용해 세상을 단순화할 수 있으며 클래스라는 도구를 이용해 타입을 코드 안으로 옮길 수 있다. 객체지향 패러다임은 사용자의 관점, 설계자의 관점, 코드의 모습을 모두 유사한 형태로 유지할 수 있게 하는 유용한 사고 도구와 프로그래밍 기법을 제공한다.
결과적으로 객체지향을 이용하면 도메인에 대한 사용자 모델, 디자인 모델, 시스템 이미지 모두가 유사한 모습을 유지하도록 만드는 것이 가능하다. 객체지향의 이러한 특징을 연결완전성, 또는 표현적 차이라고 한다.
불안정한 재료: 기능
유스케이스
유스케이스의 가치는 사용자들의 목표를 중심으로 시스템의 기능적인 요구사항들을 이야기 형식으로 묶을 수 있다는 점이다. 산발적으로 흩어져 있는 기능에 사용자 목표라는 문맥을 제공함으로써 각 기능이 유기적인 관계를 지닌 체계를 이룰 수 있게 한다. 이것은 요구사항을 기억하고 관리하는 데 필요한 다양한 정신적 과부하를 줄인다. 마틴 파울러의 말처럼 "사용자 목표가 유스케이스의 핵심이다. 유스케이스는 공통의 사용자 목표를 통해 강하게 연관된 시나리오의 집합이다."
유스케이스의 특성
첫째, 유스케이스는 사용자와 시스템 간의 상호작용을 보여주는 '텍스트'다. 유스케이스는 다이어그램이 아니다. 중요한 것은 유스케이스 안에 포함돼 있는 상호작용의 흐름이다. 유스케이스의 핵심은 사용자와 시스템 간의 상호작용을 일련의 이야기 흐름으로 표현한 것이다. 다이어그램에 노력을 쏟지 말라. 중요한 것은 유스케이스에 담겨 있는 이야기다.
둘째, 유스케이스는 하나의 시나리오가 아니라 여러 시나리오들의 집합이다. 시나리오는 유스케이스를 통해 시스템을 사용하는 하나의 특정한 이야기 또는 경로다. 이자 계산 유스케이스는 2개의 시나리오를 포함하고 있다. 첫 번재 시나리오는 예금주가 계좌를 선택하고 당이라까지의 이자액을 게산하는 것이다. 두 번째 시나리오는 예금주가 계좌를 선택하고 특정 일자까지의 이자액을 계산하는 것이다.
유스케이스는 하나의 시나리오가 아니라 이자액 계산이라는 사용자의 목표와 관련된 모든 시나리오의 집합이다. 시나리오를 유스케이스 인스턴스라고도 한다.
셋째, 유스케이스는 단순한 피처(feature) 목록과 다르다. 피처는 시스템이 수행해야 하는 기능의 목록을 단순하게 나열한 것이다. 예제 유스케이스에서 피처는 '시스템은 정기예금 정보를 보여준다'와 '시스템은 당일이나 현재 일자의 이자를 계산한다'이다. 피처의 단점은 이 두 피처를 서로 연관이 없는 독립적인 기능으로 보이게끔 만든다는 점이다. 두 피처를 '중도 해지 이자액을 계산한다'라는 유스케이스로 묶고 사용자와의 상호작용 흐름 속에서 두 피처를 포함하는 이야기를 제공함으로써 시스템의 기능에 대해 의사소통할 수 있는 문맥을 얻을 수 있다. 앞에서 언급한 것처럼 유스케이스의 강점은 유스케이스가 단순히 기능을 나열한 것이 아니라 이야기를 통해 연관된 기능들을 함께 묶을 수 있다는 점이다.
넷째, 유스케이스는 사용자 인터페이스와 관련된 세부 정보를 포함하지 말아야 한다. 위 유스케이스에는 사용자가 해지 일자를 선택하기 위해 사용자 인터페이스를 어떻게 구성해야 하는지에 대한 정보가 전혀 포함돼 있지 않다. 유스케이스는 자주 변경되는 사용자 인터페이스 요소는 배제하고 사용자 관점에서 시스템의 행위에 초점을 맞춘다. 이처럼 사용자 인터페이스를 배제한 유스케이스 형식을 본질적인 유스케이스(essential use case)라고 한다.
다섯째, 유스케이스는 내부 설계와 관련된 정보를 포함하지 않는다. 유스케이스의 목적은 연관된 시스템의 기능을 이야기 형식으로 모으는 것이지 내부 설계를 설명하는 것이 아니다. 과거의 객체지향 서적에서는 유스케이스에 나타나는 명사를 클래스로, 동사를 클래스의 메서드로 대응시키는 방식으로 객체지향 설계를 설명하기도 했지만 객체지향 설계는 그렇게 간단하지 않다. 유스케이스에서 객체 설계로의 전환은 공학적인 규칙과 원칙을 기반으로 한 변환 작업이 아니라 경험과 상식과 의사소통을 기반으로 한 창조 작업이다.
유스케이스는 설계 기법도, 객체지향 기법도 아니다.
유스케이스는 시스템이 외부에 제공해야 하는 행위만 포함하기 때문에 유스케이스로부터 시스템의 내부 구조를 유추할 수 있는 방법은 존재하지 않는다. 사실 유스케이스는 객체지향과도 상관이 없다. 유스케이스는 단지 기능적 요구사항을 사용자의 목표라는 문맥을 중심으로 묶기 위한 정리 기법일 뿐이다.
유스케이스와 객체의 구조 사이에는 커다란 간격이 존재한다. 둘 사이의 간격을 자동으로 없앨 수 있는 어떤 방법도 존재하지 않는다. 유스케이스를 객체로 변환하는 작업은 순수하게 창조적이고 예술적인 작업이다. 유스케이스를 기반으로 객체의 구조를 쉽게 추출할 수 있다는 어설픈 설명에 속지 마라. 유스케이스는 객체의 구조나 책임에 대한 어떤 정보도 제공하지 않는다.
재료 합치기: 기능과 구조의 통합
도메인 모델, 유스케이스, 그리고 책임-주도 설계
불안정한 기능을 안정적인 구조 안에 담음으로써 변경에 대한 파급효과를 최소화하는 것은 훌륭한 객체지향 설계자가 갖춰야 할 기본적인 설계 능력이다. 도메인 모델은 안정적인 구조를 개념화하기 위해, 유스케이스는 불안정한 기능을 서술하기 위해 가장 일반적으로 사용되는 도구다. 변경에 유연한 소프트웨어를 만들기 위해서는 유스케이스에 정리된 시스템의 기능을 도메인 모델을 기반으로 한 객체들의 책임으로 분배해야 한다.
객체지향 패러다임은 모든 것이 객체라는 사상에서 출발한다. 따라서 유스케이스에 명시된 기능을 구현하는 프로그래머는 시스템을 사용자로부터 전송된 메시지를 수행하기 위해 책임을 수행하는 거대한 자율적인 객체로 본다. 시스템은 사용자와 만나는 경계에서 사용자의 목표를 만족시키기 위해 사용자와의 협력에 참여하는 커다란 객체다. 사용자에게 시스템이 수행하기로 약속한 기능은 결국 시스템의 책임으로 볼 수 있다. 사용자의 관점에서 시스템은 자신이 전송한 메시지에 응답하는 데 필요한 책임을 수행하는 일종의 객체다.
시스템에 할당된 커다란 책임은 이제 시스템 안의 작은 규모의 객체들이 수행해야 하는 더 작은 규모의 책임으로 세분화된다. 그렇다면 어떤 객체를 선택할 것인가? 이 시점에 도메인 모델이 무대에 등장한다. 우리는 도메인 모델에 포함된 개념을 은유하는 소프트웨어 객체를 선택해야 한다. 이것은 소프트웨어와 코드 사이의 표현적 차이를 줄이는 첫걸음이다.
협력을 완성하는 데 필요한 메시지를 식별하면서 객체들에게 책임을 할당해 나간다. 마지막으로 협력에 참여하는 객체를 구현하기 위해 클래스를 추가하고 속성과 함께 메서드를 구현하면 시스템의 기능이 완성된 것이다. 이제 코드는 불안정한 기능을 수용할 수 있는 안정적인 구조에 기반한다.
여기서 중요한 것은 견고한 객체지향 애플리케이션을 개발하기 위해서는 사용자의 관점에서 시스템의 기능을 명시하고, 사용자와 설계자가 공유하는 안정적인 구조를 기반으로 기능을 책임을 변환하는 체계적인 절차를 따라야 한다는 것이다.
스몰토크 언어를 설계한 객체지향의 선구자인 앨런 케이는 시스템을 자율적인 객체로 바라보고 더 작은 객체로 분할하는 방식의 장점에 대해 다음과 같이 설명한다.
스몰토크의 설계-그리고 실제 모습-는 우리가 설명할 수 있는 모든 것이 상태와 처리 과정을 내부로 은닉하는 행위적인 빌딩블록의 재귀적인 합성(recursive composition)으로 표현할 수 있으며, 메시지의 교환을 통해서만 이 빌딩블록들을 처리할 수 있다는 통찰에서 기인한다. ... 컴퓨터 측면에서 스몰토크는 컴퓨터 자체에 대한 개념적 재귀다. 컴퓨터를 전체보다 덜 강한 개별적인 요소-프로그래밍 언어의 일상적인 부속품인 자료 구조, 프로시저, 함수-로 분해하는 대신 각 스몰토크 객체는 컴퓨터의 전체적인 가능성을 기반으로 한 재귀다. ... 재귀적 설계의 기본 원칙은 부분이 전체와 동일한 힘을 갖게 만드는 것이다. 처음에 나는 전체(whole)를 완전한 하나의 컴퓨터로 간주했고, 사람들이 왜 컴퓨터를 자료 구조와 프로시저라는 더 약한 개념으로 분할하려고 하는지 그 이유가 궁금했다. 시분할(time sharing)이 시작한 것처럼 왜 컴퓨터를 더 작은 컴퓨터로 나누지 않는가?
기능 변경을 흡수하는 안정적인 구조
앞에서 설명한 것처럼 도메인 모델을 기반으로 객체 구조를 설계하는 이유는 도메인 모델이 안정적이기 때문이다. 도메인 모델이 안정적인 이유는 도메인 모델을 구성하는 요소가 다음과 같은 특징을 띠기 때문이다.
- 도메인 모델을 구성하는 개념은 비즈니스가 없어지거나 완전히 개편되지 않는 한 안정적으로 유지된다. 정기예금 도메인에서 정기예금과 계좌, 이자율, 이자란 개념은 정기예금이란 금융상품이 없어지거나 완전히 개편되지 않는 한 안정적으로 유지되는 개념이다.
- 도메인 모델을 구성하는 개념 간의 관계는 비즈니스 규칙을 기반으로 하기 때문에 비즈니스 정책이 크게 변경되지 않는 한 안정적으로 유지된다. 정기예금 도메인에서 이자는 정기예금이 만기가 되거나 중도 해지를 하는 경우에 한해서 단 한 번 지급된다. 따라서 계좌와 이자 간의 0..1 관계는 이와 같은 핵심적인 비즈니스 규칙이 변경되지 않는 한 동일하게 유지될 것이다.
도메인 모델을 중심으로 객체 구조를 설계하고 유스케이스의 기능을 객체의 책임으로 분배하는 기본적인 객체지향 설계 방식의 유연함을 잘 보여 준다. 비즈니스 정책이나 규칙이 크게 변경되지 않는 한 시스템의 기능이 변경되더라도 객체 간의 관계는 일정하게 유지된다. 기능적인 요구사항이 변경될 경우 책임과 객체 간의 대응 관계만 수정될 뿐이다. 이것은 변경에 대한 파급효과를 최소화하고 요구사항 변경에 유연하게 대응할 수 있는 시스템을 구축할 수 있게 한다.
객체지향의 가장 큰 장점은 도메인을 모델링하기 위한 기법과 도메인을 프로그래밍하기 위해 사용하는 기법이 동일하다는 점이다. 따라서 도메인 모델링에서 사용한 객체와 개념을 프로그래밍 설계에서의 객체와 클래스로 매끄럽게 변환할 수 있다. 앞에서 객체지향의 이 같은 특성을 연결완전성이라고 설명했다.
객체지향이 강력한 이유는 연결완전성의 역방향 역시 성립한다는 것이다. 즉, 코드의 변경으로부터 도메인 모델의 변경 사항을 유추할 수 있다. 이것은 객체지향 이전의 대부분의 개발 방법이 대응하지 못하고 쉽게 무너졌던 영역이다. 객체지향에서는 도메인 모델과 코드 모두 동일한 모델링 패러다임을 공유하기 때문에 코드의 수정이 곧 모델의 수정이 된다. 이처럼 모델에서 코드로의 매끄러운 흐름을 의미하는 연결완전성과 반대로 코드에서 모델로의 매끄러운 흐름을 의미하는 것을 가여성(reversibility)이라고 한다.
07. 함께 모으기
마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 관해 설명한다. 세 가지 관점을 각각 개념 관점, 명세 관점, 구현 관점이라고 부른다.
개념 관점(Conceptual Perspective)에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다. 도메인이란 사용자들이 관심을 가지고 있는 특정 분야나 주제를 말하며 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발된다. 이 관점은 사용자가 도메인을 바라보는 관점을 반영한다. 따라서 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심이다.
명세 관점(Specification Perspective)에 이르면 사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다. 명세 관점은 도메인의 개념이 아니라 실제로 소프트웨어 안에서 살아 숨쉬는 객체들의 책임에 초점을 맞춘다. 즉, 객체의 인터페이스를 바라보게 된다. 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춘다. 인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 기본적인 원칙이라는 점을 기억하라. 안타깝게도 대부분의 객체지향 언어가 인터페이스와 구현을 클래스 안으로 섞어 버리기 때문에 많은 설계자들이 인터페이스와 구현을 분리하는 것이 얼마나 중요한지를 잊어버리곤 한다. 객체지향 설계 분야의 오래된 격언인 "구현이 아니라 인터페이스에 대해 프로그래밍 하라"를 따르는 것은 명세 관점과 구현 관점을 명확하게 분리하는 것에서부터 시작된다.
구현 관점(implementation Perpsective)은 프로그래머인 우리에게 가장 익숙한 관점으로, 실제 작업을 수행하는 코드와 연관돼 있다. 구현 관점의 초점은 객체들이 책임을 수행하는 데 필요한 동작한 코드를 작성하는 것이다. 따라서 프로그래머는 객체의 책임을 '어떻게' 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는 데 필요한 속성과 메서들르 클래스에 추가한다.
앞의 설명이 마치 개념 관점, 명세 관점, 구현 관점의 순서대로 소프트웨어를 개발한다는 의미처럼 들릴 수도 있지만 이것은 사실은 아니다. 개념 관점, 명세 관점, 구현 관점은 동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미한다. 클래스는 세 가지 관점이라는 안경을 통해 설계와 관련된 다양한 측면을 드러낼 수 있다. 클래스가 은유하는 개념은 도메인 관점을 반영한다. 클래스의 공용 인터페이스는 명세 관점을 반영한다. 클래스의 속성과 메서드는 구현 관점을 반영핟나.
코드와 세 가지 관점
소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁으면 좁을수록 기능을 변경하기 위해 뒤적거려야 하는 코드의 양도 점점 줄어든다.
인터페이스를 수정하면 해당 객체와 협력하는 모든 객체에게 영향을 미칠 수밖에 없다. 객체의 인터페이스는 수정하기 어렵다는 사실을 명심하라. 최대한 변화에 안정적인 인터페이스를 만들기 위해서는 인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 해야 한다. 변화에 탄력적인 인터페이스를 만들 수 있는 능력은 객체지향 설계자의 수준을 가늠하는 중요한 척도다.
'리뷰 > 책 리뷰' 카테고리의 다른 글
| [소설/리뷰] 괴테는 모든 것을 말했다 (0) | 2026.01.11 |
|---|---|
| [소설/리뷰] 죽은 시인의 사회 (1) | 2025.12.25 |
| [재테크/리뷰] 박곰희 연금부자수업 (1) | 2025.09.25 |
| [소설/리뷰] 스토너 (3) | 2025.08.15 |
| [자기계발/리뷰] 소프트웨어 개발에 ChatGPT 사용하기 (1) | 2025.08.15 |