13장이 컴포넌트 하나의 안쪽을 다뤘다면 — 무엇을 한 릴리스 단위로 묶을 것인가 — 14장은 컴포넌트 사이로 시선을 옮긴다. 묶고 나면 반드시 이어야 한다. 어떤 컴포넌트가 어떤 컴포넌트를 알고, 어떤 컴포넌트가 어떤 컴포넌트를 모르는가. 이 앎의 방향, 즉 의존성 화살표를 어디로 그을 것인가가 이 장의 주제다. 마틴은 여기서 세 개의 원칙을 세운다. 하나는 순환을 금하고(ADP), 하나는 화살표가 향할 방향을 정하고(SDP), 하나는 그 방향과 추상화의 정도를 맞춘다(SAP). 셋을 관통하는 질문은 결국 하나다 — 변경이 어느 쪽으로 흐르게 할 것인가.
순환은 밤사이 시스템을 얼어붙게 한다
여러 팀이 한 시스템을 나눠 개발하는 장면을 떠올려 보라. 각 팀은 자기 컴포넌트를 만들고, 완성되면 릴리스 번호를 붙여 다른 팀이 쓰도록 공개한다. 다른 팀은 자기가 의존하는 컴포넌트의 특정 버전에 기대어 개발하고, 그 컴포넌트가 새 버전을 내도 당장 따라가지 않을 자유를 갖는다. 준비가 되면 그때 새 버전에 맞춰 자기 코드를 조정한다. 이렇게 하면 어느 한 컴포넌트의 변경이 다른 팀을 즉시 강제로 멈춰 세우지 않는다. 각 팀은 자기 리듬으로 움직인다.
이 평화로운 그림을 단번에 깨뜨리는 것이 **순환(cycle)**이다. A가 B를 쓰고 B가 C를 쓰는데, 어느 날 C가 A의 무언가를 필요로 해서 C → A 화살표가 생기면, 세 컴포넌트는 하나의 고리로 묶인다. 이제 A를 릴리스하려면 C가 안정적이어야 하고, C가 안정적이려면 B가, B가 안정적이려면 다시 A가 안정적이어야 한다. 셋은 사실상 하나의 거대한 컴포넌트가 된다. 독립적으로 개발하던 세 팀이 갑자기 서로의 완성을 기다리며 얼어붙는다. 아침에 출근하니 어젯밤 누군가 그은 화살표 하나 때문에 내 컴포넌트가 컴파일조차 되지 않는 상황 — 순환은 이렇게 조용히, 그리고 치명적으로 온다.
**ADP(비순환 의존성 원칙, Acyclic Dependencies Principle)**는 그래서 단호하다. 컴포넌트 의존성 그래프에 순환이 있어서는 안 된다. 그래프는 방향 비순환 그래프(DAG)여야 한다. 순환이 없다면 어떤 컴포넌트든 자신이 의존하는 것들만 안정되면 릴리스할 수 있고, 그래프를 위상 정렬해 빌드 순서를 정할 수 있으며, 변경의 영향 범위를 화살표를 거슬러 올라가며 정확히 셀 수 있다.
판단 기준: 어떤 컴포넌트를 바꿨을 때 영향을 받는 범위는 그 컴포넌트로 들어오는 화살표를 거꾸로 따라가면 나온다 — 순환이 없어야 이 추적이 유한하게 끝난다. 함정: 순환은 대개 큰 결단으로 생기지 않고 “여기서 저 함수 하나만 쓰면 되는데” 하는 사소한 편의로 생긴다. 화살표 하나가 그래프 전체를 한 덩어리로 응고시킨다는 사실은 응고된 뒤에야 보인다.
생긴 순환은 두 가지 방법으로 끊는다
순환은 완벽한 설계로 예방하는 것이 아니라, 시스템이 자라면서 생겨나는 것이다. 그러니 중요한 것은 순환을 발견했을 때 끊는 법이다. 마틴은 두 가지 처방을 준다.
첫째, DIP(의존성 역전 원칙)로 화살표를 뒤집는다. C → A의 순환이 문제라면, C가 실제로 필요로 하는 기능을 인터페이스로 뽑아 C 쪽에 두고, A가 그 인터페이스를 구현하게 한다. 그러면 소스 코드 의존성은 A → (C가 소유한 인터페이스)가 되어, 방향이 뒤집힌다. 제어 흐름은 여전히 C에서 A의 구현으로 흐르지만, 컴파일 시점의 화살표는 반대가 되어 고리가 풀린다.
둘째, 순환에 얽힌 컴포넌트들이 공통으로 의존할 새 컴포넌트를 만든다. A와 C가 서로를 필요로 한다면, 둘이 함께 쓰는 것만 뽑아 D라는 새 컴포넌트로 옮기고 A → D, C → D로 만든다. 순환이 두 개의 나란한 화살표로 바뀐다. 다만 이 방법은 컴포넌트 개수를 늘리고, 그렇게 태어난 D는 애초에 계획된 것이 아니라 순환을 피하려고 급조된 것이라 — 그래프는 계속 부풀고 흔들린다.
// 순환: Entities → Authorizer → Permissions → Entities (User가 다시 Entities로)
// Permissions가 User를 직접 알아서 고리가 닫힌다.
package permissions;
import entities.User; // ← 이 화살표가 순환을 만든다
public class Permissions {
public boolean check(User user) { ... }
}// 끊기: Permissions가 필요한 것만 인터페이스로 선언하고 소유한다.
package permissions;
public interface UserGateway { // Permissions가 소유한 추상
boolean hasRole(String role);
}
public class Permissions {
public boolean check(UserGateway user) { ... }
}
// entities의 User가 그 인터페이스를 구현한다 — 화살표가 Entities → Permissions로 뒤집힌다.
package entities;
import permissions.UserGateway;
public class User implements UserGateway {
public boolean hasRole(String role) { ... }
}여기서 마틴이 강조하는 두 번째 교훈이 나온다 — 컴포넌트 구조는 하향식으로 설계할 수 없다. 시스템이 존재하기도 전에 완벽한 의존성 그래프를 그릴 수는 없다. 그래프는 시스템이 자라고, 변경되고, 순환이 생겼다 끊기는 과정을 거치며 함께 진화한다. 컴포넌트 다이어그램은 시스템의 초기 청사진이 아니라 시스템이 변경에 어떻게 대응하는지를 담은 지도이며, 그래서 처음부터 그릴 수 없고 자라나는 것이다.
판단 기준: 순환을 발견하면 먼저 DIP로 뒤집을 수 있는지 본다 — 새 컴포넌트를 만드는 것보다 추상 인터페이스 하나를 옮기는 편이 그래프를 덜 부풀린다. 함정: 컴포넌트 구조를 프로젝트 초기에 확정해 문서로 박제하려는 시도는 반드시 배신당한다. 아직 존재하지 않는 변경의 방향을 예측해 그린 그래프는, 실제 변경이 닥치는 순간 순환과 함께 무너진다.
안정성의 방향으로 의존하라
순환을 없앴다고 끝이 아니다. 비순환 그래프에도 좋은 방향과 나쁜 방향이 있다. **SDP(안정된 의존성 원칙, Stable Dependencies Principle)**는 그 방향을 정한다 — 의존성은 항상 더 안정된 쪽을 향해야 한다.
여기서 “안정성”은 품질이나 변경 빈도가 아니라 변경시키기가 얼마나 어려운가를 뜻한다. 마틴은 이것을 물리적 비유로 설명한다. 세워 둔 동전처럼 건드리면 넘어가는 것은 불안정하고, 바닥에 넓게 깔린 무거운 물건처럼 밀어도 꿈쩍 않는 것은 안정적이다. 무엇이 컴포넌트를 안정적으로 만드는가. 자신에게 의존하는 컴포넌트가 많을수록 안정적이다. 나를 쓰는 컴포넌트가 셋이면, 나를 바꿀 때마다 그 셋을 다 조정해야 하므로 나는 함부로 바뀔 수 없다. 나를 바꾸려는 힘에 저항하는 무게가 셋만큼 붙은 셈이다.
마틴은 이 안정성을 숫자로 잰다. 어떤 컴포넌트로 들어오는 의존성 수를 팬인(fan-in), 나가는 의존성 수를 팬아웃(fan-out)이라 하면, 불안정성 지표는
I가 0이면 나가는 의존성이 하나도 없고 들어오는 것만 있는, 최대로 안정된 컴포넌트다. 아무에게도 기대지 않으니 바깥 변경에 흔들릴 일이 없고, 많은 것이 나에게 기대니 함부로 못 바뀐다. 반대로 I가 1이면 나는 남에게 기대기만 하고 아무도 나에게 기대지 않는, 최대로 불안정한 컴포넌트다. 바꾸기 쉽고, 바깥 변경에 늘 흔들린다. SDP는 이 I 값이 의존성 화살표 방향을 따라 계속 낮아져야 한다고 말한다. 어떤 컴포넌트의 I는 자신이 의존하는 컴포넌트들의 I보다 커야 한다. 즉 불안정한 것이 안정된 것에 기대야 하며, 그 반대는 죄다.
왜 이 방향이어야 하는가. 안정된 컴포넌트는 바꾸기 어렵다. 그런데 자주 바뀌어야 할 정책이 그 안에 들어가 있으면, 바뀌기 어려운 자리에 바뀌어야 할 것이 갇힌 꼴이 된다. 반대로 자주 바뀔 것을 불안정한(바꾸기 쉬운) 컴포넌트에 두고, 그것이 안정된 것에 의존하게 하면, 변경은 그래프의 바깥쪽 잎에서 일어나 안쪽으로 번지지 않는다.
판단 기준: 자주 바뀔 것으로 예상되는 컴포넌트는 팬인이 낮은(불안정한) 자리에 두어 변경이 바깥에 머물게 한다 — 무엇을 어디에 놓을지는 “이게 얼마나 자주 바뀔까”와 “얼마나 많은 것이 이걸 볼까”를 함께 재서 정한다. 함정: 안정성을 “좋은 것”, 불안정성을 “나쁜 것”으로 읽지 마라. 모든 컴포넌트가 안정적이면 시스템은 아무것도 바꿀 수 없게 굳는다. 불안정성은 변경이 드나드는 문이며, 시스템에는 반드시 그런 문이 있어야 한다.
안정적인 것은 추상적이어야 한다
SDP를 따르면 한 가지 곤란이 남는다. 안정된 컴포넌트는 많은 것이 의존하니 바꾸기 어렵다. 그런데 그 안에 든 것이 구체적인 코드라면, 바꾸기 어려운 자리에 확장 불가능한 코드가 갇힌 것이다. 유연해야 할 시스템의 심장이 콘크리트로 굳는다. 이 모순을 푸는 것이 **SAP(안정된 추상화 원칙, Stable Abstractions Principle)**다 — 컴포넌트는 안정된 정도만큼 추상적이어야 한다.
논리는 8장의 OCP로 이어진다. 안정적이면서도 확장 가능하려면, 그 안정된 컴포넌트는 구체 클래스가 아니라 추상 클래스와 인터페이스로 채워져야 한다. 추상은 그 자체를 바꾸지 않고도 새 구현을 더해 확장할 수 있다. 그래서 고수준 정책 — 시스템에서 가장 바뀌면 안 되면서 동시에 가장 유연해야 하는 것 — 은 안정되고 추상적인 컴포넌트에 담긴다. DIP가 “추상에 의존하라”고 한 것을 컴포넌트 규모로 확대하면 바로 SAP가 된다. 안정된 곳은 추상으로, 불안정한 곳은 구체로.
마틴은 추상화 정도도 숫자로 잰다. 컴포넌트 안의 클래스 총수 대비 추상 클래스·인터페이스의 수를
로 정의한다. A가 0이면 추상이 하나도 없는 순수 구체 컴포넌트, 1이면 온통 추상뿐인 컴포넌트다. SAP는 안정된(I가 낮은) 컴포넌트일수록 A가 높아야 한다고 말한다. 이제 두 지표 I와 A를 두 축으로 하는 평면 하나를 그릴 수 있고, 여기서 이 장의 그림이 완성된다.
주계열 — 그리고 벗어난 곳의 두 지옥
I를 가로축, A를 세로축으로 놓으면 컴포넌트는 이 정사각형 평면 위의 한 점이 된다. 좋은 컴포넌트가 있어야 할 자리는 어디인가. 두 가지 이상적인 점을 먼저 보자. (0, 1) — 최대로 안정되고 최대로 추상적인 점. 인터페이스만 담긴, 아무에게도 의존하지 않고 모두가 의존하는 컴포넌트다. (1, 0) — 최대로 불안정하고 최대로 구체적인 점. 구현만 담긴, 남에게 기대기만 하고 아무도 안 보는 잎 컴포넌트다. 이 두 이상점을 잇는 대각선이 마틴이 **주계열(main sequence)**이라 부르는 선이다. 컴포넌트는 이 선 위에, 혹은 가까이에 있어야 한다. 안정된 만큼 추상적이고, 불안정한 만큼 구체적인 상태.
주계열에서 멀리 떨어진 두 구석이 문제다.
(0, 0) 근처 — 고통의 구역(Zone of Pain). 최대로 안정적인데 추상은 하나도 없는, 구체적이면서 많은 것이 의존하는 컴포넌트다. 굳어 있어서 바꿀 수 없고, 추상이 아니라서 확장할 수도 없다. 바꿔야 할 이유는 생기는데 바꾸면 그에 의존하는 수많은 것이 함께 깨진다. 마틴은 데이터베이스 스키마를 전형적인 예로 든다 — 온 시스템이 의존하지만 지독히 구체적이고, 그래서 스키마 변경은 늘 고통스럽다. 다만 자주 바뀌지 않는 안정된 구체는 이 구역에 있어도 덜 아프다. 예컨대 String 같은 유틸리티는 (0,0)에 가깝지만 변할 일이 없어 문제가 되지 않는다. 고통은 “구체적이고 안정적인데 변경 압력이 있는” 컴포넌트에서 터진다.
(1, 1) 근처 — 쓸모없는 구역(Zone of Uselessness). 최대로 추상적인데 아무도 의존하지 않는 컴포넌트다. 추상만 잔뜩 정의해 놓았는데 그것을 구현하거나 사용하는 것이 없다. 팔리지 않는 설계, 아무도 꽂지 않은 플러그의 소켓이다. 대개 방치된 채 남은 잔해 — 한때 쓰였으나 지금은 버려진 인터페이스들이 여기 쌓인다.
좋은 컴포넌트는 이 두 구역에서 최대한 멀리, 즉 두 구역을 잇는 다른 대각선인 주계열 위에 있어야 한다. 주계열 위에서는 안정된 정도와 추상의 정도가 균형을 이룬다. 마틴은 한 점이 주계열에서 벗어난 거리를 지표 D = |A + I − 1|로 재어, 컴포넌트들의 이 값을 통계적으로 관리하라고까지 제안한다. 다만 이것은 강박적으로 0에 맞추라는 뜻이 아니라, 주계열에서 유난히 멀리 떨어진 이상치를 찾아내 왜 그런지 물어보는 도구다.
판단 기준: 어떤 컴포넌트가 고통스럽거나 쓸모없다고 느껴지면 그 I와 A를 대략 가늠해 평면 위에 찍어 본다 — 두 구역 중 어디에 가까운지가 처방을 알려준다(고통의 구역이면 추상을 뽑아 올리고, 쓸모없는 구역이면 죽은 추상을 걷어낸다). 함정: I·A·D를 정밀한 목표치로 삼아 모든 컴포넌트를 대각선에 억지로 올려붙이려 하면, 지표를 맞추려고 무의미한 추상을 만드는 본말전도에 빠진다. 이 숫자들은 정확한 값이 아니라 방향을 가리키는 나침반이며, 눈에 띄게 벗어난 것을 잡아내는 데 쓰는 것이지 소수점을 다듬는 데 쓰는 것이 아니다.
세 원칙은 결국 하나의 방향을 가리킨다
세 원칙을 한 문장씩으로 다시 세워 보면 그것들이 실은 한 이야기의 세 측면임이 드러난다. ADP는 화살표가 되돌아오지 못하게(순환 금지) 하고, SDP는 그 화살표가 안정된 쪽을 향하게 하고, SAP는 그 안정된 쪽이 추상적이게 만든다. 셋을 겹치면, 변경은 그래프의 불안정하고 구체적인 바깥 잎에서 일어나 안쪽으로 번지지 않으며, 시스템의 안정된 심장은 추상으로 되어 있어 바꾸지 않고도 확장된다. 이것은 22장의 동심원 — 소스 의존성이 안쪽 고수준 정책을 향하고, 그 안쪽이 추상으로 채워지는 구조 — 를 컴포넌트 규모에서 미리 예고하는 그림이다.
- ADP: 컴포넌트 의존성 그래프에 순환이 있어선 안 된다(DAG). 순환은 DIP로 화살표를 뒤집거나 공통 컴포넌트를 새로 뽑아 끊는다. 그래프는 하향식으로 설계되지 않고 시스템과 함께 자란다.
- SDP: 의존성은 더 안정된(
I가 낮은) 쪽을 향해야 한다. 안정성은 품질이 아니라 “바꾸기 어려운 정도”이며, 자주 바뀔 것은 불안정한 자리에 둔다. - SAP: 안정된 컴포넌트는 그만큼 추상적이어야 한다(
A가 높아야). 안정+추상이라야 바꾸지 않고 확장할 수 있다. - 주계열:
I·A평면에서 (0,1)과 (1,0)을 잇는 선. 벗어나면 고통의 구역(구체+안정, 못 바꾼다)이나 쓸모없는 구역(추상+무의존, 안 쓰인다)으로 떨어진다.
컴포넌트를 어떻게 묶을 것인가(13장)와 어떻게 이을 것인가(14장)를 마치면, 이제 벽돌은 다 놓였다. 다음 장부터 마틴은 이 벽돌들을 쌓아 무엇을 세우는지 — 아키텍처란 무엇이며 아키텍트는 무엇을 하는 사람인지 — 로 넘어간다.
다음장으로 15장