적절한 비용 안에서 쉽게 변경할 수 있는 설계는 응집도가 높고 서로 느슨하게 결합돼 있는 요소로 구성되어 있다.
결합도와 응징도를 합리적인 수준으로 유지할 수 있는 중요한 원칙은 객체의 상태가 아니라 객체의 행동에 초점을 맞추는 것이다.
객체를 단순한 데이터의 집합
으로 바라보는 시각은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시키는 결과를 낳기 때문에 결과적으로 설계가 변경에 취약
해진다.
상태를 분할 중심 : 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의
책임을 분할 중심 : 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관
객체지향 커뮤니티에서는 오랜 기간동안 좋은 설계의 특징을 판단할 수 있는 기준에 관한 다양한 논의가 있어 왔다. 데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도 측면에서 바라보자
설계가 필요한 이유는 요구사항이 변경
되기 때문이고, 캡슐화가 중요한 이유는 불안정한 부분
과 안정적인 부분
을 분리해서 변경의 영향을 통제할 수 있기 때문이다.
변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다.
응집도 : 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.
결합도 : 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.
그렇다면 우리는 질문할 것이다.
모듈 내의 얼마나 많은 요소가 강하게 연관돼 있어야 응집도가 높다고 말할 수 있는가?
모듈 사이에 어느 정도의 의존성만 남겨야 결합도가 낮다고 말할 수 있는가?
정답은 없다.
하지만 점검해 볼 수 있는 몇가지 요소는 있다.
결합도가 높은 원인은 대부분 캡슐화가 내부 구현을 그대로 노출하기 때문이다.객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.
그리고 더 나쁜 소식은 단지 객체의 내부 구현을 변경 했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다는 것이다.
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
pubic void setfee(Money fee) {
this.fee = fee;
}
}
이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.
서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다. 변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐 놓았기 때문에 변경과 아무 상관이 없는 코드들이 영향을 받게 된다.
하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 의지해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.
객체에게 의미 있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다. 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.
아래와 같은 코드는 어떤 문제가 있을까?
public Rectangle {
private int left;
private int top;
private int right;
private int bottom;
public int getLeft() { return left; }
public void setLeft(int left) { this.left = left; }
public int getTop() { return top; }
public void setTop(int top) { this.top = top; }
public int getRight() { return right; }
public void setRight(int right) { this.right = right; }
public int getBottom() { return bottom; }
public void setBottom(int bottom) { this.bottom = bottom; }
}
class AnyClass {
void anyMethod(Rectangle rectangle, int multiple){
rectangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getBottom() * multiple);
}
}
첫 번째는 '코드 중복'이 발생할 확률이 높다는 것이다. 사각형의 너비와 높이를 증가시키는 코드가 필요하다면 아마 getRight, getBottom 메서드를 호출하고 setRight, setBottom 메서드를 수정하는 유사한 로직이 존재할 것이다. 따라서 코드 중복을 초대할 수 있다.
두 번째 문제점은 '변경에 취약'하다는 점이다. Rectangle이 right와 bottom대신 length와 height를 이용해서 사각형을 표현하거나 type을 변경한다고 하면 해당 메서드를 사용하는 클라이언트들은 모두 수정 해주어야 한다.
해결방법은 캡슐화를 강화시키는 것이다. Rectangle 내부에 너비와 높이를 조절하는 로직을 캡슐화 하면 두 가지 문제를 해결 할 수 있다.
class Rectangle {
public void enlarge(int multiple){
right *= multiple;
bottom *= multiple;
}
}
자신의 크기를 스스로 증가시키도록 책임을 이동 시킨 것이다. 이것이 바로 객체가 자기 스스로를 책임 진다는 말의 의미다.
데이터 중심 설계 방식에 익숙한 개발자들은 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식에 따른다. 이것이 첫 번째 설계가 실패한 이유다.
필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 된다. 이것이 두번째 설계가 실패한 이유다.
올바른 객체지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다.
안타깝게도 데이터 중심 설계에서 초점은 객체의 외부가 아니라 내부로 향한다. 실행 문맥에 대한 깊이 있는 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다.
객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출수밖에 없다.