이 책은 이론에서 시작하지 않는다. 소극장 하나를 세우고, 초대장과 티켓과 현금이 오가는 아주 작은 거래를 코드로 옮기는 데서 출발한다. 조영호가 첫 장에서 던지는 질문은 하나다 — 똑같이 동작하는 두 코드가 있을 때, 무엇이 더 나은 설계인가. 답은 기능이 아니라 변경에 있다. 요구가 바뀔 때 흔들리는 코드와 견디는 코드를 가르는 것은 객체 사이의 의존성이다.
모듈이 존재하는 세 가지 이유
이 장은 로버트 마틴의 문장 하나를 나침반으로 삼는다. 소프트웨어 모듈에는 세 가지 목적이 있다 — 실행 중에 제대로 동작하는 것, 변경을 위해 존재하는 것, 코드를 읽는 사람과 소통하는 것. 첫 번째는 최소한의 자격이다. 동작하지 않는 모듈은 존재할 이유가 없다. 문제는 나머지 둘이다.
두 번째, 모듈은 변경하기 쉬워야 한다. 대부분의 모듈은 한 번 작성되고 끝나지 않고, 요구가 바뀔 때마다 수정된다. 간단한 변경을 하려는데 예상치 못한 곳들이 줄줄이 함께 깨진다면, 그 모듈은 변경이라는 목적을 배신하고 있다. 세 번째, 모듈은 읽는 사람과 소통해야 한다. 코드는 컴퓨터만이 아니라 다음에 이 코드를 열 개발자(대개 미래의 자신)를 독자로 둔다. 동작하지만 이해할 수 없는 코드는 결국 변경할 수 없는 코드가 된다.
이 장의 두 코드는 모두 첫 번째 목적(동작)은 만족한다. 갈리는 것은 두 번째와 세 번째 — 변경 용이성과 의사소통이다. 그리고 이 둘을 좌우하는 것이 바로 의존성이다.
판단 기준: “동작하는가”에서 멈추지 말고 “바꾸기 쉬운가, 읽어서 이해되는가”까지 물어야 설계를 평가한 것이다. 함정: 세 목적 중 실행만 보고 코드를 합격시키면, 변경과 소통의 부채는 눈에 보이지 않은 채 쌓였다가 나중에 한꺼번에 청구된다.
무대 뒤를 들여다보는 소극장
이벤트 당첨자는 초대장을, 나머지는 현금을 들고 온다. 입장하려면 티켓이 필요하다. 처음 떠오르는 대로 짜면 Theater가 모든 걸 통제한다. 관람객(Audience)의 가방을 열어 초대장이 있는지 확인하고, 없으면 가방에서 현금을 꺼내 매표소(TicketOffice)에 넣고 티켓을 꺼내 관람객에게 건넨다.
동작은 한다. 문제는 이 Theater가 관람객의 가방 내부와 매표소의 금고 내부를 속속들이 알고 있다는 것이다. 관람객이 현금이 아니라 신용카드로 결제하려 하면? Theater를 고쳐야 한다. 가방에 초대장 대신 모바일 QR을 넣으면? 또 Theater를 고쳐야 한다. 나와 상관없어야 할 변경이 나를 흔든다.
이 연쇄를 조금 더 구체적으로 세어 보면 심각성이 드러난다. Theater는 Audience가 Bag을 가진다는 사실, Bag 안에 Invitation과 현금과 Ticket이 들어 있다는 사실, TicketSeller가 TicketOffice를 통해 표를 판다는 사실을 전부 알고 있다. 이 여섯 개 클래스의 내부 구조 중 어느 하나라도 바뀌면 Theater가 함께 바뀔 후보가 된다. 하나의 객체가 이렇게 넓은 앎을 가질수록, 그 객체는 시스템 곳곳의 변경에 붙잡힌 인질이 된다.
판단 기준: 어떤 클래스를 고쳐야 하는지가 “누구의 변경이냐”와 어긋난다면 의존성이 잘못 놓인 것이다. 함정: 처음 떠오르는 절차적 흐름은 대개 한 객체가 나머지의 내부를 훤히 아는 형태로 수렴한다 — 동작하니까 방치되기 쉽다.
예상을 빗나가는 코드는 이해를 해친다
이 Theater의 진짜 죄는 결합만이 아니다. 코드가 상식을 배신한다. 현실에서 관람객이 극장에 들어갈 때, 판매원이 관람객의 가방을 직접 열어 초대장을 확인하고 손을 넣어 현금을 꺼내 가는 장면을 상상해 보라. 무례하고 부자연스럽다. 관람객은 자기 가방을 스스로 열고, 판매원은 요청만 한다 — 그것이 우리가 아는 세상의 질서다.
Theater.enter는 이 질서를 뒤집는다. 판매원(정확히는 극장)이 관람객의 가방과 매표소의 금고를 제 것처럼 뒤진다. 코드를 읽는 사람은 자신이 아는 세상의 상식으로 코드를 예상하는데, 코드가 그 예상을 계속 빗나가면 매 줄마다 “왜 이게 여기서 남의 속을 뒤지지?” 하고 멈춰 서야 한다. 이해에 드는 비용이 치솟는다. 앞서 로버트 마틴이 말한 세 번째 목적 — 소통 — 이 무너지는 지점이 바로 여기다.
반대로 각 객체가 현실에서 맡은 역할대로 자기 일을 처리하면, 코드는 우리가 아는 이야기의 흐름을 그대로 따라가므로 예상을 빗나가지 않는다. 이해하기 쉬운 코드란 세상에 대한 우리의 예상을 배신하지 않는 코드다.
판단 기준: 코드를 현실의 상황으로 옮겼을 때 “저 사람이 왜 남의 가방을 뒤지지?” 같은 어색함이 생기면, 그 지점의 책임 배치가 예상을 빗나가고 있다는 신호다. 함정: 익숙해지면 어색함조차 안 보인다 — “원래 이렇게 짜는 거”라며 넘어가는 순간, 소통 비용은 새 독자에게 고스란히 전가된다.
자율적인 존재로 되돌리기
해법의 방향은 분명하다. 가방을 뒤지는 일은 가방을 가진 관람객이, 금고를 여는 일은 금고를 가진 판매원이 하도록 되돌린다. Theater는 “표를 팔아라”라고 요청만 하고, 내부 처리는 각자에게 맡긴다. 아래 스텝이 이 장의 전부다 — 절차적 원본에서 시작해 책임을 한 단계씩 제자리로 돌려보낸다.
public class Theater { private TicketSeller ticketSeller; public void enter(Audience audience) { if (audience.getBag().hasInvitation()) { Ticket ticket = ticketSeller.getTicketOffice().getTicket(); audience.getBag().setTicket(ticket); } else { Ticket ticket = ticketSeller.getTicketOffice().getTicket(); audience.getBag().minusAmount(ticket.getFee()); ticketSeller.getTicketOffice().plusAmount(ticket.getFee()); audience.getBag().setTicket(ticket); } }}// Theater가 남의 내부를 직접 연다 — getBag(), getTicketOffice()가 결합의 증거다.
무엇이 달라졌나. 마지막 상태에서 TicketSeller는 더 이상 audience.getBag()을 호출하지 않는다. 가방의 존재조차 모른다. 관람객이 카드로 결제하도록 바꾸고 싶으면 Bag(혹은 Audience)만 고치면 되고, Theater와 TicketSeller는 그대로다. 캡슐화가 의존성을 끊었고, 끊긴 의존성이 변경의 파급을 막았다.
판단 기준: 한 객체가 다른 객체의 getX().getY()로 내부의 내부까지 파고든다면(디미터 법칙 위반), 그 사슬만큼 결합이 깊다는 뜻이다. 함정: 자율화가 지나치면 객체 수가 늘고 흐름을 눈으로 좇기 어려워진다 — 절차적 코드가 읽기 쉬워 보이는 것은 착각이 아니라 사실이며, 그 대가로 변경에 취약할 뿐이다.
변경 용이성, 의존성, 결합도
여기서 세 낱말의 뜻을 분명히 못박아 둘 필요가 있다. 이 셋은 이 책 전체를 관통하는 어휘다.
의존성. 어떤 객체가 다른 객체를(혹은 그 내부를) 알고 있어서, 그 다른 객체가 바뀌면 자신도 영향을 받을 가능성이 있는 관계다. Theater가 Audience.getBag()을 호출하는 순간 Theater는 Bag에 의존한다 — Bag이 바뀌면 Theater도 흔들릴 수 있다. 의존성 자체는 나쁜 게 아니다. 객체는 협력해야 하고, 협력하려면 서로를 어느 정도 알아야 한다. 문제는 그 앎이 얼마나 깊은가다.
결합도. 의존성이 얼마나 강한지를 가리키는 척도다. 한 객체가 다른 객체의 존재만 아는 것과, 그 내부 구현(가방 속에 초대장이 필드로 있다는 사실)까지 아는 것은 결합의 깊이가 다르다. 후자가 결합도가 높다. 결합도가 높을수록 상대의 사소한 변경에도 함께 깨질 확률이 커진다.
변경 용이성. 결국 우리가 얻으려는 것이다. 결합도가 낮으면 한 객체를 바꿔도 파급이 그 객체 안에 갇히므로, 변경이 쉽다. 리팩터링 전후를 비교하면 명확하다 — 관람객을 카드 결제로 바꿀 때, 결합도 높은 원본은 Theater까지 고쳐야 했고 결합도 낮은 최종본은 Bag만 고치면 된다.
판단 기준: “이 객체를 바꾸면 어디까지 함께 바뀌어야 하는가”를 세어 보면 결합도가 드러난다. 함께 바뀌는 범위가 좁을수록 좋은 설계다. 함정: 의존성을 0으로 만들려는 시도는 헛되다 — 협력하는 객체는 반드시 의존한다. 목표는 의존성을 없애는 것이 아니라 필요한 최소한으로, 추상적인 것에 대해서만 두는 것이다.
트레이드오프 — 자율에는 대가가 따른다
Bag을 자율화하면서 우리는 무엇을 얻고 무엇을 내주었나. Bag이 hold로 스스로 초대장을 판단하게 만들자, TicketSeller와 Audience는 가방 내부를 몰라도 되게 됐다 — 그쪽 결합은 끊겼다. 그러나 동시에 Bag은 이제 Ticket과 Invitation을 알아야 하고, Ticket의 요금을 꺼내 자기 잔액에서 빼는 책임까지 떠안았다. 한쪽에서 끊은 결합이 다른 쪽에서 새로 생긴 것이다.
이것이 설계의 본질이다 — 설계는 항상 트레이드오프다. 어떤 결합을 끊으면 대개 다른 결합이 생기거나, 객체 수가 늘거나, 흐름이 여러 객체로 흩어져 한눈에 안 들어오게 된다. 절차적 원본이 실제로 더 짧고 한 곳에서 흐름을 읽기 쉬웠다는 사실을 부정할 필요는 없다. 우리가 그것을 버린 이유는 “더 예쁘다”가 아니라, 변경 용이성이라는 가치를 위해 가독성의 일부와 코드량을 지불하기로 결정했기 때문이다.
그래서 좋은 설계자는 “무엇이 옳은가”가 아니라 “이 상황에서 무엇을 지불하고 무엇을 살 것인가”를 묻는다. 정답이 하나 있는 게 아니라, 맥락에 따라 저울의 균형점이 달라진다.
판단 기준: 어떤 리팩터링을 결정할 때, 얻는 것(끊기는 결합)과 내주는 것(늘어나는 객체·새 결합·흐름 분산)을 둘 다 명시적으로 세어 본 뒤 순이득을 판단한다. 함정: “객체지향이니까 무조건 자율화”는 트레이드오프를 보지 않는 태도다 — 변경될 일이 없는 곳까지 잘게 쪼개면 대가만 치르고 얻는 이득은 쓰이지 않는다.
의인화 — 객체를 살아 있는 존재로 대하기
책임을 제자리로 돌려놓는 사고를 돕는 실천적 기법이 의인화다. 객체를, 데이터를 담은 수동적 구조가 아니라 스스로 판단하고 행동하는 인격체처럼 대하는 것이다. 현실의 Bag은 스스로 무언가를 하지 못하는 무생물이지만, 코드 속 Bag에게는 “네 안에 초대장이 있으면 티켓을 받아 두고, 없으면 요금만큼 현금을 내놓아라”라고 말을 걸 수 있다.
이렇게 객체를 능동적 존재로 상상하면, 앞서 본 “예상을 빗나가는 코드”의 어색함이 곧바로 진단 도구가 된다. 잘 설계된 객체들은 각자 자기 몫을 아는 자율적 존재들이 정중하게 요청을 주고받는 대화처럼 읽힌다. 반대로 한 객체가 다른 객체의 속을 뒤지는 코드는, 인격체들의 대화로 옮기는 순간 무례하고 부자연스러운 장면으로 드러난다.
판단 기준: 협력을 “객체들이 나누는 대화”로 소리 내어 읽었을 때 자연스러우면 책임 배치가 대체로 옳다. 함정: 의인화를 현실의 물리 법칙에 가두지 말 것 — 현실에서 가방이 스스로 판단 못 한다고 코드 속 Bag도 수동적이어야 하는 건 아니다. 소프트웨어 객체는 현실보다 더 능동적이어도 된다.
설계가 좋다는 것은 무엇인가
두 코드는 같은 결과를 낸다. 그런데 하나는 요구가 바뀔 때 한 곳만 고치면 되고, 다른 하나는 무관해야 할 곳까지 번진다. 조영호의 결론은 담백하다 — 훌륭한 설계란 오늘의 기능을 만족시키면서 내일의 변경을 수용할 수 있는 코드다. 그리고 그 열쇠는 객체를 자율적인 존재로 대하는 것, 남의 내부를 들여다보지 않고 메시지로만 협력하게 하는 것이다.
여기서 한 가지 오해를 경계해야 한다. 이 장이 리팩터링으로 “더 나은” 코드에 도달했다고 해서, 객체지향이 언제나 절차적 코드보다 우월하다는 뜻은 아니다. 최종 코드는 더 유연해진 대신 객체가 늘고 흐름이 흩어졌다. 우리가 그 대가를 치르기로 한 것은 이 예제에서 결제 수단과 입장 방식의 변경이 충분히 예상되었기 때문이다. 변경이 예상되지 않는 코드였다면 원본의 단순함이 오히려 미덕일 수 있다. 좋은 설계란 절대적 정답이 아니라, 예상되는 변경에 맞춰 균형점을 고르는 판단이다.
절차적 프로그래밍 대 객체지향 프로그래밍
두 코드에 이름을 붙이면 대비가 또렷해진다. 원본은 절차적 프로그래밍의 전형이다. 프로세스(Theater.enter)와 데이터(Audience, Bag, TicketOffice)를 분리하고, 하나의 프로세스가 여러 데이터를 끌어와 처리한다. 데이터는 그저 처리 대상일 뿐 스스로 아무것도 하지 않는다. 이 구조에서 프로세스는 자신이 다루는 모든 데이터의 내부를 알아야 하므로, 데이터의 세부가 바뀌면 프로세스가 흔들린다 — 변경이 프로세스로 집중되어 취약하다.
리팩터링한 코드는 객체지향 프로그래밍이다. 데이터와 그 데이터를 다루는 프로세스를 같은 객체 안에 둔다. Bag은 자기 데이터(초대장·현금)를 갖고, 그것을 다루는 로직(hold)도 자신이 갖는다. 데이터와 로직이 한 몸이 되니 캡슐화가 자연히 따라오고, 데이터의 세부는 그 객체 안에 갇혀 바깥으로 새지 않는다. 변경이 데이터를 소유한 객체 안에 국한되므로 파급이 좁다.
조영호가 강조하는 것은 “무엇이 옳으냐”가 아니라 두 패러다임이 변경을 다루는 방식이 다르다는 점이다. 절차적 설계는 데이터 변경에 취약하고, 객체지향 설계는 데이터를 소유한 객체 안으로 변경을 가둔다. 그래서 객체지향은 “데이터를 누가 소유하고, 그 데이터를 다루는 책임을 그 소유자에게 주는가”라는 질문으로 요약된다.
판단 기준: 데이터와 그 데이터를 다루는 로직이 같은 객체에 있으면 객체지향, 서로 다른 곳에 흩어져 있으면 절차적이다. 후자라면 그 로직을 데이터의 소유자에게 옮길 수 있는지 물어라. 함정: 클래스를 여럿 만들었다고 객체지향인 것은 아니다 — 데이터만 든 클래스와 그것을 조종하는 클래스로 나뉘어 있으면, 문법은 객체지향이되 사고는 절차적이다.
- 절차적 설계는 한 객체(
Theater)에 데이터와 로직이 몰리고, 나머지는 수동적인 데이터 덩어리가 된다. 변경이 중앙으로 집중돼 취약하다. - 객체지향 설계는 데이터를 가진 객체에게 그 데이터를 다루는 책임까지 준다. 캡슐화가 자연히 따라오고, 의존성이 줄어든다.
- 좋고 나쁨의 기준은 “동작하느냐”가 아니라 “변경에 어떻게 반응하느냐”다.
- 설계는 트레이드오프다. 어떤 결합을 끊으면 다른 대가(객체 증가·흐름 분산)가 따르므로, 얻는 것과 내주는 것을 함께 저울질한다.
설계가 왜 필요한가
마지막 질문. 어차피 동작하는데, 왜 굳이 설계에 공을 들이나. 조영호의 답은 시간에 있다. 소프트웨어는 한 번 만들고 끝나는 게 아니라 끊임없이 변경되며 살아간다. 오늘의 요구사항은 내일 바뀌고, 예상하지 못한 요청이 계속 도착한다. 그리고 미래를 완벽하게 예측할 방법은 없다.
그래서 설계의 목표는 미래를 다 맞히는 것이 아니다. 오늘 필요한 기능을 지금 구현하면서, 내일 있을지 모를 변경을 최소한의 비용으로 수용할 수 있도록 코드를 배치하는 것이다. 변경이 왔을 때 그 파급을 좁은 범위에 가둘 수 있다면 설계가 제 몫을 한 것이고, 사소한 변경이 코드 전체를 흔든다면 아무리 동작해도 설계는 실패한 것이다.
이것이 이 책 전체를 여는 전제다. 설계는 미래의 변경에 드는 비용을 오늘 미리 낮추는 투자이며, 그 투자의 성패는 객체 사이 의존성을 어떻게 놓았는가로 판가름 난다. 2장부터는 이 원리를 영화 예매라는 더 큰 무대에서 구체적인 기법으로 풀어 나간다.
덧붙여 조영호는 이 투자에도 한계가 있음을 분명히 한다. 어떤 설계도 모든 변경을 값싸게 만들 수는 없다. 설계는 “예상되는 변경”의 방향을 골라 그 축으로만 유연성을 배치하는 선택이며, 예상하지 못한 축의 변경은 여전히 비쌀 수 있다. 그러니 설계의 실력은 “얼마나 많이 대비했는가”가 아니라 “어느 변경이 실제로 올지를 얼마나 정확히 골랐는가”에서 나온다. 이 절제된 태도가 뒤따르는 모든 장의 밑바탕에 깔려 있다.
판단 기준: 설계 결정을 내릴 때 “지금 이 코드에 어떤 변경이 닥칠 가능성이 높은가”를 먼저 그린다. 변경이 예상되는 축을 따라 유연성을 배치하는 것이 설계다. 함정: 반대로, 오지 않을 변경까지 미리 대비해 모든 것을 추상화하면 과잉 설계다 — 설계는 예측 가능한 변경에 거는 것이지 모든 미래에 거는 보험이 아니다.
다음장으로 2장