오브젝트 - 코드로 이해하는 객체지향 설계 / 조영호 지음 / 위키북스
객체지향에 대해 그 동안 잊고 있었던 것들을 상기시켜주고 새로운 인사이트를 줬으며 그 동안의 설계에 대해 돌이켜 보게 해준 유익한 책.
객체 사이의 의존성을 완전히 없애는 것이 정답은 아니다. 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.
객체가 어떤 데이터를 가지느냐보다는 객체에 어떤 책임을 할당할 것이냐에 초점을 맞춰야 한다.
객체지향 프로그램을 작성할 때 가장 먼저 고려하는 것은 무엇인가? 대부분의 사람들은 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민한다. 안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 객체지향 패러다임으로의 전환은 다음의 두 가지에 집중해야 한다.
첫째, 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
둘째, 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
의존성의 양면성
코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다는 것이다. 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하라. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다는 사실도 기억하라. 여러분이 훌륭한 객체지향 설계자로 성장하기 위해서는 항상 유연성과 가독성 사이에서 고민해야 한다. 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다. 이것이 객체지향 설계가 어려우면서도 매력적인 이유다.
역할,책임,협력
객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration)이다.
협력
객체지향 시스템은 자율적인 객체들의 공동체다. 객체는 고립된 존재가 아니라 시스템의 기능이라는 더 큰 목표를 달성하기 위해 다른 객체와 협력하는 사회적인 존재다. 협력은 객체지향의 세계에서 기능을 구현할 수 있는 유일한 방법이다. 두 객체 사이의 협력은 하나의 객체가 다른 객체에게 도움을 요청할 때 시작된다. 메시지 전송은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다. 메시지를 수신한 객체는 메서드를 실행해 요청에 응답한다. 여기서 객체가 메시지를 처리할 방법을 스스로 선택한다는 점이 중요하다.
협력이 설계를 위한 문맥을 결정한다. 객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력이다. 협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 한다. 협력은 객체가 필요한 이유와 객체가 수행하는 행동의 동기를 제공한다.
책임
객체의 책임은 '무엇을 알고 있는가'와 '무엇을 할 수 있는가'로 구성된다. 크레이그 라만은 이러한 분류 체계에 따라 객체의 책임을 크게 '하는 것(doing)'과 '아는 것(knowing)'의 두 가지 범주로 나누어 세분화하고 있다.
객체는 자신이 맡은 책임을 수행하는 데 필요한 정보를 알고 있을 책임이 있다. 또한 객체는 자신이 할 수 없는 작업을 도와줄 객체를 알고 있을 책임이 있다. 어떤 책임을 수행하기 위해서는 그 책임을 수행하는데 필요한 정보도 함께 알아야 할 책임이 있는 것이다.
책임할당
자율적인 객체를 만드는 가장 기본적인 방법은 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당하는 것이다. 이를 책임 할당을 위한 정보전문가 패턴이라고 부른다.
책임주도설계
책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법을 책임 주도 설계라고 부른다.
책임을 할당할 때 고려해야 하는 두 가지 요소
1. 메시지가 객체를 결정한다 - 객체에게 책임을 할당하는 데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택하는 것이 중요하다.
2. 행동이 상태를 결정한다 - 객체지향 패러다임에 갓 입문한 사람들이 가장 쉽게 빠지는 실수는 객체의 행동이 아니라 상태에 초점을 맞추는 것이다. 초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정한다. 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다.
역할 - 추상화
객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 부른다. 예) 배역과 배우. 배역과 배우 사이의 특성은 동일한 배역을 여러 명우 배우들이 연기할 수 있다는 것이다.
캡슐화, 응집도와 결합도
캡슐화의 정도가 응집도와 결합도에 영향일 미친다는 사실을 강조하고 싶다. 캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다. 캡슐화를 위반하면 모듈 안의 응집도는 낮아지고 모듈 사이의 결합도는 높아진다. 따라서 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상시키기 위해 노력하라. 캡슐화는 설계의 제1원리다.
책임 주도 설계를 향해
데이터보다 행동을 먼저 결정하라. 데이터 중심의 설계에서는 이 객체가 포함해야 하는 데이터가 무엇인가를 결정한 후에 데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가를 결정한다. 반면 책임 중심의 설계에서는 이 객체가 수행해야 하는 책임은 무엇인가를 결정한 후에 이 책임을 수행하는 데 필요한 데이터는 무엇인가를 결정한다.
협력이라는 문맥 안에서 책임을 결정하라
협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 다시 말해서 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다는 것이다. 객체를 결정한 후에 메시지를 선택하는 것이 아니라 메시지를 결정한 후에 객체를 선택해야 한다.
객체를 가지고 있기 때문에 메시지를 보내는 것이 아니다. 메시지를 전송하기 때문에 객체를 갖게 된 것이다.
퍼블릭 인터페이스
디미터법칙 - 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다. 오직 하나의 도트만 사용하라는 말로 요약되기도 한다. 무비판적으로 디미터 법칙을 수용하면 퍼블릭 인터페이스 관점에서 객체의 응집도가 낮아질 수도 있다. 기차충돌처럼 보이는 코드라도 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것이다.
묻지말고 시켜라 - 절차적인 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다. 내부의 상태를 묻는 오퍼레이션을 인터페이스에 포함시키고 있다면 더 나은 방법은 없는지 고민해 보라. 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재하는가? 그렇다면 해당 객체가 책임져야 하는 어떤 행동이 객체 외부로 누수된 것이다.
의도를 드러내는 인터페이스 - 무엇을 하는지를 드러내는 이름은 코드를 읽고 이해하기 쉽게 만들뿐만 아니라 유연한 코드를 낳는 지름길이다.
명령-쿼리 분리
순수한 가공물에게 책임 할당하기
크레이그 라만은 시스템을 객체로 분해하는 데는 크게 두 가지 방식이 존재한다고 설명한다. 하나는 표현적 분해이고 다른 하나는 행위적 분해다. 표현적 분해는 도메인 모델에 담겨 있는 개념과 관계를 따른다. 그러나 종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자의 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 크레이그 라만은 이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 순수한 가공물이라고 부른다. 순수한 가공물은 표현적 분해보다 행위적 분해에 의해 생성되는 것이 일반적이다.
이런 측면에서 객체지향이 실세계의 모방이라는 말은 옳지 않다. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라. 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다. 우리가 애플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제공하기 위해서지 실세계를 모방하거나 시뮬레이션하기 위한 것이 아니다.
유연성에 대한 조언
유연한 설계는 유연성이 필요할 때만 옳다 - 유연상은 항상 복잡성을 수반한다. 유연하지 않은 설계는 단순하고 명확하다. 유연한 설계는 복잡하고 암시적이다. 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다. 따라서 유연함은 단순성과 명확성의 희생 위에서 자라난다. 단순하고 명확한 해법이 그런대로 만적스럽다면 유연성을 제거하라. 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있다.
협력과 책임이 중요하다 - 초보자가 자주 저지르는 실수 중 하나는 객체의 역할과 책임의 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다. 중요한 비지니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다. 불필요한 싱글턴 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다. 핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것이다.
언제 상속을 사용해야 하는가?
상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 한다. 타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안 된다.
마틴 오더스키는 다음과 같은 질문을 해보고 두 질문에 모두 "예"라고 답할 수 있는 경우에만 상속을 사용하라고 조언한다.
1. 상속 관계가 is-a 관계를 모델링하는가?
2. 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
is-a관계 - 생각처럼 직관적이고 명쾌한 것은 아니다. 어휘적인 정의가 아니라 기대되는 행동에 따라 결정해야 한다. 어휘적으로 펭귄은 새지만 새의 정의에 날 수 있다는 행동이 포함된다면 펭귄은 새의 서브타입이 될 수 없다. 어떤 두 대상을 언어적으로 is-a라고 표현할 수 있더라도 일단은 상속을 사용할 예비 후보 정도로만 생각하라.
행동 호환성 - 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다. 행동이 호환될 경우에만 타입 계층으로 묶어야 한다. 여기서 중요한 것은 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이라는 것이다. 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라.