2장에서 객체지향답게 짠 영화 예매 시스템을, 이 장에서는 일부러 데이터 중심으로 다시 짠다. 목적은 나쁜 코드를 감상하는 게 아니라, 왜 나쁜지를 이름 붙여 진단하는 것이다. 조영호는 응집도와 결합도라는 잣대로 같은 문제의 두 설계를 저울에 올린다. 여기서 만들어지는 파탄한 코드가 5장에서 책임 주도 설계로 되살아나는 출발점이 된다 — 그러니 이 장의 마지막 상태를 잘 기억해 둘 값어치가 있다.
데이터부터 정하면 벌어지는 일
데이터 중심 설계는 “이 객체가 어떤 데이터를 가질까”에서 시작한다. Movie는 요금·할인 종류·할인 금액·할인 비율을 필드로 갖고, 그걸 꺼내 쓰라고 getter를 연다. DiscountCondition은 기간이냐 순번이냐를 나타내는 타입과 요일·시간·순번을 담는다. 로직은? 데이터를 꺼내다 쓸 누군가 — ReservationAgency — 에게 몰아준다.
public enum MovieType { AMOUNT_DISCOUNT, PERCENT_DISCOUNT, NONE_DISCOUNT }public class Movie { private Money fee; private MovieType movieType; private Money discountAmount; private double discountPercent; private List<DiscountCondition> discountConditions; public Money getFee() { return fee; } public MovieType getMovieType() { return movieType; } public Money getDiscountAmount() { return discountAmount; } public double getDiscountPercent() { return discountPercent; } public List<DiscountCondition> getDiscountConditions() { return discountConditions; }}public class DiscountCondition { private DiscountConditionType type; private int sequence; private DayOfWeek dayOfWeek; private LocalTime startTime; private LocalTime endTime; // ... 모든 필드에 getter. 아는 것도, 하는 것도 없이 데이터만 담는다.}// 데이터는 있는데 행동이 없다. 이 데이터를 다룰 책임은 어디로 갔나?
진단 — 세 가지 냄새
동작은 한다. 그러나 세 가지가 무너졌다.
캡슐화 붕괴. Movie도 DiscountCondition도 자기 데이터를 getter로 죄다 열어 놨다. 데이터를 숨기고 행동만 노출하는 것이 캡슐화인데, 이 클래스들은 행동이 없고 데이터만 있다. 필드 하나를 startTime에서 beginTime으로 바꾸면 그걸 꺼내 쓰던 ReservationAgency가 깨진다 — 내부 표현의 변경이 바깥으로 새어 나간다.
높은 결합도. ReservationAgency.reserve 하나가 Movie, Screening, DiscountCondition의 내부 구조에 전부 묶여 있다. 세 클래스 중 어느 하나의 데이터 표현이 바뀌어도 Agency를 고쳐야 하고, 반대로 할인 정책이 하나 늘면 switch를 찾아 열어야 한다. 변경의 파급이 사방으로 번진다.
낮은 응집도. reserve 한 메서드 안에 “할인 조건 판별”과 “요금 계산”이라는 서로 다른 관심사가 뒤엉켜 있다. 조건 판별 방식이 바뀌어도, 할인 계산 방식이 바뀌어도, 똑같은 이 메서드를 열어야 한다. 하나의 모듈이 여러 이유로 바뀐다면 응집도가 낮은 것이다.
판단 기준: “이 클래스가 데이터만 있고 행동이 없다”면 데이터 중심 설계의 징후다. 로직은 어딘가 다른 곳에서 그 데이터를 캐물으며 돌고 있다. 함정: getter/setter를 기계적으로 다는 관행은 캡슐화를 흉내만 낸다 — 필드를 private으로 두고 getX()로 다 열면, 사실상 public 필드와 다를 게 없다.
캡슐화를 흉내 내도 소용없는 이유
getter를 없애고 필드를 진짜 숨기면 해결될까. 아니다. 문제의 뿌리는 접근 제어자가 아니라 책임이 데이터에서 떨어져 있다는 것이다. 할인 여부를 판별할 정보(할인 조건)는 DiscountCondition이 가졌는데, 그 판별 책임은 ReservationAgency가 가로챘다. 3장의 언어로 말하면, 정보 전문가가 아닌 자에게 책임이 갔다. 그러니 아무리 필드를 숨겨도 Agency가 그 데이터를 필요로 하는 한, 결국 getter를 다시 열게 된다.
되살리는 길은 방향을 뒤집는 것이다 — 데이터를 가진 객체에게 그 데이터를 다루는 책임까지 돌려주기. DiscountCondition이 “나는 이 상영에 해당하는가”를 스스로 답하고, Movie가 “내 요금은 얼마인가”를 스스로 계산하게 하면, Agency는 조율만 하면 된다. 그것이 5장의 책임 주도 설계다.
- 데이터 중심 설계는 “어떤 데이터를 가질까”에서 시작해, 데이터는 수동적 덩어리가 되고 로직은 한 곳(
ReservationAgency)으로 몰린다. - 증상은 세 가지 — 캡슐화 붕괴(getter 남발), 높은 결합도(한 메서드가 여러 객체 내부에 묶임), 낮은 응집도(한 메서드에 여러 관심사).
- 근본 원인은 책임을 데이터에서 떼어 놓은 것. 접근 제어자를 조인다고 풀리지 않는다. 책임을 정보 전문가에게 돌려주는 5장이 해법이다.
다음장으로 5장