앞 두 장이 코드였다면 이 장은 개념이다. 조영호는 여기서 잠시 코드를 내려놓고 묻는다 — 좋은 객체를 만들려면 무엇부터 생각해야 하는가. 초보의 답은 “어떤 클래스가 필요한가”이지만, 이 책의 답은 다르다. 클래스는 마지막에 온다. 먼저 오는 것은 협력이고, 협력이 책임을 부르고, 책임이 모여 역할이 되고, 역할을 수행할 존재로서 비로소 객체(클래스)가 정해진다. 이 순서를 뒤집지 않는 것이 책임 주도 설계다.
협력이 출발점이다
객체는 혼자 존재하지 않는다. 영화를 예매하려면 Screening이 Movie에게 요금을 묻고, Movie가 DiscountPolicy에게 할인을 묻는다. 하나의 목표(예매)를 이루기 위해 객체들이 요청과 응답을 주고받는 이 상호작용이 협력이다.
협력이 먼저인 이유는, 객체가 무엇을 할지가 그 객체 혼자서 정해지지 않기 때문이다. Movie가 “할인 금액을 계산할 줄 안다”는 능력은 예매라는 협력이 그것을 필요로 하기 때문에 생긴다. 협력이 요청하지 않는 능력은 군더더기다. 그래서 설계는 “이 객체는 무엇을 할 수 있어야 하지?”가 아니라 “이 목표를 이루려면 누가 무엇을 해줘야 하지?”에서 시작한다.
협력을 상호작용의 연쇄로 보면, 하나의 요청이 다음 요청을 낳으며 목표를 향해 이어진다. 예매라는 큰 목표는 “요금을 계산한다”는 하위 요청을 낳고, 그것은 다시 “할인 금액을 구한다”, “할인 대상인지 판단한다”는 더 작은 요청으로 갈라진다. 협력은 이렇게 큰 책임이 작은 책임으로 분해되며 여러 객체에 나뉘는 과정이고, 이 분해의 결과가 곧 각 객체의 인터페이스가 된다. 그래서 협력을 잘 설계한다는 것은 이 요청의 연쇄를 자연스럽고 자율적인 대화로 다듬는 일과 같다.
판단 기준: 어떤 메서드가 필요한지 막막하면, 그 메서드가 속한 협력 시나리오를 먼저 그린다. 요청하는 쪽이 있어야 응답할 책임이 정당화된다. 함정: 협력 없이 “이 클래스엔 이런 기능이 있으면 좋겠다”며 메서드를 채우면, 아무도 호출하지 않는 죽은 능력이 쌓인다.
메시지가 유일한 소통 수단이다
협력이 건강하려면 객체들이 소통하는 방식에 규율이 있어야 한다. 그 규율은 단순하다 — 객체 사이의 유일한 소통 수단은 메시지 전송이다. 한 객체가 다른 객체에게 무언가를 시키려면 메시지를 보내는 수밖에 없고, 그 메시지를 받은 객체가 어떻게 처리할지는 순전히 자신이 결정한다.
이 규율에서 자율적 책임이라는 개념이 나온다. 요청하는 쪽은 “무엇을” 원하는지만 전할 뿐, “어떻게” 하라고 지시하지 않는다. Screening이 Movie에게 “요금을 계산해 달라”고 요청할 때, 계산 절차를 일일이 일러 주지 않는다. Movie가 그것을 어떻게 해낼지는 Movie의 자율에 맡긴다. 책임이 이렇게 자율적인 수준으로 기술되면, 요청하는 쪽은 상대의 내부를 몰라도 되고 상대는 자기 방식을 자유롭게 바꿀 수 있다.
반대로 책임이 너무 구체적이면 — “네 필드에서 요금을 꺼내, 할인율을 곱해, 빼서 돌려줘” 수준이면 — 요청하는 쪽이 상대의 내부를 속속들이 알아야 하고, 상대는 자율성을 잃는다. 이것이 4장에서 데이터 중심 설계가 파탄하는 원리와 정확히 맞닿아 있다.
판단 기준: 책임을 “무엇을 해야 하는가”의 수준으로 기술했으면 자율적이다. “어떻게 하는가”의 절차까지 요청 쪽이 알아야 한다면 너무 구체적인 것이다. 함정: 편의상 getter로 데이터를 꺼내 호출자가 대신 계산하는 것은 자율적 책임이 아니라 자율성을 박탈하는 것이다 — 메시지를 보낸 게 아니라 데이터를 빼앗은 것이다.
책임이 협력을 구체화한다
협력 속에서 한 객체가 맡는 몫이 책임이다. 책임은 두 종류다 — 무언가를 아는 것(knowing)과 무언가를 하는 것(doing). Movie는 자기 기본 요금을 알고, 요금을 계산할 줄 안다. 책임은 “이 객체가 협력을 위해 제공해야 하는 것”의 목록이며, 이것이 나중에 메서드(와 필드)로 구현된다.
여기서 중요한 원칙 하나. 책임은 그 책임에 필요한 정보를 가진 객체에게 할당한다 — 정보 전문가(information expert)에게. 할인 금액 계산에 필요한 데이터를 가진 것은 DiscountPolicy이므로, 계산 책임도 거기 있어야 한다. 1장에서 Theater가 Audience의 가방을 뒤지던 코드가 나빴던 이유가 정확히 이것이다 — 가방의 정보는 Audience가 가졌는데 책임은 Theater가 가로챘다.
정보 전문가에게 책임을 주면 두 가지가 자연히 따라온다. 첫째, 책임을 수행하는 객체가 자기 데이터로 일을 끝내므로 남의 내부를 캐물을 필요가 없어 결합도가 낮아진다. 둘째, 데이터와 그 데이터를 다루는 로직이 한곳에 모여 응집도가 높아진다. 다만 여기서 “정보”란 반드시 그 객체가 필드로 직접 소유한 데이터만을 뜻하지는 않는다. 필요한 정보를 아는 다른 객체에게 물어서 알아낼 수 있다면, 그 조율을 맡는 객체도 정보 전문가가 될 수 있다 — 정보를 소유하는 것과 정보에 접근하는 방법을 아는 것은 다르며, 책임은 후자에게 가도 된다.
판단 기준: “이 일에 필요한 데이터를 누가 가졌는가”를 물으면 책임의 주인이 대개 드러난다. 함정: 책임을 데이터에서 떼어 놓으면(데이터는 A가, 그 데이터를 다루는 로직은 B가) B가 A의 내부를 캐물어야 하고, 결합이 깊어진다. 4장이 바로 이 실수를 일부러 저질러 보이는 장이다.
책임을 카드로 나눠 보기 — knowing과 doing
책임을 두 종류로 가르는 이 관점은 CRC 카드(Class-Responsibility-Collaboration)에서 왔다. 클래스 하나를 카드 한 장으로 삼고, 그 위에 “이 객체가 무엇을 아는가(knowing)“와 “무엇을 하는가(doing)”, 그리고 “누구와 협력하는가”를 적어 내려간다. 카드 크기가 물리적으로 제한돼 있다는 점이 요령이다 — 한 객체에 책임이 넘쳐 카드를 벗어나면, 그 객체가 너무 많은 일을 떠안았다는 신호이므로 책임을 쪼개 다른 객체에게 나눠야 한다.
Movie 카드를 채워 보면, 아는 것(knowing)에는 “기본 요금”, “할인 정책”이 오고, 하는 것(doing)에는 “요금을 계산한다”가 온다. 협력자에는 “DiscountPolicy”가 적힌다. 이렇게 적어 놓으면 책임이 한쪽으로 쏠렸는지, 데이터만 있고 하는 것이 없는 수동적 객체인지가 한눈에 드러난다.
판단 기준: 한 객체의 doing 목록이 유난히 길고 다른 객체들은 knowing만 잔뜩이라면, 책임이 한 객체에 몰린 것이다 — 4장의 ReservationAgency가 그 극단이다. 함정: knowing(데이터를 안다)을 책임으로 착각해 “이 객체는 필드가 많으니 책임이 많다”고 여기면 안 된다 — 진짜 책임은 doing 쪽이며, 아는 것에는 반드시 그것을 다루는 하는 것이 따라와야 한다.
역할은 책임의 묶음이자 교체 가능한 슬롯
여러 책임이 하나의 목적으로 묶이면 역할이 된다. 역할은 “누가 오든 이 책임들만 수행하면 되는” 자리, 즉 슬롯이다. 예매 협력에서 “할인 정책”이라는 역할은 “할인 금액을 계산한다”는 책임을 요구하고, 이 슬롯에는 금액 할인이든 비율 할인이든 그 책임을 수행할 수 있는 누구나 들어갈 수 있다.
역할이 슬롯이라는 관점은 곧 다형성이다. 2장의 DiscountPolicy는 역할이었고, AmountDiscountPolicy/PercentDiscountPolicy는 그 슬롯에 꽂힌 객체였다. 협력을 역할들의 상호작용으로 설계하면, 구체 객체를 갈아 끼워도 협력 구조 자체는 그대로다.
flowchart LR C["협력<br/>(목표를 위한 상호작용)"] -->|필요한 일을 나눈다| R["책임<br/>(아는 것 / 하는 것)"] R -->|관련된 책임을 묶는다| Role["역할<br/>(교체 가능한 슬롯)"] Role -->|슬롯을 채운다| O["객체 / 클래스"] O -.->|이 협력에 참여하며<br/>다시 협력을 만든다| C
화살표의 방향이 이 장의 전부다. 설계는 왼쪽(협력)에서 오른쪽(객체)으로 흐른다. 클래스에서 시작하지 않는다.
다만 이 흐름은 한 방향으로 끝나지 않고 순환한다. 맨 오른쪽에서 정해진 객체는 다시 새로운 협력에 참여하며 다음 순환의 출발점이 되고, 시스템은 이런 협력들이 겹겹이 엮인 그물로 자란다. 그래서 하나의 객체를 이해하려면 그 객체가 참여하는 협력들을 함께 봐야 하고, 협력을 이해하려면 참여하는 객체들의 책임을 함께 봐야 한다. 역할·책임·협력이라는 세 재료는 따로 떼어 생각할 수 없이 맞물려 있으며, 이 맞물림을 다루는 감각이 곧 객체지향 설계 역량이다.
판단 기준: 같은 책임을 여러 구현이 나눠 맡을 수 있으면 그 자리는 역할(추상)이고, 오직 하나뿐이면 그냥 객체다. 미리 역할로 추상화할지, 객체로 두었다가 나중에 뽑아낼지는 “정말 여러 후보가 있는가”로 판단한다. 함정: 후보가 하나뿐인데 미리 역할로 추상화하면, 쓰이지 않을 유연성을 위해 이해 비용만 치른다 — 역할은 필요가 증명된 뒤 추출해도 늦지 않다.
배우와 배역 — 역할을 이해하는 비유
조영호는 역할을 연극에 빗댄다. 연극의 대본에는 “햄릿”이라는 배역이 있고, 그 배역을 실제로 연기하는 배우가 있다. 대본은 특정 배우가 아니라 배역을 기준으로 쓰인다 — 햄릿이 어떤 대사를 하고 어떻게 움직이는지는 정해져 있지만, 그 자리에 어떤 배우가 서든 상관없다. 오늘은 이 배우가, 내일은 저 배우가 같은 햄릿을 연기할 수 있다.
여기서 두 가지 유연함이 나온다. 첫째, 같은 배역에 여러 배우가 설 수 있다 — DiscountPolicy라는 배역에 AmountDiscountPolicy도, PercentDiscountPolicy도 설 수 있는 것과 같다(다형성). 둘째, 한 배우가 여러 배역을 겸할 수 있다 — 한 클래스가 여러 인터페이스를 구현해 여러 역할을 동시에 수행할 수 있는 것과 같다. 협력(대본)을 배우가 아니라 배역 단위로 설계하면, 협력 구조를 건드리지 않고 출연진만 갈아 끼울 수 있다.
왜 DiscountPolicy가 역할인가
2장의 DiscountPolicy를 이 언어로 다시 읽어 보자. Movie가 필요로 한 것은 “AmountDiscountPolicy”라는 특정 배우가 아니라 “할인 금액을 계산해 주는 누군가”라는 배역이었다. Movie는 배역의 이름(DiscountPolicy)과 그 배역이 응답해야 할 대사(calculateDiscountAmount)만 알면 되고, 그 자리에 실제로 누가 서는지는 몰라도 된다.
그래서 DiscountPolicy는 클래스이기 이전에 역할이다. 추상 클래스나 인터페이스라는 문법적 형태는 이 역할을 코드로 옮기는 수단일 뿐이다. 중요한 것은 문법이 아니라, 그 자리가 “교체 가능한 배역”으로 설계되었다는 사실이다. 설계를 역할 단위로 하면 다형성은 자연스러운 결과로 따라온다 — 다형성을 먼저 목표로 삼는 게 아니라, 역할로 협력을 설계했더니 다형성이 되어 있는 것이다.
판단 기준: 어떤 협력 참여자를 “특정 클래스”가 아니라 “이 대사를 할 줄 아는 누군가”로 기술할 수 있으면, 그 자리는 역할로 뽑을 후보다. 함정: 역할을 먼저 인터페이스 문법으로 정의하려 들면 순서가 뒤집힌다 — 역할은 협력에서 발견되는 것이지 문법에서 시작되는 것이 아니다.
역할이 만드는 유연함을 코드로 보기
역할을 슬롯으로 설계했을 때 협력이 얼마나 견고해지는지는 코드로 확인하면 분명하다. Movie가 의존하는 것은 DiscountPolicy라는 역할뿐이고, 그 슬롯에 어떤 배우가 서든 Movie는 손대지 않는다.
// 배역: "할인 금액을 계산한다"는 대사만 정의한다.
public abstract class DiscountPolicy {
public abstract Money calculateDiscountAmount(Screening screening);
}
// 배우들: 같은 배역을 저마다의 방식으로 연기한다.
public class AmountDiscountPolicy extends DiscountPolicy { /* 금액만큼 */ }
public class PercentDiscountPolicy extends DiscountPolicy { /* 비율만큼 */ }
public class NoneDiscountPolicy extends DiscountPolicy { /* 0원 */ }
// 대본(Movie)은 배역만 안다. 어떤 배우가 서는지는 캐스팅(생성) 시점에 정해진다.
Movie avatar = new Movie(new AmountDiscountPolicy(...));
Movie titanic = new Movie(new PercentDiscountPolicy(...));
Movie starWars = new Movie(new NoneDiscountPolicy());세 편의 영화는 모두 같은 Movie 클래스이고 같은 요금 계산 협력을 따른다. 다른 것은 오직 어떤 배우를 캐스팅했는가뿐이다. 새 할인 방식이 필요하면 DiscountPolicy라는 배역에 설 새 배우를 하나 추가하면 되고, 대본(Movie)도 다른 배우들도 건드릴 필요가 없다. 역할로 협력을 설계했기에 얻는 이 확장성이, 2장에서 본 개방-폐쇄 원칙의 개념적 근거다.
판단 기준: 새 요구가 “기존 배역에 새 배우를 세우는 것”으로 흡수되면 역할 설계가 제 몫을 한 것이다. 함정: 배역이 특정 배우의 사정(구체 구현의 세부)을 대사에 담으면 — 예컨대 getDiscountAmountByPercent 같은 특정 방식 전용 메서드를 배역에 두면 — 다른 배우가 그 자리에 서기 어려워져 역할의 교체 가능성이 깨진다.
책임 주도 설계의 과정
지금까지의 개념들을 하나의 절차로 꿰면 책임 주도 설계가 된다. 조영호가 제시하는 순서는 대략 이렇다.
- 시스템이 사용자에게 제공할 기능(책임)을 찾는다. 무엇을 해내야 하는지 큰 덩어리부터 정한다 — “영화를 예매한다”.
- 그 기능을 더 작은 책임으로 나눈다. “요금을 계산한다”, “할인 여부를 판단한다” 같은 하위 책임으로 쪼갠다.
- 각 책임을 수행할 객체(정보 전문가)에게 할당한다. 그 책임에 필요한 정보를 가진 객체를 찾아 맡긴다.
- 혼자 못 하는 책임은 다른 객체에게 요청한다. 이 요청이 곧 메시지이고, 메시지가 협력을 만든다. 요청받은 객체에는 새 책임이 생기고, 다시 3~4를 반복한다.
핵심은 메시지가 객체를 결정한다는 것이다. 어떤 객체가 필요한지 먼저 정하고 그 객체가 받을 메시지를 나중에 붙이는 게 아니라, “이 협력에 이런 메시지가 필요하다”를 먼저 정하고 그 메시지를 받을 적임자로서 객체를 고른다. 이 순서를 지키면 객체의 인터페이스가 협력에 딱 맞게 최소한으로 다듬어진다.
판단 기준: 객체의 퍼블릭 인터페이스는 “협력이 요구한 메시지”의 집합과 같아야 한다. 그보다 많으면 아무도 안 보내는 메시지를 받을 준비를 한 셈이다. 함정: 객체를 먼저 정하고 “이 객체엔 뭐가 있으면 좋을까” 상상하며 메서드를 채우면, 협력과 무관한 인터페이스가 붙어 데이터 중심 설계로 미끄러진다.
영화 예매 협력을 메시지로 그리기
책임 주도 설계의 결과를 메시지의 흐름으로 그리면 이렇게 된다. 각 화살표가 하나의 책임 할당이자 협력의 한 마디다.
sequenceDiagram participant Sc as Screening participant M as Movie participant D as DiscountPolicy (역할) participant Co as DiscountCondition (역할) Sc->>M: 요금을 계산해 줘 (calculateFee) M->>D: 할인 금액을 알려 줘 (calculateDiscountAmount) D->>Co: 이 상영이 할인 대상이야? (isSatisfiedBy) Co-->>D: 응 / 아니 D-->>M: 할인 금액 (또는 0) M-->>Sc: 기본 요금 - 할인 금액
DiscountPolicy와 DiscountCondition을 “역할”로 표기한 데 주목하라. 이 다이어그램은 구체 클래스가 아니라 역할들의 대화를 그린 것이고, 각 역할 자리에는 그 대사를 할 줄 아는 어떤 배우든 설 수 있다.
설계 관점과 구현 관점
역할·책임·협력은 설계 관점의 언어다. 반면 클래스·필드·메서드는 구현 관점의 언어다. 같은 시스템을 두 관점에서 볼 수 있어야 한다. 설계 관점에서 DiscountPolicy는 “할인 금액을 계산하는 역할”이고, 구현 관점에서는 “추상 메서드 getDiscountAmount를 가진 추상 클래스”다. 둘은 같은 것의 다른 층위다.
초보는 구현 관점(클래스)에서 시작해 설계가 그 안에 갇힌다. 능숙한 설계자는 설계 관점(역할·책임·협력)에서 먼저 사고하고, 그것을 구현 관점으로 옮긴다. 이 장 전체가 강조하는 것이 바로 이 순서다 — 구현의 문법에 붙잡히기 전에, 협력과 책임과 역할이라는 설계의 재료로 먼저 생각하라.
판단 기준: 설계를 설명할 때 클래스 이름 없이 “누가 누구에게 무엇을 요청하는가”만으로 말할 수 있으면 설계 관점을 갖춘 것이다. 함정: 두 관점을 뭉개 “역할 = 인터페이스, 그러니 인터페이스부터 만들자”로 직행하면 설계 사고를 건너뛰고 구현으로 미끄러진다.
책임을 객체에게 묻는다 — 의인화
책임 주도 설계를 몸에 붙이는 실전 기법 하나가 의인화다. 객체를 데이터 구조가 아니라 인격을 가진 존재로 대하고 말을 건네 본다 — “너는 상영에 대해 할인 조건을 만족하는지 판단할 수 있니?” 이 질문에 DiscountCondition이 “그래, 내가 요일과 시간을 아니까 내가 답할게”라고 대답하면 책임의 주인이 정해진다. 반대로 “나는 데이터만 있고 판단은 남이 해”라고 대답하는 객체가 있다면, 그건 3장이 경계하는 수동적 데이터 덩어리다.
이 태도가 실용적인 이유는, 협력을 사람들의 대화로 상상하면 어색한 지점이 곧바로 드러나기 때문이다. Theater가 Audience의 지갑을 직접 뒤지는 장면(1장)은 사람 사이라면 무례하고 부자연스럽다 — 그 어색함이 곧 설계 결함의 신호다. 잘 설계된 협력은 정중한 요청과 자율적인 응답으로 이루어진 대화처럼 읽힌다.
판단 기준: 협력을 사람들의 대화로 옮겼을 때 어색하거나 무례한 요청(“네 속을 좀 보자”)이 있으면, 책임이 잘못 놓였다는 신호다. 함정: 의인화를 은유로만 여기고 넘기면 실전에서 쓰지 못한다 — 실제로 객체에게 소리 내어 질문을 던져 보는 것이 요령이다.
자율적 책임의 조건
협력의 품질은 결국 각 책임이 얼마나 자율적인가에 달려 있다. 조영호는 자율적 책임이 갖춰야 할 성질을 몇 가지로 짚는다.
첫째, 책임은 적절히 추상적이어야 한다. 너무 구체적이면(“요일을 꺼내 비교하고, 시간을 꺼내 범위를 확인하라”) 요청 쪽이 절차를 알아야 하고, 너무 막연하면(“알아서 처리하라”) 무엇을 요청한 건지 불분명하다. “이 상영이 할인 대상인지 판단하라”처럼 무엇을 원하는지는 분명하되 어떻게는 맡기는 수준이 적절하다.
둘째, 책임은 하나의 목적을 향해야 한다. 한 메시지가 여러 관심사를 뭉뚱그려 요청하면(예매하면서 로그도 남기고 통계도 갱신하라) 응답하는 객체의 응집도가 무너진다. 셋째, 책임의 수준(추상화 계층)이 요청 쪽과 맞아야 한다. 높은 수준의 협력자에게 낮은 수준의 세부 처리를 요청하면 계층이 뒤섞인다.
이 성질들이 지켜지면 협력은 자율적 존재들의 정중한 대화가 되고, 어긋나면 한쪽이 다른 쪽을 조종하는 명령-복종 관계로 전락한다. 자율적 책임은 개념적 이상이 아니라, 메시지 하나하나를 설계할 때 실제로 조율해야 하는 실무 기준이다.
자율적 책임은 캡슐화와도 직결된다. 책임이 자율적일수록 요청하는 쪽은 상대의 내부를 덜 알아도 되므로, 상대는 자기 구현을 그 뒤에서 자유롭게 바꿀 수 있다. 반대로 책임이 구체적이면 요청 쪽이 상대의 처리 절차에 의존하게 되어, 상대가 방법을 바꾸는 순간 요청 쪽까지 깨진다. 그래서 “책임을 얼마나 자율적으로 기술했는가”는 곧 “그 객체가 변경으로부터 얼마나 보호받는가”와 같은 질문이다 — 3장의 개념이 1·4장의 변경 용이성 논의와 만나는 지점이다.
판단 기준: 하나의 메시지를 놓고 “이 요청은 무엇을 원하는지 분명한가(추상화), 하나의 목적만 담았는가(응집), 받는 쪽 수준에 맞는가(계층)“를 물어 세 조건을 점검한다. 함정: 자율성을 “많은 일을 알아서 하기”로 오해하면 신(God) 객체를 만든다 — 자율성은 넓은 권한이 아니라, 자기 책임 범위 안에서 방법을 스스로 정할 자유다.
협력이 객체의 문맥을 결정한다
같은 객체라도 어떤 협력에 참여하느냐에 따라 필요한 책임이 달라진다. Movie가 “요금 계산” 협력에 참여할 때 필요한 책임과, 가령 “상영 일정 편성” 협력에 참여할 때 필요한 책임은 다르다. 그래서 객체의 인터페이스는 그 객체를 홀로 들여다봐서는 정할 수 없고, 그 객체가 참여하는 협력이라는 문맥 안에서만 정해진다.
이것이 3장이 “협력이 먼저”라고 거듭 말하는 실천적 이유다. 협력이 문맥을 제공하지 않으면, 객체에 어떤 책임을 줄지 결정할 근거가 사라져 “있으면 좋을 것 같은” 기능을 상상으로 채우게 된다. 반대로 협력에서 출발하면 각 메시지가 필요를 증명하며 도착하고, 객체는 그 필요에 응답하는 최소한의 인터페이스만 갖춘다. 협력은 객체를 설계하기 위한 문맥이자, 그 인터페이스가 군더더기 없이 다듬어지도록 제약을 거는 틀이다.
판단 기준: 어떤 책임을 객체에 부여하기 전에 “이 책임을 요구하는 협력이 실제로 있는가”를 물어라. 요구하는 협력이 없으면 그 책임은 아직 필요하지 않다. 함정: 재사용을 노려 “여러 협력에서 두루 쓰일 법한” 범용 인터페이스를 미리 넓게 만들면, 어떤 협력도 요구하지 않은 메서드가 쌓여 응집도를 떨어뜨린다.
왜 이 순서인가
클래스부터 떠올리면 “이 클래스에 어떤 기능을 담을까”라는 데이터 중심 사고로 미끄러지기 쉽다. 반대로 협력에서 시작하면 책임이 자연스럽게 정보 전문가에게 배정되고, 객체는 자율적이 되며, 협력은 역할 단위라 유연해진다. 조영호가 순서를 강조하는 이유가 이것이다 — 같은 재료로도 사고 순서가 설계 품질을 가른다.
한 가지 덧붙일 것은, 이 순서가 한 번에 끝나는 직선이 아니라는 점이다. 협력을 그리다 보면 새 책임이 드러나고, 책임을 할당하다 보면 협력이 다시 다듬어진다. 역할이 필요해 보이던 자리가 후보가 하나뿐이라 그냥 객체로 남기도 하고, 반대로 객체로 두었던 것이 후보가 늘며 역할로 승격되기도 한다. 설계는 이 네 재료 사이를 오가며 조금씩 수렴하는 반복 과정이지, 왼쪽에서 오른쪽으로 한 번 흘려보내면 완성되는 파이프라인이 아니다.
- 협력 → 책임 → 역할 → 객체. 이 방향을 지키면 자율적이고 유연한 설계가, 뒤집으면 데이터 중심의 경직된 설계가 나온다.
- 객체 사이의 유일한 소통 수단은 메시지다. 책임은 “무엇을”의 수준으로 자율적으로 기술하고, “어떻게”는 받는 객체에 맡긴다.
- 책임은 두 종류(아는 것 knowing / 하는 것 doing)로 나뉘며, 진짜 책임은 doing 쪽이다. 아는 것에는 그것을 다루는 하는 것이 따라와야 한다.
- 책임은 그 책임을 수행할 정보를 가진 객체에게 준다(정보 전문가). 데이터와 로직을 떼어 놓지 않는다.
- 역할은 교체 가능한 슬롯이고, 슬롯으로 협력을 설계하면 구체 객체를 갈아 끼워도 구조가 견딘다 — 다형성의 개념적 뿌리다.
- 설계 관점(역할·책임·협력)과 구현 관점(클래스·필드·메서드)은 같은 것의 두 층위다. 설계 관점에서 먼저 사고하고 구현으로 옮긴다.
- 책임이 자율적일수록 요청 쪽이 상대의 내부를 덜 알아, 상대는 구현을 자유롭게 바꿀 수 있다 — 자율적 책임은 곧 변경으로부터의 보호다.
책임 주도 설계가 데이터 중심 설계와 갈리는 지점
이 장의 사고를 뒤집으면 곧바로 데이터 중심 설계가 된다. 데이터 중심 설계는 협력을 묻지 않고 “이 객체가 어떤 데이터를 가질까”부터 정한다. 그러면 객체는 협력의 문맥 없이 홀로 정의되어, 데이터를 내놓는 창구(getter) 말고는 할 줄 아는 게 없어진다. 책임은 그 데이터를 그러모으는 제3의 객체로 몰리고, 자율성도 역할도 사라진다.
책임 주도 설계는 정확히 그 반대 순서로 이 함정을 피한다. 협력을 먼저 그리므로 각 객체가 맡을 책임(행동)이 정해지고, 데이터는 그 책임을 수행하는 데 필요한 만큼만 딸려 온다. 데이터가 아니라 책임이 객체의 중심이 되니 자율성이 서고, 책임의 묶음을 역할로 추상화하니 유연성이 따라온다. 4장은 이 대비를 실증하기 위해 일부러 데이터 중심으로 영화 예매를 다시 짜서 파탄을 보여 준다 — 이 장이 세운 순서를 어겼을 때 무엇이 무너지는지를, 코드로 확인하는 장이다.
판단 기준: 설계를 시작할 때 첫 질문이 “어떤 데이터?”인지 “어떤 협력·책임?”인지를 스스로 점검하라. 첫 질문이 순서를 결정하고, 순서가 설계 품질을 결정한다. 함정: 데이터 중심 설계도 클래스와 private 필드를 쓰므로 겉보기엔 객체지향이다 — 구별은 문법이 아니라 “협력에서 출발했는가”라는 사고의 순서에 있다.
다음장으로 4장