Step 03. 모든 아키텍처를 관통하는 하나의 원리 — 의존성

만화로 보는 요약 — 먼저 읽어보세요
의존성 화살표의 방향이 경계를 만들며, 핵심 정책은 외부 DB와 프레임워크 같은 세부사항에 끌려가지 않아야 한다.
면접 실전 질문: ① 의존성이란 정확히 무엇인가? ② DIP는 무엇을 뒤집는가? ③ 추상화는 누가 소유해야 하는가?
이 장의 질문: “의존성 방향이 곧 아키텍처다”라는 말은 무슨 뜻인가? 그리고 그 방향을 마음대로 정할 수 있게 해주는 지렛대 — 의존성 역전 — 는 정확히 어떻게 작동하는가?
1. “의존한다”는 것의 정확한 의미
앞 장의 세 무기(분리, 은닉, 결합도)에는 공통의 재료가 있었다. 모듈 A가 모듈 B를 안다는 사실 자체다. 이 “안다”를 정확히 정의하자.
A가 B에 의존한다 = A의 소스 코드에 B의 이름이 등장한다. import, 타입 선언, 생성자 호출, 상속 — 형태는 여러 가지지만 결과는 하나다: B가 바뀌면 A도 영향을 받는다. 다시 컴파일되거나, 수정되거나, 최소한 “B의 변경이 A를 깨뜨리지 않았는지” 검증돼야 한다.
여기서 앞 장의 논의가 기하학이 된다. 시스템의 모든 모듈을 점으로, 모든 의존을 화살표로 그리면 의존성 그래프가 나온다. 그리고:
변경의 파급은 의존성 화살표의 역방향으로 흐른다.
B가 바뀌면, B를 가리키는 모든 화살표의 출발점(A)이 흔들리고, A를 가리키는 화살표의 출발점이 또 흔들린다. 앞 장에서 “변경의 파급을 가두라”고 했다 — 파급이 화살표를 거슬러 흐른다면, 파급을 가둔다는 것은 곧 화살표의 방향을 통제한다는 것이다. 아키텍처 스타일들이 그리는 온갖 도형(계층, 육각형, 동심원)의 실체는 전부 이 화살표 배치도다.
한 가지 구분을 미리 해두자. 의존에는 두 종류가 있다.
- 소스 의존성(source dependency): 코드가 코드의 이름을 아는 것. 컴파일 타임의 관계.
- 제어 흐름(flow of control): 실행 시 누가 누구를 호출하는가. 런타임의 관계.
직관적으로 둘은 같은 방향일 것 같다 — 호출하려면 이름을 알아야 하니까. 이 둘을 분리할 수 있다는 발견이 이 장의 클라이맥스다. 잠시 미뤄두고, 먼저 왜 분리가 필요한지부터.
2. 방향이 전부다 — 정책과 세부사항
같은 박스 세 개로 이루어진 두 시스템을 상상하자: 주문정책, 주문저장, 알림발송. 박스가 같아도 화살표가 다르면 완전히 다른 시스템이다.
- 시스템 1:
주문정책 → 주문저장(MySQL),주문정책 → 알림발송(SMTP) - 시스템 2:
주문저장(MySQL) → 주문정책 ← 알림발송(SMTP)(화살표가 정책을 향한다)
시스템 1에서 MySQL을 바꾸면 정책이 흔들린다. 시스템 2에서는 정책이 미동도 않는다. 박스의 목록은 기능 명세가 결정하지만, 화살표의 방향은 설계자가 결정한다 — 그리고 Step 01의 정의(아키텍처 = 되돌리기 비싼 결정들)에 비추면, 화살표 배치야말로 되돌리기 가장 비싼 결정이다.
그렇다면 어느 방향이 옳은가? 판단 기준은 Robert C. Martin의 용어를 빌리면 **수준(level)**이다.
- 상위 수준(정책, policy): 시스템이 존재하는 이유. 업무 규칙. “주문 금액이 한도를 넘으면 승인이 필요하다.”
- 하위 수준(세부사항, detail): 그 규칙을 오늘 실현하는 수단. MySQL, SMTP, HTTP, 프레임워크.
Parnas의 기준(바뀔 법한 것)으로 보면 세부사항이 정책보다 훨씬 자주 바뀐다. DB는 교체돼도 “한도 초과 시 승인” 규칙은 남는다. 따라서 원칙은 하나다:
의존성 화살표는 세부사항에서 정책을 향해야 한다. 자주 바뀌는 것이 덜 바뀌는 것을 가리켜야 한다.
문제는, 자연스럽게 코드를 짜면 정확히 반대가 된다는 것이다. 정책이 저장을 호출하니까(제어 흐름), 정책 코드에 저장 클래스의 이름이 등장하고(소스 의존성), 화살표는 정책 → 세부사항을 향한다. 1970년대의 구조적 설계에서 상위 함수가 하위 함수를 호출하며 자연히 생기던 그 방향이다. 호출 방향과 의존 방향이 묶여 있는 한, 정책은 영원히 세부사항의 인질이다.
3. 지렛대 — 의존성 역전 원칙 (DIP)
이 인질극을 끝낸 것이 다형성(polymorphism)이고, 그것을 설계 원리로 정식화한 것이 Robert C. Martin의 **의존성 역전 원칙(Dependency Inversion Principle, 1996년 C++ Report 칼럼)**이다. 원문은 두 문장이다.
A. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다. 둘 다 추상에 의존해야 한다. B. 추상은 세부사항에 의존해서는 안 된다. 세부사항이 추상에 의존해야 한다.
작동 원리를 주문 예제로 보자. 원래 그림:
[주문정책] ──의존──> [MySQL주문저장] (호출도, 의존도 같은 방향)여기에 인터페이스 하나를 끼운다. 주문저장소라는 추상 — “저장하고 조회할 수 있다”는 계약만 있고 MySQL은 모른다.
[주문정책] ──의존──> [주문저장소(인터페이스)] <──의존── [MySQL주문저장]
↑ 소유화살표를 보라. 정책은 인터페이스를 알고, MySQL 구현체도 인터페이스를 안다(구현하니까). MySQL 쪽 화살표가 뒤집혔다. 제어 흐름은 여전히 정책 → 저장소 방향으로 흐른다 — 런타임에는 정책이 (인터페이스 뒤의) MySQL 코드를 호출한다. 그러나 소스 의존성은 제어 흐름을 거슬러 세부사항 → 추상을 향한다. 1절에서 예고한 분리가 이것이다:
다형성은 소스 의존성을 제어 흐름으로부터 해방시킨다. 그 순간부터 화살표의 방향은 물리 법칙이 아니라 설계 결정이 된다.
“역전(inversion)“이라는 이름은 여기서 온다 — 구조적 설계에서 당연했던 방향(상위가 하위의 이름을 앎)이 뒤집히기 때문이다. 공정하게 말하면 OO 이전에도 함수 포인터로 방향을 뒤집을 수는 있었다. 다형성이 한 일은 무에서의 창조가 아니라, 그 위험하고 어려운 곡예를 안전하고 일상적인 도구로 만든 것이다.
4. 진짜 핵심 — 추상의 소유권
여기까지는 많은 글이 다룬다. 그런데 DIP에는 자주 누락되는 절반이 있다. 그 인터페이스는 누구의 것인가?
주문저장소 인터페이스를 저장 구현체 옆에 — persistence 패키지에 — 둔다고 하자. 인터페이스는 있지만, 정책은 여전히 persistence 패키지를 import한다. 화살표는 하나도 안 뒤집혔다. 인터페이스라는 형식만 갖췄을 뿐이다.
Martin의 답: 추상은 사용자(상위 정책)의 것이다. 주문저장소 인터페이스는 주문 도메인 패키지 안에 산다. 그 인터페이스는 “저장 기술이 제공할 수 있는 것”의 목록이 아니라 “정책이 필요로 하는 것”의 선언이다. 그래서 메서드도 INSERT INTO...를 닮지 않고 한도초과주문을찾는다() 처럼 정책의 언어를 닮는다.
이 소유권 이동이 역전의 실체다. 계약을 구현자가 쓰면 “내가 제공하는 것 중에 골라 써”가 되고, 사용자가 쓰면 “내가 필요한 걸 네가 맞춰”가 된다. 갑을이 바뀌는 것이다. 훗날 헥사고날 아키텍처가 이 소유권 관계에 이름을 붙인다 — 애플리케이션이 소유한 계약이 포트, 그것을 바깥에서 맞춰주는 구현이 어댑터다. 이 장의 문장 하나가 그 장 전체의 예습인 셈이다.
5. 어느 쪽으로 뒤집을 것인가 — 안정성의 방향 (SDP)
모든 화살표를 뒤집을 수는 없고, 그럴 필요도 없다. 방향의 일반 원칙을 주는 것이 Martin의 **안정 의존성 원칙(Stable Dependencies Principle)**이다.
안정된 것을 향해 의존하라(Depend in the direction of stability).
여기서 “안정”은 칭찬이 아니라 측정값이다. 어떤 모듈이 안정적이라는 것은 좋다는 뜻이 아니라 바꾸기 어렵다는 뜻이다 — 많은 모듈이 그것에 의존하고 있어서, 바꾸면 파급이 크기 때문에. Martin은 이를 수치화했다: 들어오는 의존(Ca, 나를 쓰는 쪽)과 나가는 의존(Ce, 내가 쓰는 쪽)의 비율로 불안정도 I = Ce/(Ca+Ce)를 정의한다. I=0이면 완전 안정(모두가 나를 쓰고 나는 아무도 안 씀), I=1이면 완전 불안정.
한 가지 주석: 이 지표들은 원래 개별 클래스가 아니라 컴포넌트(패키지) 단위의 원칙이다. 클래스마다 I를 계산하라는 뜻이 아니라, 여기서는 방향 감각을 익히기 위해 모듈 수준으로 축약해 쓴다.
원칙의 의미: 자주 바뀌어야 하는 모듈(불안정)이 화살표를 받아서는 안 된다. 자주 바뀌는 것에 많은 것이 의존하면, 매 변경이 대규모 파급이 된다. 반대로 화살표가 몰리는 곳(안정)은 바뀌지 않을 것들 — 정책, 그리고 추상 — 이어야 한다. 이것이 짝 원칙인 **안정 추상 원칙(SAP)**이다: 안정된 모듈일수록 추상적이어야 한다. 바꾸기 어려운 자리에 구체적인 것(MySQL 클래스)이 앉아 있으면 시스템은 경직되고, 그 자리에 추상(저장소 인터페이스)이 앉아 있으면 안정성과 유연성이 공존한다.
정리하면 방향 결정의 알고리즘이 나온다: 변경 가능성으로 수준을 나누고(Parnas), 화살표를 안정한 쪽·추상한 쪽으로 향하게 하며(SDP/SAP), 자연 방향이 그걸 거스르면 DIP로 뒤집는다.
6. 경계 — 화살표가 그리는 국경선
이제 이 장의 도구들을 하나의 그림으로 합치자.
의존성 그래프 위에 선을 하나 긋는다. 그 선을 **경계(boundary)**라 부를 수 있으려면 조건이 하나다: 화살표가 그 선을 한 방향으로만 건너야 한다. 양방향으로 넘나들면 그것은 경계가 아니라 그냥 밀집 지대다 — 선 양쪽이 사실상 한 덩어리라는 뜻이므로.
- 어디에 선을 긋는가? → Parnas: 변경의 이유가 갈리는 곳. 함께 바뀌는 것은 같은 쪽에, 따로 바뀌는 것은 다른 쪽에.
- 화살표는 어느 쪽으로 건너는가? → SDP/SAP: 더 안정적이고 더 추상적인 쪽으로.
- 자연 방향이 거꾸로라면? → DIP: 경계 위에 정책 소유의 인터페이스를 세우고 뒤집는다.
경계 조건에서 따라 나오는 금지 규칙이 하나 있다. 화살표가 원을 그리면 안 된다 — Martin의 비순환 의존성 원칙(ADP, Acyclic Dependencies Principle)이다. A→B→C→A로 사이클이 생기면 셋 중 무엇을 바꿔도 파급이 나머지 전부를 돌아 자신에게 되돌아온다. 셋은 사실상 한 덩어리이고, 그 사이 어디에도 경계는 존재하지 않는다. 실무에서 가장 흔한 그래프 병리이며, 해법은 이미 손에 있다 — 사이클의 한 변에 DIP를 적용해 화살표 하나를 뒤집어 끊거나, 얽힌 부분을 새 모듈로 뽑아낸다.
이 도구 상자가 Part 2 전체의 열쇠다. 미리 말해두면 — 레이어드 아키텍처는 경계를 수평으로 긋되 화살표를 아래(DB)로 흘려보낸 설계이고, 헥사고날과 클린은 바깥의 양쪽(UI도, DB도)이 모두 안(도메인)을 향하도록 화살표를 뒤집은 설계다. 스타일들의 차이는 결국 (1) 선을 어디에 긋는가, (2) 화살표를 어느 쪽으로 향하게 하는가, 두 선택의 조합이다. 다음 파트에서 각 스타일을 만날 때마다 이 두 질문을 던지면 된다.
7. 흔한 오해 바로잡기
오해 1: “DIP = DI 컨테이너(스프링) 쓰기.”
전혀 다른 층위다. DIP는 원리 — 화살표를 어디로 향하게 할 것인가, 추상을 누가 소유하는가. DI(의존성 주입)는 기법 — 객체를 누가 생성해서 꽂아주는가 (용어는 Fowler의 2004년 글에서 정착됐다). DI 컨테이너를 쓰면서 인터페이스를 구현체 패키지에 두면, 주입은 받지만 역전은 없다. 반대로 컨테이너 없이 main()에서 수동으로 조립해도 DIP는 완벽할 수 있다. 스프링이 해주는 것은 조립이지 설계가 아니다.
오해 2: “인터페이스를 끼우면 DIP다.”
4절에서 봤듯 소유권이 핵심이다. 구현체 옆에 사는 인터페이스, 구현체가 제공하는 메서드를 그대로 베낀 인터페이스(IUserService 류)는 화살표를 하나도 뒤집지 않는다. 형식적 추상은 간접층 하나만큼의 비용을 내고 아무것도 사지 않는다.
오해 3: “모든 의존을 역전해야 한다.” SDP를 다시 보라 — 안정된 것에는 그냥 의존하면 된다. 표준 라이브러리, 언어 런타임, 사실상 바뀌지 않을 유틸리티에 인터페이스를 씌우는 것은 역전이 아니라 낭비다. 역전의 비용(간접층, 탐색 난이도)이 정당화되는 곳은 변경 가능성이 높은 세부사항과 정책이 만나는 국경뿐이다. 앞 장의 언어로: 결합의 제거가 아니라 배치가 목표다.
오해 4: “의존성이 적을수록 좋은 설계다.” 화살표의 개수가 아니라 방향과 배치가 문제다. 화살표가 많아도 전부 안정된 추상을 향하면 건강하고, 단 하나라도 정책이 세부사항을 직접 가리키면 그곳이 파급의 통로가 된다.
8. 요약, 그리고 다음 장으로
- 의존성은 “이름을 아는 것”이고, 변경의 파급은 화살표의 역방향으로 흐른다. 아키텍처 다이어그램의 실체는 화살표 배치도다.
- 원칙: 세부사항(자주 바뀜)이 정책(덜 바뀜)을 가리켜야 한다. 그러나 자연스러운 코드는 반대 방향이 된다 — 호출이 의존을 끌고 가므로.
- DIP: 다형성으로 소스 의존성을 제어 흐름에서 분리하면, 화살표 방향은 설계 결정이 된다. 형식이 아니라 추상의 소유권 이동(계약은 사용자의 것)이 역전의 실체다.
- SDP/SAP: 화살표는 안정되고 추상적인 쪽으로. 불안정한 것이 화살표를 받으면 안 된다.
- 경계 = 화살표가 한 방향으로만 건너는 선. 사이클(순환 의존)은 경계를 소멸시키므로 금지된다(ADP) — 끊는 도구 역시 DIP다.
- 어디에 긋는가(Parnas) × 어느 쪽으로 건너는가(DIP/SDP) — 이 두 질문이 Part 2의 모든 스타일을 여는 열쇠다.
다음 장 예고: 원론의 마지막 조각이다. 화살표를 통제하는 법은 알았는데, 무엇을 위해 통제하는가? “변경에 강하다”, “테스트하기 쉽다”, “장애에 견딘다” — 이런 좋음들은 서로 다른 종류이고, 자주 충돌한다. 다음 장에서는 그 좋음들의 이름표 — 품질 속성 — 를 배운다. Part 2에서 각 아키텍처가 치른 “고통”을 측정할 바로 그 언어다.
원전
| 연도 | 문헌 | 이 장에서의 역할 |
|---|---|---|
| 1994 | Martin, OO Design Quality Metrics: An Analysis of Dependencies | 불안정도 I = Ce/(Ca+Ce), 안정성 지표의 원전 |
| 1996 | Martin, The Dependency Inversion Principle (C++ Report) | DIP 정식화 — 두 문장의 원문 |
| 2002 | Martin, Agile Software Development: Principles, Patterns, and Practices | SOLID 집대성, SDP/SAP, 패키지 원칙 |
| 2004 | Fowler, Inversion of Control Containers and the Dependency Injection pattern | ”의존성 주입” 용어의 정착 — DIP와의 층위 구분 |