이번 장에서는 절차적 프로그래밍 방식으로 영화 예매 시스템을 설계해보고, 어떤 문제점이 있는지 설명한다.
객체의 상태는 구현에 속하며, 구현은 불안정하기 때문에 변하기 쉽다.
객체의 상태를 중심으로 설계하면 구현에 관한 세부사항이 인터페이스에 노출된다. 이때 객체의 상태가 변경되면 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼지게 되므로, 변경에 취약하다.
반면에 객체의 책임은 인터페이스에 속한다. 안정적인 인터페이스 뒤로 책임을 수행하는데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 영향이 외부로 퍼져나가는 것을 방지할 수 있다.
이러한 이유로 훌륭한 객체지향 설계를 위해서는 객체의 상태가 아니라 책임에 초점을 맞춰야 한다.
응집도와 결합도는 소프트웨어의 품질을 측정하기 위한 기준이다.
응집도란 모듈에 포함된 내부 요소들이 연관되어 있는 정도를 나타내는 척도를 말한다.
모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가지는 것이다.
반대로 서로 다른 목적을 추구한다면 낮은 응집도를 가진다고 할 수 있다.
객체지향의 관점에서 응집도는 객체에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.
변경의 관점에서 응집도는 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.

음영이 칠해진 곳은 변경이 발생했을 때 수정되는 영역을 가리킨다.
높은 응집도를 가진 설계에서는 하나의 원인에 의해 하나의 모듈만 수정하면 된다. 하지만 낮은 응집도를 가진 설계에서는 하나의 원인에 의해 여러 모듈을 수정해야 한다.
응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.
결합도란 의존성의 정도, 즉 다른 모듈에 대해 얼마나 많은 지식을 가지고 있는지를 나타내는 척도를 말한다.
어떤 모듈이 다른 모듈에 대해 너무 자세하게 알고 있다면 두 모듈은 높은 결합도를 가지는 것이다.
반대로 어떤 모듈이 다른 모듈에 대해 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다고 할 수 있다.
객체지향의 관점에서 결합도는 객체가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.
변경의 관점에서 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.

낮은 결합도를 가진 설계에서는 모듈 A를 변경했을 때 변경의 영향이 외부로 퍼져나가지 않는다. 하지만 높은 결합도를 가진 설계에서는 모듈 A를 변경했을 때 모듈 A를 강하게 의존하는 4개의 모듈도 함께 변경해야 한다.
아래 Movie 클래스는 인스턴스 변수 fee와 getter, setter만 가지고 있다.
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee() {
this.fee = fee;
}
}
getFee, setFee 메서드는 Movie 내부에 Money 타입의 fee라는 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러낸다.
객체 내부 구현이 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
...
Money fee;
if (discountable) {
...
fee = movie.getFee().minus(discountAmount);
} else {
fee = movie.getFee();
}
...
}
}
ReservationAgency는 Movie의 getFee 메서드를 호출하여 계산한 결과를 Money 타입의 fee에 저장한다.
만약 fee의 타입이 변경된다면 getFee 메서드의 반환 타입도 변경될 것이다. 여기서 ReservationAgency의 구현도 변경된 타입에 맞춰 함께 수정해야 한다는 문제가 발생한다.
사실 getter, setter를 사용하는 것은 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.
위에서 본 ReservationAgency의 reserve 메서드는 movie의 getFee 메서드를 호출하여 금액을 계산한다. 만약 이와 같은 코드가 다른 곳에서도 필요하다면, 아마 유사한 코드로 구현할 가능성이 높다.
"코드 중복은 악의 근원이다." - Martin Fowler
Movie 클래스의 인스턴스 필드 fee의 타입이 변경된다면 기존의 getFee, setFee 메서드를 사용하던 모든 코드를 함께 변경해야 한다.
기존에 getter, setter만 있던 DiscountCondition 클래스를 아래와 같이 내부 구현을 캡슐화하여 개선했다.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
...
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
if (type != DiscountConditionType.PERIOD) {
throw new IllegalArgumentException();
}
return this.dayOfWeek.equals(dayOfWeek) &&
this.startTime.compareTo(time) <= 0 &&
this.endTime.compareTo(time) >= 0;
}
...
이제 DiscountCondition은 자신의 데이터를 이용해 할인 가능 여부를 스스로 판단한다. 하지만 아직 개선할 부분이 남아 있다.
isDiscountable 메서드는 DiscountCondition의 속성인 DayOfWeek 타입의 요일 정보와 LocalTime 타입의 시간 정보를 파라미터로 받고 있다.
이는 객체 내부에 DayOfWeek 타입의 요일과 LocalTime 타입의 시간 정보가 인스턴스 변수로 포함돼 있다는 사실을 인터페이스를 통해 외부에 노출하고 있는 것이다.
따라서 내부 구현의 변경이 외부로 퍼져나가는 파급 효과가 아직 존재한다고 볼 수 있다.
사실 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 내부 속성을 외부로부터 감추는 것은 ‘데이터 캡슐화’라고 불리는 캡슐화의 한 종류일 뿐이다.
감추는 것이 속성의 타입이건, 할인 정책의 종류건 상관 없이 내부 구현의 변경으로 인해 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다. 설계에서 변하는 것이 무엇인지 고려해 변하는 개념을 캡슐화해야 한다. 이것이 캡슐화의 진정한 의미다.
데이터 주도 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 한다. 이는 다음과 같은 문제를 발생시킨다.
객체지향 설계에서 가장 중요한 것은 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이다.
하지만 데이터 중심 설계는 협력에 대한 깊이 있는 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다.
결과적으로 객체의 인터페이스에 구현이 노출되어 있었기 때문에 협력이 구현 세부사항에 종속된다. 그에 따라 객체의 내부 구현이 변경됐을 때 협력하는 객체 모두가 영향을 받을 수밖에 없다.