17장이 “경계선을 어디에 그을 것인가”를 물었다면, 18장은 그 선을 실제로 그었을 때 런타임에 무슨 일이 벌어지는가를 해부한다. 아키텍처 다이어그램 위에서 경계는 컴포넌트를 가르는 우아한 실선이지만, 돌아가는 프로그램 안에서 그 실선의 정체는 훨씬 시시하다 — 한쪽의 함수가 다른 쪽의 함수를 호출하고, 인자를 넘기고, 값을 돌려받는 것. 경계 횡단은 그 이상도 이하도 아니다.
이 장의 핵심은 두 층을 분리해서 보는 데 있다. 하나는 런타임에 제어가 흐르는 방향 — 이건 자연스럽게 저수준이 고수준을 부르는 쪽으로 흐른다. 다른 하나는 소스 코드가 서로를 참조하는 의존성의 방향 — 이건 아키텍트가 통제해야 하는 값이다. 경계란 이 둘이 갈라지도록, 즉 제어 흐름과 소스 의존성이 반대가 되도록 관리되는 지점이다. 그리고 그 관리에는 등급이 있다. 소스 수준에서 살짝 갈라놓을 수도, 물리적으로 다른 기계에 떼어 놓을 수도 있다. 그 등급마다 값이 다르다.
경계 횡단은 결국 함수 호출이다
거창한 이름에 속으면 안 된다. 경계를 넘는다는 것은 런타임에 경계 이쪽의 함수가 경계 저쪽의 함수를 호출하고 데이터를 주고받는 일이다. 그래서 경계 횡단을 제대로 다루는 기술의 전부는, 이 함수 호출에서 소스 코드 의존성을 어느 쪽으로 향하게 할 것인가를 관리하는 데 있다.
왜 이게 문제가 되나. 자연 상태에서 제어 흐름과 소스 의존성은 같은 방향으로 묶인다. A가 B를 호출하려면 A의 소스가 B의 이름을 알아야 하니, 제어가 A→B로 흐르는 만큼 소스 의존성도 A→B로 걸린다. 문제는 아키텍처가 요구하는 방향이 종종 그 반대라는 데 있다. 고수준 업무 규칙이 저수준 장치를 런타임에 호출해야 하지만, 소스 의존성만큼은 저수준이 고수준을 향하게 뒤집어야 한다. 이 뒤집기가 바로 11장의 의존성 역전이고, 경계는 그 역전이 일어나는 자리다.
풀어 보면 이렇다. 고수준 컴포넌트가 저수준 컴포넌트를 부르되, 그 부름의 대상을 저수준의 구체 클래스가 아니라 고수준 쪽에 선언된 인터페이스로 삼는다. 그러면 런타임 제어는 여전히 고수준→저수준으로 흐르지만, 소스에서는 저수준이 그 인터페이스를 구현하느라 고수준을 향해 의존하게 된다. 화살표 하나가 반대로 꺾인 것뿐인데, 이 한 번의 꺾임이 저수준의 변경을 경계 안에 가둔다.
판단 기준: 어떤 경계가 “진짜 아키텍처 경계”인지 알고 싶으면, 그 선을 넘는 소스 의존성이 제어 흐름과 반대 방향으로 꺾여 있는지를 보라. 꺾여 있으면 경계고, 제어와 나란히 흐르면 그냥 함수 호출이다. 함정: 다이어그램에 실선을 그었다고 경계가 생기는 게 아니다 — 소스 의존성이 여전히 고수준에서 저수준으로 새고 있으면, 그 선은 그림 속 장식일 뿐 런타임에는 아무것도 갈라놓지 못한다.
두려운 단조로움 — 경계가 없는 세계
가장 단순한 경계는 경계가 아예 없는 것이다. 단일 프로세서, 단일 주소 공간에서 함수와 데이터가 자유롭게 서로를 부른다. 마틴은 이 상태를 “두려운 단조로움(the dreadful monolith)“이라 부른다 — 물리적 경계가 하나도 없어서, 시스템이 커질수록 아무 곳이나 아무 곳을 부를 수 있는 균질한 진흙 덩어리로 굳어 가기 때문이다.
이 두려움을 다스리는 첫 수단이 소스 수준의 규율이다. 물리적으로는 한 덩어리로 배포되지만, 소스 코드 안에서 의존성의 방향을 다형성으로 통제해 컴포넌트들을 논리적으로 갈라 놓는다. 함수 하나가 경계를 넘을 때, 저수준이 고수준의 인터페이스를 구현하도록 의존성을 역전시켜 두는 것 — 이것이 모놀리식 안에서도 경계가 살아 있게 만드는 방법이다.
이런 경계 횡단은 값이 거의 공짜다. 경계를 넘는 것이 여전히 같은 주소 공간 안의 함수 호출이라, 데이터를 복사하거나 직렬화할 필요가 없다. 스택에 인자를 얹고 점프할 뿐이다. 대신 값을 치르는 곳은 런타임이 아니라 빌드와 배포다. 논리적으로 갈라 놓았어도 물리적으로는 한 덩어리라, 한 컴포넌트를 고치면 전체를 다시 컴파일하고 통째로 다시 배포해야 한다.
판단 기준: 통신 오버헤드에 예민한 시스템(고빈도 호출, 저지연 요구)이라면 경계를 물리적으로 벌리지 말고 모놀리식 안의 소스 수준 경계로 유지하라 — 호출 비용이 사실상 0이면서도 의존성 규율은 지킬 수 있다. 함정: 모놀리식이라는 이유로 경계 규율까지 생략하면, 남는 건 정말로 두려운 단조로움이다. 물리적 한 덩어리와 논리적 진흙 덩어리는 다른 문제다 — 전자는 배포 전략이고 후자는 설계 실패다.
결합을 벌리는 스펙트럼
경계는 있거나 없는 이분법이 아니다. 같은 논리적 경계라도 그것을 얼마나 물리적으로 벌려 놓느냐는 연속적인 눈금 위에서 고를 수 있다. 마틴은 그 눈금을 결합이 약해지는 순서대로 늘어놓는다.
- 소스 수준 (모놀리식). 컴포넌트를 같은 실행 파일 안에 두되 소스 의존성만 관리한다. 경계 횡단은 함수 호출, 비용 0. 대가는 전체 재컴파일·재배포.
- 배포 수준 (.jar / .dll / gem / DLL). 컴포넌트를 독립적으로 배포 가능한 바이너리 단위로 가른다. 여전히 같은 주소 공간에서 도니 횡단은 함수 호출이라 저렴하다. 얻는 것은 독립 배포 — 한 컴포넌트만 새 버전으로 갈아 끼울 수 있다.
- 스레드 수준. 배포 단위 안에서 실행을 여러 스레드로 나눈다. 경계라기보다 실행 계획·순서에 관한 조직화에 가깝다. 같은 프로세스·주소 공간을 공유하므로 데이터 접근은 값싸지만, 순서와 공유 상태를 사람이 관리해야 한다.
- 로컬 프로세스 수준. 같은 기계 안에서 별도의 주소 공간을 가진 프로세스로 가른다. 소켓이나 OS의 프로세스 간 통신(공유 메모리, 파이프, 메시지 큐)으로 대화한다. 횡단할 때 데이터를 직렬화·역직렬화해야 하니 함수 호출보다 확연히 비싸다. 얻는 것은 강한 격리 — 한 프로세스가 죽어도 다른 프로세스는 산다.
- 서비스 수준. 가장 강한 분리. 네트워크 너머의 다른 기계일 수도 있는 프로세스로 가르고, 모든 통신이 네트워크를 탄다. 횡단 비용이 가장 크고(직렬화 + 네트워크 왕복 + 지연), 물리적 위치를 몰라도 되는 만큼 결합은 가장 약하다.
이 스펙트럼을 관통하는 법칙은 단순하다. 아래로 내려갈수록 결합은 약해지고 통신 비용은 비싸진다. 소스 수준에서 서비스 수준으로 갈수록 컴포넌트는 서로의 존재를 덜 알아도 되고 독립적으로 배포·확장·격리되지만, 그 대가로 경계를 넘는 한 번의 대화가 함수 호출에서 네트워크 왕복으로 부풀어 오른다. 아키텍처 결정이란 이 눈금 위에서 어디에 설 것인가를 고르는 일이다.
판단 기준: 경계의 물리적 등급을 고를 때 “얼마나 격리하고 싶은가”와 “그 경계를 초당 몇 번 넘나드는가”를 함께 저울에 올려라 — 격리 욕구가 통신 빈도를 이길 때만 눈금을 아래로 내린다. 함정: 결합이 약할수록 좋다는 믿음으로 무작정 서비스 수준까지 내려가는 것. 약한 결합은 공짜가 아니라 매 호출의 직렬화와 왕복으로 값을 치르는 것이며, 자주 넘나드는 경계를 네트워크 너머로 밀면 그 비용이 시스템의 발목을 잡는다.
로컬 프로세스 — 격리를 사고 직렬화를 치른다
로컬 프로세스는 스펙트럼의 중간에서 흥미로운 지점을 차지한다. 명령줄 프로세스든 OS가 만든 자식 프로세스든, 이들은 각자 독립된 주소 공간을 갖는다. 대개 메모리 보호로 격리되어 서로의 메모리를 함부로 공유하지 못한다. 그래서 얻는 격리는 스레드 수준보다 확실히 강하다 — 한쪽의 메모리 오염이 다른 쪽으로 번지지 않는다.
문제는 대화의 값이다. 프로세스 경계를 넘어 함수를 부르려면 인자를 직렬화해 상대에게 보내고, 상대는 그걸 역직렬화해 받는다. 응답도 같은 여정을 거꾸로 밟는다. 이 마샬링·언마샬링 비용이 로컬 프로세스 경계의 본질적 세금이다. 같은 기계 안이라 네트워크 지연은 없지만, 데이터를 바이트 열로 폈다 접는 수고는 피할 수 없다.
그래서 로컬 프로세스 경계에서도 소스 의존성의 규율은 그대로 살아 있어야 한다. 저수준 프로세스가 고수준 프로세스에 소스로 의존하게 만들어, 고수준이 저수준의 구체를 모르게 둔다. 프로세스로 물리적으로 갈라 놓았다고 저수준이 고수준을 향해 마음대로 의존해도 되는 게 아니다 — 스펙트럼의 어느 등급에 있든, 경계를 넘는 의존성은 언제나 고수준을 향해 꺾여 있어야 한다.
판단 기준: 강한 격리(장애 격리, 자원 분리)가 필요하지만 네트워크까지 벌릴 이유는 없는 경우, 로컬 프로세스가 서비스보다 값싼 답이다 — 직렬화 비용은 물되 네트워크 왕복은 물지 않는다. 함정: 프로세스로 갈랐으니 의존성 규율은 저절로 따라온다고 믿는 것. 물리적 분리와 논리적 의존성 방향은 별개의 축이라, 프로세스를 나눠 놓고도 고수준이 저수준 구체를 향해 의존하면 격리만 사고 결합은 그대로 남는다.
서비스 — 가장 약한 결합, 가장 비싼 대화
스펙트럼의 맨 아래, 가장 강한 경계가 서비스다. 서비스끼리는 상대가 같은 기계에 있는지 네트워크 너머에 있는지조차 몰라도 된다고 가정한다. 이 무지가 곧 가장 약한 결합이다 — 물리적 위치, 배포 시점, 사용하는 언어까지 서로에게서 숨긴다. 그래서 서비스는 독립 개발과 독립 배포의 정점에 선다.
값은 그만큼 비싸다. 모든 통신이 네트워크를 타므로, 경계를 넘는 한 번의 대화가 함수 호출에 비해 여러 자릿수 느리다. 직렬화 비용에 더해 네트워크 왕복 지연이 얹히고, 그 지연은 물리 법칙이라 코드로 지울 수 없다. 그래서 서비스 경계에서는 경계를 넘는 횟수 자체를 아끼도록 대화를 설계해야 한다. 잦고 자잘한 왕복은 서비스 아키텍처의 가장 흔한 성능 재앙이다.
여기서 마틴이 미리 심어 두는 경계심 하나 — 서비스로 갈랐다는 사실이 좋은 아키텍처를 보장하지는 않는다. 이 문제는 27장에서 본격적으로 다뤄지지만, 스펙트럼의 관점에서 미리 짚어 둘 값어치가 있다. 서비스는 물리적 분리의 가장 강한 형태일 뿐, 그 안에서 소스 의존성이 아무렇게나 걸려 있으면 여전히 결합된 진흙 덩어리다. 값비싼 네트워크 경계를 치르고도 아키텍처 경계는 못 사는, 최악의 거래가 가능하다.
판단 기준: 서비스로 벌리는 결정은 “독립 배포·독립 확장·언어 독립”이라는 이득이 “네트워크 왕복 비용 + 운영 복잡도”를 확실히 넘어설 때만 내린다 — 이 셋 중 지금 실제로 필요한 게 없다면 눈금을 위로 올려라. 함정: 서비스 경계를 아키텍처 경계와 동일시하는 것. 네트워크로 갈라 놓는 것은 배포 전략이지 의존성 규칙이 아니며, 진짜 아키텍처 경계는 각 서비스 내부의 컴포넌트 의존성이 고수준을 향해 꺾여 있느냐에서 온다.
경계는 하나로 고정되지 않는다
이 스펙트럼을 알면 얻는 실천적 자유가 있다. 경계의 등급은 시스템의 수명 동안 옮겨 다닐 수 있는 값이라는 것이다. 소스 수준 경계를 잘 관리해 두면, 나중에 그 선을 배포 수준으로, 다시 프로세스나 서비스 수준으로 밀어낼 때 소스 코드는 거의 손대지 않고 통신 메커니즘만 갈아 끼울 수 있다.
핵심은 어느 등급에서든 소스 의존성의 방향을 똑같이 관리해 두는 것이다. 고수준이 저수준의 구체를 모르게, 경계를 넘는 화살표가 늘 고수준을 향해 꺾이게 유지하면, 물리적 등급은 배포 시점의 결정으로 미룰 수 있다. 처음엔 모놀리식 안의 함수 호출로 시작했다가, 부하가 커지면 그 경계 하나만 서비스로 승격시키는 식이다. 15장이 말한 “선택지를 열어 두기”가 경계의 물리적 등급에도 그대로 적용된다.
판단 기준: 미래에 벌어질지 모를 분산을 위해 오늘부터 서비스로 갈라 놓지 말고, 소스 수준 경계로 의존성만 바르게 꺾어 둔 뒤 물리적 승격은 실제 필요가 증명될 때 하라 — 이게 값을 뒤로 미루는 방법이다. 함정: 반대로, 소스 의존성을 방치한 채 물리적 분리만 서두르면 나중에 등급을 옮길 때 통신 메커니즘뿐 아니라 얽힌 의존성까지 함께 뜯어야 한다 — 그 순간 경계 승격은 값싼 배포 결정이 아니라 값비싼 재설계가 된다.
- 경계 횡단은 런타임에 한쪽 함수가 다른 쪽 함수를 부르는 일일 뿐이다. 경계를 다루는 기술의 전부는 그 호출에서 소스 의존성을 고수준을 향해 꺾어 두는 것이다.
- 제어 흐름(저수준→고수준으로 자연히 흐름)과 소스 의존성(고수준을 향해 인위적으로 꺾음)은 다른 축이다. 이 둘이 갈라지는 지점이 진짜 아키텍처 경계다.
- 결합 분리의 스펙트럼: 소스(모놀리식) → 배포(.jar/.dll) → 스레드 → 로컬 프로세스 → 서비스. 아래로 갈수록 결합은 약해지고 통신 비용은 비싸진다.
- 물리적 등급을 고르는 일은 “얼마나 격리할까”와 “얼마나 자주 넘나들까”의 저울질이다. 격리는 공짜가 아니라 직렬화와 네트워크 왕복으로 값을 치른다.
- 소스 의존성만 바르게 관리해 두면 경계의 물리적 등급은 나중으로 미룰 수 있는 배포 결정이 된다 — 미리 서비스로 벌리지 말고 필요가 증명될 때 승격하라.
다음장으로 19장