오브젝트 4장 설계품질과 트레이드오프
01. 데이터 중심의 영화 예매 시스템 #
- 가끔 나쁜 설계를 살펴보는 과정에서 통찰을 얻기도 한다.
- 이번 장에서는 데이터(상태) 중심의 관점에서 영화 예매 시스템을 설계해본다.
02. 설계 트레이드오프 #
- 데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화의 응집도, 결합도를 사용한다.
- 본격적으로 두 방법을 비교하기 전에 세 가지 품질 척도의 의미를 살펴보자.
캡슐화 #
- 상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다.
- 구현이란 나중에 변경될 가능성이 높은 어떤 것
- 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.
- 객체를 사용해 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.
- 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.
- 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다.
- 객체지향의 가장 중요한 원리가 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.
- 캡슐화로 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있다.
응집도와 결합도 #
-
응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
- 모듈내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
- 모듈 내의 요소들이 서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가짐.
-
결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.
- 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.
- 반면 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다.
-
좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미한다.
- 좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계다.
-
변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.
- 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것이고 모듈 일부만 변경된다면 응집도가 낮은 것이다.
- 응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.
- 변경으로 인해 수정되는 부분을 파악하기 위해 코드 구석구석을 헤매고 다니거나 여러 모듈을 동시에 수정할 필요 없이 한 모듈만 수정하면 된다.
-
결합도는 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.
- 결합도가 높으면 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기 어려워진다.
-
클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다.
- 인터페이스에 대해 프로그래밍하라
-
일반적으로 변경될 확률이 매우 적은 안정적인 모듈에는 의존하여 결합도가 높아도 상관없다. (표준 라이브러리 등)
- 하지만 직접 작성한 코드는 항상 불안정하며 언제라도 변경될 가능성이 높다.
03. 데이터 중심의 영화 예매 시스템의 문제점 #
- 데이터 중심의 설계는 캡슐화를 위반하고 객체 내부 구현을 인터페이스의 일부로 만든다.
- 반면 책임 중심 서례는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다.
- 캡슐화의 정도가 객체의 응집도와 결합도를 결정한다.
- 데이터 중심 설계의 대표적인 문제
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
캡슐화 위반 #
class Movie {
private Money fee;
getFee = () => this.fee;
setFee = () => this.fee;
}
- 데이터 중심으로 설계한 예시 코드는 오직 메서드를 통해서만 객체의 내부 상태에 접근한다.
- 위 코드는 직접 내부에 접근할 수 없기 때문에 캡슐화 원칙을 지키는 것 처럼 보이지만, 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
- get, set 메서드는 Movie 내부에 fee라는 이름의 변수가 존재한다는 사실을 퍼블릭 인스턴스에 노골적으로 드러낸다.
- 설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.
- 객체가 사용될 문맥을 추측할 수 밖에 없는 경우 개발자는 어떤 상황에서도 해당 객체가 사용될 수 있게 최대한 많은 접근자 메서드를 추가하게 되는 것이다.
- 앨런 홀럽은 이처럼 접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략이라고 부른다.
- 객체가 사용될 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.
- 따라서 프로그래머는 내부 상태를 드러내는 메서드를 최대한 많이 추가해야 한다는 압박에 시달릴 수 밖에 없으며 결과적으로 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출될 수밖에 없다.
높은 결합도 #
- 객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.
- 단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다.
- 결합도 측면에서 데이터 중심 설계가 가지는 또 다른 단점은 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다.
- 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.
- 데이터 중심 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들어 버리기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수 밖에 없다.
낮은 응집도 #
- 낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다
- 변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐놓았기 때문에 변경과 아무 상관이 없는 코드들이 영향을 받게 된다.
- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.
04. 자율적인 객체를 향해 #
캡슐화를 지켜라 #
- 캡슐화는 설계의 제 1원리다.
- 데이터 중심 설계가 낮은 응집도와 결합도로 박살난 원인은 바로 캡슐화를 위반했기 때문
- 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
- 여기서 말하는 메서드는 단순히 속성 하나의 값을 반환하거나 변경하는 접근자나 수정자를 의미하는 것은 아니다.
- 객체에게 의미 있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다.
class Rectangle {
private left: number;
private top: number;
private right: number;
private bottom: number;
public getLeft()
...
}
- 위 코드에서 사각형의 너비와 높이를 증가시키는 코드가 필요하다고 가정하자.
- 이 클래스를 사용하는 클라이언트 측에서 set함수를 이용해 너비와 높이를 변경하는 구현을 직접 해야 한다.
- 이는 코드 중복을 발생시킬 확률이 높으며 변경에 취약하다.
- 만약 right와 bottom 대신 length, height를 이용해서 사각형을 표현한다면? 호출부의 모든 코드를 수정해야 한다.
class Rectangle {
enlarge = (multiple: number) => {
right *= multiple;
bottom *= multiple;
};
}
- 위와같이 Rectangle 내부에서 너비와 높이를 조절하는 로직을 캡슐화 하면 두가지 문제를 해결할 수 있다.
스스로 자신의 데이터를 책임지는 객체 #
- 우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다.
- 객체는 단순한 데이터 제공자가 아니다.
- 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 중요하다.
- 객체를 설계할 때 ‘이 객체가 어떤 데이터를 포함해야 하는가?’라는 질문은 다음 두 개의 개발적인 질문으로 쪼개져야한다.
- 이 객체가 어떤 데이터를 포함해야 하는가?
- 이 객체가 데이터에 대해 수행해야 하는 오퍼레이터는 무엇인가?
05. 하지만 여전히 부족하다 #
06. 데이터 중심 설계의 문제점 #
- 캡슐화를 위반한 설계는 변경에 취약하다.
- 데이터 중심의 설계가 변경에 취약한 이유 두 가지
- 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
- 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다. #
- 데이터 중심 설계를 시작할 때 던지는 첫 번째 질문은 “이 객체가 포함해야 하는 데이터가 무엇인가?” 이다.
- 데이터는 구현의 일부이다.
- 데이터 주도 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 된다.
- 데이터 중심 설계 방식에 익숙한 개발자들은 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 따른다.
- 이것은 상태와 행동을 하나의 단위로 캡슐화하는 객체지향 패러다임에 반한다.
- 이로 인해 접근자와 수정자를 과도하게 추가하게 되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다.
- 접근자와 수정자는 public 속성과 큰 차이가 없기 때문에 객체의 캡슐화는 완전히 무너질 수 밖에 없다.
- 비록 데이터를 처리하는 작업과 데이터를 같은 객체 안에 두더라도 데이터에 초점이 맞춰져 있다면 만족스러운 캡슐화를 얻기 어렵다.
- 데이터를 먼저 결정하고 데이터를 처리하는 데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 된다.
- 결과적으로 객체의 인터페이스는 구현을 캡슐화하는데 실패하고 변경은 취약해진다.
- 결론적으로 데이터 중심 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.
- 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.
데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다 #
- 올바른 객체지향 설계의 무게 중심은 하상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다.
- 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다.
- 중요한 것은 객체가 다른 객체와 협력하는 방법이다.
- 데이터 중심 설계에서 초점은 객체의 외부가 아니라 내부로 향한다.
- 실행 문맥에 대한 깊이있는 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다.
- 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다.
- 객체의 인터페이스에 구현이 노출돼 있었기 때문에 협력이 구현 세부사항에 종속돼 있고 그에 따라 객체의 내부 구현이 변경됐을 때 협력하는 객체 모두가 영향을 받는다.