🟣 객체를 나눴는데, 일을 누가 하나
앞 장에서 도메인 모델로 판매, 판매 항목, 상품 같은 개념을 그렸다. 그런데 여기서 막힌다. "이 판매의 총액을 계산한다"는 동작은 어느 객체가 해야 하나. 판매인가, 판매 항목인가, 아니면 계산을 전담하는 별도의 객체를 만들어야 하나. 개념을 나눴다고 설계가 끝난 것이 아니었다. 오히려 진짜 설계는 각 동작을 누구에게 맡길지 정하는 데서 시작됐다.
이 관점을 책임 주도 설계라고 부른다. 객체를 데이터 덩어리로 보지 않고, 각자 책임을 지고 서로 메시지를 주고받으며 협력하는 작은 공동체로 본다. 책임은 두 종류다. 무언가를 아는 책임(자기 속성을 알고, 연관된 객체를 알고, 자기 정보로 값을 도출한다)과 무언가를 하는 책임(스스로 계산하거나, 다른 객체를 만들거나, 다른 객체의 동작을 지시한다)이다.
문제는 책임을 나누는 방식이 무수히 많다는 것이다. 잘 나누면 변경에 강하고 읽기 쉬운 설계가 되고, 잘못 나누면 한 객체가 비대해지거나 객체들이 서로 얽혀 손대기 어려워진다. GRASP는 이 배분을 주먹구구가 아니라 이름 붙은 원칙으로 판단하게 해 주는 도구다.
🟣 정보 전문가: 아는 자에게 맡긴다
가장 자주 쓰는 원칙이 정보 전문가(Information Expert)다. 어떤 책임을 수행하는 데 필요한 정보를 가장 많이 가진 객체에게 그 책임을 맡긴다. 총액 계산으로 돌아가 보자. 총액을 구하려면 모든 판매 항목의 소계를 더해야 한다. 판매 항목의 목록을 아는 것은 누구인가. 도메인 모델에서 판매가 판매 항목들을 포함하고 있었다. 그러니 총액 계산은 판매의 책임이다.
그럼 각 항목의 소계는 누가 구하나. 소계는 수량 곱하기 단가다. 수량을 아는 것은 판매 항목이고, 단가를 아는 것은 상품이다. 판매 항목이 자기 수량을 알고 연관된 상품에게 가격을 물어보면 소계를 낼 수 있다. 그래서 소계 계산은 판매 항목의 책임이 된다.
책임이 정보를 따라 자연스럽게 세 객체로 흩어졌다. 총액을 계산하는 거대한 관리자 객체를 만들지 않아도 됐다. 각 객체는 자기가 아는 것에 대해서만 계산한다. 정보 전문가 원칙이 가리키는 방향은 대체로 이렇게 데이터와 그 데이터를 다루는 동작을 한 객체 안에 붙여 놓는 쪽이다. 이것이 곧 객체지향에서 말하는 캡슐화의 실질적 이유이기도 하다.
🟣 창조자: 누가 만드나
객체를 계산만 하는 게 아니라 새로 만들어야 할 때도 있다. 판매가 시작되면 판매 항목 인스턴스가 생겨야 한다. 이 생성 책임을 누구에게 줄지 정하는 원칙이 창조자(Creator)다. B가 A를 포함하거나, 기록하거나, 밀접하게 사용하거나, A를 만들 초기 데이터를 쥐고 있다면, A를 만드는 책임은 B에게 준다.
판매 항목의 경우, 판매가 그것들을 포함한다. 그러니 판매 항목을 만드는 것은 판매의 책임이다. 상품 목록을 뒤져 판매 항목을 조립하는 별도의 팩토리를 굳이 세우지 않아도, 이미 그 항목들을 담을 판매가 자연스러운 생성자다.
창조자 원칙이 언제나 판매 같은 도메인 객체를 가리키는 것은 아니다. 생성 규칙이 복잡하거나 만들 대상이 여러 종류로 갈릴 때는, 앞서 디자인 패턴에서 본 팩토리를 별도로 세우는 편이 낫다. GRASP는 기본 배분을 정해 주고, 그 기본이 부담스러워지는 지점에서 GoF 패턴이 등장한다는 관계가 여기서 처음 드러난다. 창조자가 곧 팩토리 패턴의 출발점인 셈이다.
🟣 낮은 결합과 높은 응집: 두 저울
정보 전문가와 창조자가 "누구에게"를 정하는 원칙이라면, 낮은 결합(Low Coupling)과 높은 응집(High Cohesion)은 그 선택이 좋은지 나쁜지를 재는 두 저울이다. 배분 후보가 여럿일 때, 이 둘로 저울질해 더 나은 쪽을 고른다.
결합은 한 객체가 다른 객체를 얼마나 많이 알고 의존하는지다. 결합이 높으면 한 곳을 고칠 때 연결된 여러 곳이 함께 흔들리고, 그 객체를 떼어 재사용하거나 테스트하기 어려워진다. 응집은 한 객체가 맡은 책임들이 서로 얼마나 관련돼 있는지다. 응집이 낮으면 한 객체가 관계없는 일들을 잡다하게 떠안아 이해하기 어렵고 잘 깨진다.
왼쪽처럼 한 객체가 계산과 저장과 인쇄와 통신을 다 쥐면 응집이 무너지고 온갖 하위 객체에 결합된다. 내가 처음 만든 OrderManager가 정확히 이 모습이었다. 오른쪽처럼 책임을 정보 전문가에 따라 흩어 놓으면, 각 객체는 관련된 일만 하고 꼭 필요한 상대만 안다.
두 저울은 종종 서로 당긴다. 결합을 극단적으로 낮추려고 모든 걸 한 객체에 몰면 응집이 깨지고, 응집만 좇아 객체를 잘게 쪼개면 객체 사이 연결이 늘어 결합이 오른다. 그래서 GRASP는 하나의 정답이 아니라 저울질의 언어다. 앞서 공부한 디자인 패턴들이 왜 존재하는지도 이 저울로 설명됐다. 옵서버는 주체와 구독자의 결합을 낮추려는 것이고, 파사드는 복잡한 하위 시스템에 대한 결합을 한 창구로 모으려는 것이다. 패턴은 결국 이 두 저울을 특정 상황에서 맞추는 검증된 방법이었다.
🟣 정리
- 책임 주도 설계는 객체를 데이터가 아니라 책임을 지고 협력하는 공동체로 본다. 책임은 아는 것과 하는 것으로 나뉜다.
- 정보 전문가는 필요한 정보를 가장 많이 가진 객체에 책임을 맡긴다. 데이터와 동작을 한 객체에 붙이는 캡슐화의 실질적 근거다.
- 창조자는 대상을 포함하거나 초기 데이터를 쥔 객체에 생성 책임을 준다. 이 기본이 부담스러워지는 지점에서 팩토리 패턴이 나온다.
- 낮은 결합과 높은 응집은 배분의 좋고 나쁨을 재는 두 저울이다. 서로 당기므로 하나의 정답이 아니라 저울질의 언어로 쓴다.
- 디자인 패턴은 이 두 저울을 특정 상황에서 맞추는 검증된 방법이다. GRASP가 기본을 정하고 패턴이 그 위에 선다.
면접에서 "이 로직을 왜 이 클래스에 뒀느냐"는 질문은 결국 책임 배분을 묻는 것이다. 나는 정보 전문가와 결합·응집으로 그 선택을 설명한다. iOS에서 계산 로직을 뷰모델에 둘지 모델에 둘지 고민할 때도 같은 저울을 쓴다. 그런데 이렇게 정한 책임 배분은 내 머릿속에만 있으면 소용이 없다. 팀과 나누려면 공용 표기가 필요하다. 다음 장에서 그 표기인 UML을 다룬다.