Chapter 4. 설계 품질과 트레이드오프

객체지향 설계의 핵심은 역할,책임,협력이다.

협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용 이다. 책임은 객체가 다른 객체와 협력 하기 위해 수행하는 행동이고 역할은 대체 가능한 책임의 집합이다.

책임 주도 설계라는 이름에 나오는 것처럼 저 셋중 가장 중요한 것은 책임이다.

책임이 객체지향 애플리케이션 전체의 품질을 결정한다.

객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.

객체지향 설계에 관한 두 가지 관점이 섞여 있다.

  1. 객체지향 설계의 핵심이 책임이라는 것.
  2. 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관돼 있다는 것.

훌륭한 설계는 합리적인 비용안에서 변경을 수용할 수 있는 구조를 만드는 것이다.

객체를 단순한 데이터의 집합으로 바라보는 시각은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시키는 결과를 낳기 때문에 설계가 변경에 취약해 진다.

이런 문제를 피할 수 있는 가장 좋은 방법이 객체의 책임에 초점을 맞추는 것이다.

객체지향 설계에서는 두 가지 방법을 이용해 시스템을 객체로 분할할 수 있다.

  1. 상태를 분할의 중심축으로 삼는 방법.
  2. 책임을 분할의 중심축으로 삼는 방법.

훌륭한 객체지향 설계는 데이터가 아니라 책임에 초점을 맞춰야 한다.

객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다.

상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼진다.

따라서 데이터에 초첨을 맞춘 설계는 변경에 취약할 수밖에 없다.

객체의 책임은 인터페이스에 속한다. 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는데 필요한 상태를 캡슐화해 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 방지한다.

책임에 초점을 맞추면 상대적으로 변경에 안정적인 설계를 얻을 수 있다.

캡슐화

상태와 행동을 하나의 객체에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다.

객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.

변경이될 가능성이 높은 부분을 구현이라 하고 상대적으로 안정적인 부분을 인터페이스라 한다.

객체를 설계하기 위해 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

복잡성을 다루기 위한 가장 효과적인 도구는 추상화다. 그리고 주요한 추상화 방법은 캡슐화다.

하지만 복잡성이 잘 캡슐화될 것이라고 보장할 수는 없다. 객체지향 프로그래밍을 통해 전반적으로 얻을 수 있는 장점은 오직 설계 과정 동안 캡슐화를 목표로 인식할 때 달성될 수 있다.

설계가 필요한 이유는 요구사항이 변경되기 때문이고 캡슐화가 중요한 이유는 불안정한 부분과 안전적인 부분을 분리해 변경의 영향을 통제할 수 있기 떄문이다.

캡슐화란 어떤 것을 숨긴다는 것을 의미한다. 뜻밖의 피해가 발생할 수 있는 가능성을 사전에 방지해 변경으로 부터 자유로워지는 것이다.

캡슐화는 우리를 좋은 코드로 안내하기 떄문에 가장 중요한 제1원리다.

응집도와 결합도

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목절을 위해 긴밀하게 협력한다면 높은 응집도를 가지고 서로 다른 목적을 추구하면 낮은 응집도를 가진다.

객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지 나타낸다.

결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가지고 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 낮은 결합도를 가진다.

결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지 나타낸다.

좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미하며 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계이다.

높은 응집도와 낮은 결합도를 가진 설계를 추구해야 하는 이유는 설계를 변경하기 쉽게 만들기 때문이다.

변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.

High Coupling and Low Coupling 높은 결합도(High coupling)와 낮은 결합도(Low coupling)

응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.

결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.

결합도가 높으면 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기 어려워진다.

낮은 결합도를 가진 설계는 모듈을 변경했을 때 오직 하나의 모듈만 영향을 받지만 높은 결합도를 가진 설계는 모듈을 변경했을 떄 다른 모듈을 동시에 변경해야 한다.

loosecouplinghighcohesion

클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다.

인터페이스에 대해 프로그래밍하라

What Does It Mean to Program to Interfaces?

결합도가 높아도 상관 없는 경우도 있지만 직접 장성한 코드의 경우에는 항상 불안정하며 언제라도 변경될 가능성이 높기 때문에 코드를 완성한 그 순간부터 코드를 수정할 준비를 해야한다.

그렇기 때문에 직접 작성한 코드의 경우에는 낮은 결합도를 유지하려 노력해야 한다.

응집도와 결합도는 변경과 관련이 깊기 때문에 변경의 관점에서 바라보는 것은 설계에 대해 시각을 크게 변화시킬 수 있다.

캡슐화의 정도는 응집도와 결합도에 영향을 미친다.

캡슐화를 지키면 모듈안의 응집도는 낮아지고 모듈 사이의 결합도는 낮아진다.

하지만 캡슐화를 위반하면 모듈 안의 응집도는 낮아지고 모듈 사이의 결합도는 높아진다. 그렇기 때문에 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 항상시키기 위해 노력해야한다.

데이터 중심의 시스템의 문제점

데이터 중심의 설계가 가진 대표적인 문제점은

  • 캡슐화 위반
  • 높은 결합도
  • 낮은 응집도

이렇게 3가지로 요약할 수 있다.

캡슐화 위반

객체에게 중요한 것은 책임이다. 그리고 구현을 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다.

협력에 관해 고민하지 않고 설계를 하면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.

객체가 사용될 문맥을 추측할 수밖에 없는 경우 개발자는 어떤 상황에서도 해당 객체가 사용될 수 있게 최대한 많은 접근자 메서드를 추가하게 되는 것이다.

이렇게 접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략(design by guessing strategy)라고 한다.

막연한 추측을 기반으로 설계를 하게 되면 프로그래머는 내부 상태를 드러내는 메서드를 최대한 많이 추가해야 한다는 압박에 시달리게 되고 결과적으로 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출된다.

그 결과 캡슐화의 원칙을 위반하는 변경에 취약한 설계를 하게 된다.

높은 결합도

객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.

결합도 측면에서 데이터 중심 설계가 가지는 또 다른 단점은 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다.

이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수밖에 없다.

데이터 중심의 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들어 버리기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수밖에 없다.

낮은 응집도

서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.

낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다.

  1. 변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐놓았기 떄문에 변경과 아무 상관이 없는 코드들이 영향을 받게 된다. 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.

  2. 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.

어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거이다.

단일 책임 원칙

단일 책임 원칙-책임 고립시키기(최소한의 책임을 가져라)

캡슐화를 지켜라

캡슐화는 설계의 제1원리다.

데이터 중심의 설계가 낮은 응집도와 높은 결합도로 문제가 생기게 된 근본적인 원인은 바로 캡슐화의 원칙을 위반했기 때문이다.

객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다.

스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.

스스로 자신의 데이터를 책임지는 객체

상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다.

객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

객체를 설계할때 두개의 개별적인 질문으로 분리해야 한다.

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

두 질문은 조합하면 객체 내부 상태를 저장하는 방식과 저장된 상태에 대해 호출할 수 있는 오퍼레이션의 집합을 얻을 수 있다.

즉 새로운 데이터 타입을 만들 수 있다.

캡슐화의 진정한 의미

캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다.

내부 속성을 외부로부터 감추는 것은 데이터 캡슐화라고 불리는 캡슐화의 한 종류일 뿐이다.

캡슐화라는 것은 변할 수 있는 어떤 것이라도 감추는 것이다.

그것이 속성의 타입이건 정책의 종류건 상관 없이 내부 구현의 변경으로 인해 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다.

설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화해야 한다.

높은 결합도

캡슐화 위반으로 내부 구현이 외부로 노출된 경우 클래스 사이의 결합 도는 높을 수밖에 없다.

두 객체 사이에 결합도가 높을 경우 한 객체의 구현을 변경할 때 다른 객체에게 변경의 영향이 전파될 확률이 높아진다는 사실을 기억하자.

유연한 설계를 창조하기 위해서는 캡슐화를 설계의 첫 번째 목표로 삼아야 한다.

데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다

데이터 주도 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 된다.

데이터 중심 설계 방식에 익숙한 개발자들은 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 따른다.

이것은 상태와 행동을 하나의 단위로 캡슐화하는 객체지향 패러다임에 반하는 것이다.

데이터 중심의 관점에서는 객체는 그저 단순한 데이터의 집합체일 뿐이다. 이로 인해 접근자와 수정자를 과도하게 추가하게 되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다.

데이터를 처리하는 작업과 데이터를 같은 객체 안에 두더라도 데이터에 초점이 맞춰져 있다면 만족스러운 캡슐화를 얻기 어렵다.

데이터를 먼저 결정하고 데이터를 처리하는 데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 되며 결과적으로 객체의 인터페이스는 구현을 캡슐화하는 데 실패하고 코드는 변경에 취약해진다.

데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 되고 객체 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 떄문에 변경에 취약한 코드를 낳게 된다.

데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다

객체지향 애플리케이션을 구현한다는 것은 협력하는 객체들의 공동체를 구축한다는 것을 의미한다.

협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다.

올바른 객체지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 하고 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다.

중요한 것은 객체가 다른 객체와 협력하는 방법이다.

하지만 데이터 중심 설계에서는 객체의 외부가 아니라 내부로 향하게 되고 데이터의 세부 정보를 먼저 결정하게 된다.

이렇게 되면 이미 결정된 상태에ㅅ 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워 맞출 수밖에 없다.