12장이 컴포넌트를 배포와 개발의 단위로 세웠다면, 13장은 그 앞에 실무자가 매일 마주하는 질문 하나를 세운다 — 어떤 클래스를 어느 컴포넌트에 넣을 것인가. 얼핏 사소해 보이는 이 결정이 사소하지 않은 이유는, 컴포넌트가 곧 함께 릴리스되고 함께 배포되고 함께 변경되는 운명 공동체이기 때문이다. 한 컴포넌트 안에 묶인 클래스들은 한 배를 탄다. 그러니 누구를 같은 배에 태우느냐가 시스템의 유지보수 비용을 결정한다.

마틴은 이 배치를 지도하는 원칙 세 개를 내놓는다. 그런데 이 장의 진짜 교훈은 세 원칙의 내용 자체가 아니라, 세 원칙이 서로를 배반한다는 사실에 있다. 각각은 흠잡을 데 없이 옳지만, 셋을 동시에 완벽히 지키는 배치는 존재하지 않는다. 하나를 세게 당기면 다른 하나가 끌려온다. 좋은 아키텍트는 이 세 힘이 팽팽히 맞선 삼각형 어딘가에서 지금 프로젝트에 맞는 균형점을 고르는 사람이다.

REP — 재사용의 단위는 릴리스의 단위다

첫 번째 원칙은 재사용·릴리스 등가 원칙(Reuse/Release Equivalence Principle)이다. 문장은 짧다 — 재사용의 단위는 릴리스의 단위와 같다.

이 원칙을 이해하려면 남의 코드를 재사용해 본 경험을 떠올리면 된다. 우리는 어떤 라이브러리를 가져다 쓸 때, 그 소스 파일들을 아무렇게나 복사해 오지 않는다. 버전 번호가 붙은 릴리스를 가져온다. 버전이 있어야 우리가 무엇에 기대고 있는지 알 수 있고, 저자가 새 버전을 내놓았을 때 지금 갈아탈지 기존 버전에 머무를지를 판단할 수 있다. 릴리스 번호와 변경 이력, 그리고 “이 버전은 여기까지 호환된다”는 약속이 없다면, 우리는 그 코드를 안심하고 재사용할 수 없다.

여기서 REP의 아키텍처적 함의가 나온다. 재사용 가능한 요소로 묶으려면, 그것들을 함께 하나의 릴리스 단위로 추적하고 버전을 매기고 릴리스 문서를 붙여야 한다. 뒤집어 말하면, 하나의 컴포넌트를 이루는 클래스와 모듈은 함께 릴리스될 수 있을 만큼 서로 응집돼 있어야 한다. 아무 관계 없는 클래스들을 한 컴포넌트에 몰아넣고 하나의 버전으로 릴리스하는 것은, 사용자에게 “이것들은 한 묶음으로 의미가 있다”는 거짓 약속을 하는 셈이다. 함께 릴리스된다는 것은 함께 하나의 이야기를 이룬다는 뜻이어야 한다.

판단 기준: 어떤 클래스들을 한 컴포넌트로 묶을지 망설여질 때, “이것들을 하나의 버전으로 릴리스 노트에 적어 사용자에게 내놓았을 때 말이 되는가”를 물어라. 하나의 응집된 목적으로 설명되면 REP를 만족한다. 함정: REP는 “무엇을 함께 묶어야 하는가”만 말할 뿐 “무엇을 떼어 놓아야 하는가”는 말하지 않는다. 이 원칙 하나만 따르면 자꾸 크게 뭉치는 쪽으로 기운다 — 나머지 두 원칙이 그 팽창을 제어하기 위해 존재한다.

CCP — 함께 바뀌는 것을 한데 모아라

두 번째 원칙은 공통 폐쇄 원칙(Common Closure Principle)이다 — 같은 이유로, 같은 시점에 바뀌는 클래스들을 같은 컴포넌트에 모아라. 다른 이유로 다른 시점에 바뀌는 클래스는 다른 컴포넌트로 나눠라.

이 문장을 어디서 본 것 같다면 정확한 직감이다. 이것은 7장의 단일 책임 원칙(SRP)을 컴포넌트 크기로 확대한 것이다. SRP가 “하나의 클래스는 하나의 액터에 대해서만 변경 이유를 가져야 한다”였다면, CCP는 “하나의 컴포넌트는 하나의 이유로만 바뀌어야 한다”고 말한다. 변경의 이유가 응집의 축이 되는 것이다.

왜 이것이 중요한가. 유지보수의 진짜 비용은 코드를 짜는 데서가 아니라 바꾸는 데서 나온다. 그리고 하나의 변경 요청이 여러 컴포넌트에 흩어져 걸치면, 그 컴포넌트들을 전부 다시 릴리스하고 다시 검증하고 다시 배포해야 한다. 반대로 함께 바뀌는 코드가 한 컴포넌트에 모여 있으면, 변경의 파급이 그 컴포넌트 하나에 갇힌다. 손대야 할 곳이 하나로 줄고, 재배포할 것도 하나로 줄고, 다른 개발자가 무관한 변경에 휩쓸릴 일도 없다. CCP의 목표는 이것이다 — 하나의 변경이 건드리는 컴포넌트의 수를 최소로 줄이는 것.

여기서 2장에서 만난 통찰이 컴포넌트 크기로 되살아난다. 변경 비용은 요청의 형태가 아니라 범위에 비례해야 한다고 했다. CCP는 바로 그 이상을 컴포넌트 배치로 실현하는 방법이다. 같은 이유로 바뀔 것들을 한데 모아 두면, 그 이유가 발생했을 때 변경의 범위가 물리적으로 한 컴포넌트에 정렬된다.

판단 기준: 컴포넌트 경계를 그을 때 “이 둘은 앞으로 같은 이유로 함께 바뀔 것인가, 아니면 서로 다른 이유로 따로 바뀔 것인가”를 물어라. 함께 바뀔 것이면 모으고, 따로 바뀔 것이면 가른다. 경계는 변경의 축을 따라 놓인다. 함정: “지금 서로 호출한다”는 사실을 “함께 바뀐다”로 착각하지 말 것. 자주 협력하는 두 클래스가 정작 변경의 이유는 완전히 다를 수 있다 — 응집의 기준은 현재의 호출 관계가 아니라 미래의 변경 이유다.

CRP — 함께 쓰지 않는 것을 억지로 묶지 마라

세 번째 원칙은 공통 재사용 원칙(Common Reuse Principle)이다 — 한 컴포넌트의 사용자들에게, 그들이 실제로는 쓰지 않는 것까지 의존하도록 강요하지 마라. 함께 재사용되는 경향이 있는 클래스와 모듈만 같은 컴포넌트에 두라는 것이다.

이 원칙 역시 낯익은 뿌리를 가진다. 10장의 인터페이스 분리 원칙(ISP)을 컴포넌트 크기로 늘린 것이다. ISP가 “쓰지 않는 메서드에 의존하지 마라”였다면, CRP는 “쓰지 않는 클래스가 든 컴포넌트에 의존하지 마라”고 말한다.

문제의 핵심은 의존의 대가에 있다. 내가 어떤 컴포넌트에 의존하면, 나는 그 컴포넌트 전체에 묶인다. 내가 실제로 쓰는 것은 그 안의 클래스 하나뿐이어도, 그 컴포넌트가 새 버전을 내면 나는 그 릴리스를 검토하고 내 코드를 다시 검증하고 다시 배포해야 한다. 내가 쓰지도 않는 클래스가 바뀌어 릴리스가 나왔을 뿐인데도 말이다. 쓰지 않는 것에 대한 의존은 이렇게 불필요한 재검증과 재배포의 사슬을 끌고 온다.

그래서 CRP는 “무엇을 함께 두라”보다 “무엇을 함께 두지 마라”를 말하는 원칙이다. 함께 쓰이지 않는 클래스들을 한 컴포넌트에 욱여넣으면, 그중 하나만 필요한 사용자에게 나머지 전부에 대한 의존이라는 세금을 물린다. CRP는 그 세금을 없애기 위해, 함께 쓰이지 않을 것들을 갈라놓으라고 요구한다.

판단 기준: 어떤 클래스들을 한 컴포넌트에 둘지 정할 때, “이것들은 대체로 함께 쓰이는가, 아니면 누군가는 이 중 일부만 쓰고 나머지는 짐으로 지고 가는가”를 물어라. 일부만 쓰일 것 같으면 그 조각은 떼어 낸다. 함정: CRP는 CCP와 정확히 반대 방향으로 당긴다. CCP가 “혹시 함께 바뀔지 모르니 모아 두자”며 컴포넌트를 키우면, CRP는 “함께 쓰지 않으니 갈라라”며 그것을 쪼갠다 — 이 둘을 동시에 최대로 만족시키는 배치는 없다.

응집도의 삼각형 — 셋은 서로를 배반한다

이제 이 장의 심장에 도달한다. 세 원칙을 나란히 세워 보면 이들이 사이좋게 협력하지 않는다는 것이 드러난다. 마틴은 이것을 세 꼭짓점을 가진 긴장의 삼각형으로 그린다.

REP와 CCP는 포함적이다. 둘 다 컴포넌트를 크게 만드는 쪽으로 당긴다. REP는 의미 있는 릴리스 단위를 위해 관련된 것을 모으라 하고, CCP는 함께 바뀔 것을 한 컴포넌트에 모아 변경 파급을 줄이라 한다. 두 힘 모두 “모아라”라고 말한다.

CRP는 배제적이다. 이것만 홀로 컴포넌트를 작게 만드는 쪽으로 당긴다. 쓰지 않는 것에 의존하지 않게 하려면 함께 안 쓰이는 것을 떼어 내야 하고, 그러면 컴포넌트는 잘게 쪼개진다.

세 원칙이 삼각형의 세 변을 이룬다고 상상해 보자. 각 변은 “그 원칙을 어겼을 때 치르는 대가”를 적어 놓은 경고문이다. REP와 CCP만 붙들고 CRP를 버리면, 컴포넌트가 지나치게 커져서 사용자는 자신과 무관한 변경 때문에 끊임없이 재검증과 재배포에 시달린다. CCP와 CRP만 붙들고 REP를 버리면, 재사용이 너무 잘게 쪼개져서, 재사용하려는 사람이 어느 버전을 어떻게 조합해야 할지 추적하기가 지옥이 된다. REP와 CRP만 붙들고 CCP를 버리면, 컴포넌트가 재사용과 사용 패턴 기준으로만 정렬돼서, 하나의 변경이 여러 컴포넌트로 흩어지고 그 전부를 다시 릴리스해야 한다.

여기서 결정적인 통찰이 나온다 — 세 원칙을 동시에 다 만족시킬 수는 없다. 어느 두 원칙을 세게 쥐면 나머지 하나는 반드시 느슨해진다. 이것은 실력이 부족해서 생기는 타협이 아니라, 세 힘의 방향이 애초에 어긋나 있기 때문에 생기는 구조적 긴장이다. 그러니 아키텍트가 할 수 있는 최선은 “셋 다 완벽히”가 아니라, 삼각형 안 어느 지점에 설 것인가를 고르는 일이다.

판단 기준: 컴포넌트 구조를 평가할 때 “이 배치가 셋 다 지키는가”를 묻지 말고 “지금 우리는 어느 변을 일부러 느슨하게 두었고, 그 대가를 감당할 만한가”를 물어라. 어떤 원칙을 희생했는지 스스로 답할 수 있으면 의식적으로 균형점을 고른 것이다. 함정: 세 원칙을 전부 만점으로 지키려 애쓰는 것 자체가 함정이다 — 그런 배치는 존재하지 않으므로, 만점을 노리면 어느 것도 제대로 못 지키는 어정쩡한 구조에 이른다.

무게중심은 프로젝트를 따라 움직인다

삼각형이 정적인 그림이었다면 이 장은 절반만 말한 셈이 된다. 마틴의 마지막 통찰은 그 균형점이 시간이 지나며 이동한다는 것이다.

프로젝트의 초기를 떠올려 보자. 아직 아무것도 확정되지 않았고, 무엇이 재사용될지도 불분명하다. 이 시기에 세 원칙 중 CRP와 REP — 재사용성과 관련된 두 힘 — 를 완벽히 지키려 들면, 개발은 오히려 발목을 잡힌다. 재사용을 위한 깔끔한 컴포넌트 경계를 그리느라, 정작 지금 필요한 것은 “빠르게 만들고 쉽게 바꾸는” 개발 편의인데 그것을 희생하게 된다. 그래서 젊은 프로젝트에서는 삼각형의 무게중심이 CCP 쪽에 놓인다. 재사용성보다 개발 편의성과 변경 용이성이 훨씬 중요하기 때문이다. 이때는 함께 바뀌는 것을 모아 변경을 편하게 하는 쪽이 이긴다.

시간이 흐르고 프로젝트가 성숙하면 사정이 달라진다. 다른 프로젝트들이 이 프로젝트의 컴포넌트를 가져다 쓰기 시작한다. 이제는 재사용하는 쪽의 고통 — 쓰지 않는 것에 딸려 오는 의존, 추적하기 힘든 버전 조합 — 이 실제 비용으로 나타난다. 그래서 성숙한 프로젝트에서는 무게중심이 REP와 CRP 쪽으로 미끄러져 간다. 개발 편의보다 재사용성을 지키는 쪽의 값이 더 커지기 때문이다.

이 이동이 뜻하는 바는 분명하다 — 컴포넌트 구조는 한 번 그리고 끝나는 그림이 아니다. 처음부터 완벽한 재사용 컴포넌트를 설계하려는 시도는 대개 잘못된 방향이다. 그 시점에 필요한 것은 재사용성이 아니라 개발 속도이기 때문이다. 컴포넌트 구성은 프로젝트가 자라고 쓰임이 바뀜에 따라 계속 재조정되어야 하는, 살아 있고 유동하는 결정이다. 15장 이후 아키텍처의 목적을 “선택지를 열어 두는 것”이라 부르게 되는데, 컴포넌트 경계 역시 나중에 다시 그을 수 있도록 열어 둔 선택지의 하나다.

판단 기준: 컴포넌트를 나눌 때 “이 프로젝트가 지금 삼각형의 어디쯤 있는가”를 먼저 물어라. 초기라면 CCP 쪽(변경 편의)에, 재사용이 실제로 일어나는 성숙기라면 REP·CRP 쪽(재사용성)에 무게를 싣는다. 함정: 젊은 프로젝트에 성숙한 프로젝트의 재사용 기준을 들이대는 것이 가장 흔한 과잉 설계다 — 아직 아무도 재사용하지 않는 코드를 위해 재사용 컴포넌트를 정교하게 깎느라, 정작 지금 필요한 변경 속도를 갖다 바친다.

왜 세 원칙 모두 “변경”으로 수렴하는가

마지막으로 한 걸음 물러서서 보면, 세 원칙이 사실 하나의 관심사를 세 각도에서 비추고 있음이 보인다. REP는 재사용하는 사람이 변경(새 릴리스)을 안전하게 받아들이게 하고, CCP는 변경이 최소한의 컴포넌트만 건드리게 하며, CRP는 무관한 변경이 사용자에게 새어 나가지 않게 한다. 세 원칙 모두 결국 변경이 왔을 때 그 파급을 어떻게 통제할 것인가를 묻고 있다.

이것이 이 책 전체와 이어지는 지점이다. 2장에서 소프트웨어의 진짜 가치를 변경 용이성이라 불렀고, SOLID의 다섯 원칙이 클래스 수준에서 변경을 다스렸다면, 컴포넌트 원칙은 같은 싸움을 배포 단위라는 더 큰 크기에서 이어 간다. 응집도의 삼각형은 그 싸움에서 우리가 공짜 점심을 먹을 수 없다는 사실 — 재사용성, 변경 편의, 의존의 순결함 중 어느 둘을 얻으려면 나머지 하나를 어느 정도 내주어야 한다는 사실 — 을 정직하게 인정하라고 요구한다.

  • 컴포넌트에 무엇을 담을지는 세 원칙이 지도한다. REP(재사용 단위 = 릴리스 단위), CCP(같은 이유로 바뀌는 것을 모아라 = 컴포넌트 SRP), CRP(함께 쓰지 않는 것을 묶지 마라 = 컴포넌트 ISP).
  • REP와 CCP는 컴포넌트를 키우는 쪽으로, CRP는 쪼개는 쪽으로 당긴다. 세 힘이 맞선 삼각형에서 셋을 동시에 만족시키는 배치는 없다.
  • 각 변을 버렸을 때의 대가가 다르다 — 너무 크면 무관한 변경에 시달리고, 너무 잘게 쪼개면 재사용 버전 추적이 지옥이 되며, 재사용만 좇으면 변경이 여러 컴포넌트로 흩어진다.
  • 균형점은 고정돼 있지 않다. 젊은 프로젝트는 CCP(변경 편의)로 기울고, 성숙한 프로젝트는 REP·CRP(재사용성)로 미끄러진다. 컴포넌트 구조는 계속 재조정되는 살아 있는 결정이다.

응집도가 “무엇을 같은 컴포넌트에 담을 것인가”의 문제였다면, 다음 장은 그렇게 나눈 컴포넌트들이 서로를 어떻게 향해야 하는가 — 의존의 방향과 안정성 — 라는, 한 단계 위의 질문으로 넘어간다.

다음장으로 14장