15장이 아키텍처의 목적을 “개발·배포·운영·유지보수를 지원하고 선택지를 열어 두는 것”이라 못박았다면, 16장은 그 추상적인 목적을 손에 잡히는 요구로 번역한다. 좋은 아키텍처는 네 가지를 동시에 떠받쳐야 한다 — 시스템의 유스케이스, 운영, 개발, 배포다. 마틴이 이 장에서 던지는 질문은 이것이다. 이 넷은 서로 다른 시점에, 서로 다른 이유로, 서로 다른 사람에 의해 압력을 받는데, 하나의 구조가 어떻게 그 모두를 한꺼번에 견디는가.

답은 결국 하나의 원리로 수렴한다 — 같은 이유로 바뀌는 것을 모으고, 다른 이유로 바뀌는 것을 가른다. 이 장은 그 원리를 네 개의 지지 요구 위에서 차례로 검증하고, 마지막에 중복과 결합 분리 모드라는 두 개의 실천적 함정으로 착지한다.

네 가지를 동시에 떠받친다는 것

네 요구를 하나씩 보면 각각이 아키텍처에 서로 다른 것을 요구한다.

유스케이스. 아키텍처는 시스템이 무슨 일을 하는지를 지원해야 한다. 장바구니 시스템의 아키텍처는 장바구니 유스케이스를 드러내고 떠받쳐야 한다. 이것이 21장에서 “소리치는 아키텍처”로 다시 등장할 주제의 씨앗이다 — 구조를 들여다보면 그 시스템의 의도가 읽혀야 한다.

운영. 아키텍처는 시스템이 감당해야 할 처리량과 응답 시간을 지원해야 한다. 초당 십만 건을 받아야 한다면 그만한 규모로 확장 가능한 형태여야 한다. 다만 마틴은 여기서 한 가지를 분명히 한다 — 운영 요구는 대개 하드웨어를 더 투입해 해결할 수 있고, 아키텍처가 좋으면 처리량 요구가 바뀌어도 소스 코드를 갈아엎지 않고 배포 형태만 바꿔 대응할 수 있다. 운영은 네 요구 중 가장 유연하게 미룰 수 있는 축이다.

개발. 아키텍처는 그것을 만드는 조직을 지원해야 한다. 이 대목에서 콘웨이의 법칙이 걸린다 — 시스템의 구조는 그것을 만든 조직의 소통 구조를 닮는다. 다섯 팀이 붙는 시스템이라면, 다섯 팀이 서로 발을 밟지 않고 각자의 컴포넌트를 독립적으로 개발할 수 있도록 잘 격리된 컴포넌트로 나뉘어야 한다. 팀 경계와 컴포넌트 경계가 어긋나면 모든 변경이 협상이 된다.

배포. 아키텍처는 배포 용이성을 지원해야 한다. 목표는 “즉시 배포(immediate deployment)“다 — 빌드한 시스템이 별도의 수작업 조립 없이 한 번에 떠야 한다. 여러 설정 스크립트를 순서대로 돌리고 여기저기 파일을 손으로 옮겨야 뜨는 시스템은, 아무리 잘 짜였어도 배포라는 목적을 배신하고 있다.

판단 기준: 아키텍처를 평가할 때 “동작하는가”만이 아니라 이 네 축 각각에 대해 “이 구조가 그것을 떠받치는가”를 따로 물어라 — 유스케이스가 읽히는가, 부하가 늘면 형태만 바꿔 대응되는가, 팀들이 독립적으로 개발되는가, 한 번에 배포되는가. 함정: 네 축은 서로 다른 사람의 관심사라 한 축만 챙기면 성과처럼 보인다. 개발 편의만 좇아 팀 단위로 자르면 유스케이스가 흩어지고, 운영 성능만 좇아 최적화하면 배포가 얼어붙는다 — 넷을 저울에 함께 올려야 아키텍처다.

왜 이 넷을 한 구조로 떠받칠 수 있는가

여기서 마틴이 짚는 반가운 사실이 하나 있다. 이 네 요구를 만족시키는 방법이 서로 충돌하기보다 대체로 같은 방향을 가리킨다는 것이다. 유스케이스를 잘 분리하면, 그 유스케이스를 다루는 팀도 잘 분리되고, 그 컴포넌트를 따로 배포하기도 쉬워지고, 부하가 몰리는 유스케이스만 떼어 확장하기도 쉬워진다. 잘 그은 경계선 하나가 네 요구에 동시에 값을 지불한다.

물론 완벽히 일치하지는 않는다. 미래에 어떤 유스케이스가 추가될지, 어떤 운영 요구가 닥칠지, 조직이 어떻게 바뀔지 지금 다 알 수는 없다. 그래서 마틴의 처방은 정답을 맞히려는 것이 아니라 선택지를 열어 두는 것이다. 결합을 잘 분리해 두면, 예상이 빗나갔을 때 시스템을 통째로 다시 짜지 않고 경계선 몇 개를 옮겨 대응할 수 있다. 이것이 15장의 “선택지를 열어 둔다”가 16장에서 구체적 기법으로 바뀌는 지점이다.

계층을 변경의 축을 따라 가른다

첫 번째 결합 분리는 수평적 계층을 따라 긋는다. UI, 애플리케이션 특화 업무 규칙, 애플리케이션 독립 업무 규칙, 데이터베이스 — 이들은 서로 다른 이유로, 서로 다른 속도로 바뀐다. UI는 업무 규칙과 무관하게 바뀌고, 데이터베이스는 UI나 업무 규칙과 무관하게 바뀐다. 서로 다른 이유로 바뀌는 것들이므로, 서로 다른 계층으로 갈라 두어야 한 축의 변경이 다른 축을 흔들지 않는다.

이것은 SRP와 CCP(공통 폐쇄 원칙)를 아키텍처 규모로 확대한 것이다. 7장에서 급여 클래스가 회계·인사·DBA라는 세 액터의 코드를 한 몸에 두어 병합 충돌을 부른 것처럼, 계층을 뭉개 두면 UI 변경이 업무 규칙 파일을 건드리고 그 반대도 일어난다.

유스케이스도 서로 갈라야 한다

두 번째 결합 분리는 계층과 직교하는 수직적 방향, 곧 유스케이스를 따라 긋는다. “주문을 추가한다”는 유스케이스와 “주문을 삭제한다”는 유스케이스는 서로 다른 이유로 바뀐다. 그러니 이 둘도 갈라 두어야 한다.

유스케이스는 시스템을 세로로 얇게 관통하는 조각이다 — 그것 나름의 UI, 그것 나름의 업무 규칙, 그것 나름의 데이터베이스 접근을 관통한다. 계층으로 한 번 가르고 유스케이스로 다시 가르면, 시스템은 격자로 쪼개진다. 이렇게 두 방향으로 결합을 분리해 두면 강력한 결과가 따라온다. 새 유스케이스를 추가할 때 기존 유스케이스를 건드릴 일이 거의 없고, 한 유스케이스를 담당하는 팀이 다른 유스케이스 팀과 충돌할 일도 줄어든다. 유스케이스 단위로 얇게 갈라 두었기 때문에, 부하가 몰리는 특정 유스케이스만 떼어 별도로 확장하는 것도 가능해진다. 계층 분리가 개발과 배포를 돕고, 유스케이스 분리가 다시 개발·배포·운영을 함께 돕는다 — 네 요구가 한 격자 위에서 만난다.

판단 기준: 새 유스케이스를 하나 추가한다고 상상해 보라. 손대야 할 파일이 그 유스케이스의 얇은 세로 조각 안에 갇히면 결합 분리가 제대로 된 것이고, 다른 유스케이스의 코드까지 열어야 한다면 경계가 잘못 놓인 것이다. 함정: 계층으로만 가르고 유스케이스로 가르지 않으면, 모든 유스케이스가 같은 서비스·같은 컨트롤러에 뭉쳐 서로의 변경에 붙잡힌다 — 세로줄을 긋지 않은 격자는 격자가 아니다.

개발과 배포의 독립성

이렇게 두 방향으로 결합을 분리해 두면 두 가지 독립성이 저절로 따라온다.

개발 독립성. 컴포넌트가 잘 분리되어 있으면 팀 구조에 대한 압력이 사라진다. 어느 팀이 UI를 맡든 업무 규칙을 맡든 특정 유스케이스를 맡든, 서로 격리된 컴포넌트를 만지므로 간섭이 최소화된다. 콘웨이의 법칙이 위협이 아니라 도구가 되는 지점이다.

배포 독립성. 계층과 유스케이스가 잘 분리되어 있으면 배포에 큰 유연성이 생긴다. 운영 중인 시스템에서도 계층이나 유스케이스를 핫스왑하듯 갈아 끼울 수 있다. 새 유스케이스를 추가할 때 그것을 담은 컴포넌트만 새로 배포하면 되고, 나머지는 재빌드·재배포할 필요가 없다.

판단 기준: 이 두 독립성은 목표가 아니라 결합 분리가 잘 됐는지를 비추는 거울이다 — 팀을 늘렸을 때 협상 비용이 늘지 않고, 한 기능을 고쳤을 때 그 조각만 다시 배포되면 결합이 제대로 갈라진 것이다. 함정: 독립성 자체를 목적으로 삼아 지금 필요도 없는 컴포넌트 경계를 미리 다 세우면, 아직 오지 않은 팀 분할과 배포 시나리오에 대가부터 치르는 과잉 설계가 된다 — 독립성은 결합 분리의 결과로 얻는 것이지, 미리 사 두는 보험이 아니다.

진짜 중복과 우발적 중복

결합을 분리하다 보면 반드시 마주치는 딜레마가 있다. 코드를 유스케이스별로 세로로 가르다 보면, 서로 다른 유스케이스에서 비슷해 보이는 코드가 여기저기 나타난다. DRY(중복하지 마라)를 배운 프로그래머의 손은 반사적으로 그것을 하나로 합치려 든다. 마틴은 여기서 멈추라고 말한다. 중복에는 두 종류가 있고, 그 둘을 구별하지 못하면 결합 분리가 거꾸로 무너진다.

진짜 중복(true duplication) 은 한쪽이 바뀌면 다른 쪽도 반드시 똑같이 바뀌어야 하는 중복이다. 이것은 실제로 하나의 지식이 두 곳에 복제된 것이므로, 합쳐서 제거해야 한다.

우발적 중복(accidental duplication) 은 지금 이 순간 코드가 우연히 닮았을 뿐, 서로 다른 이유로 바뀌기 때문에 시간이 지나면 갈라질 중복이다. 이것을 성급히 합치면, 나중에 두 유스케이스가 서로 다른 방향으로 진화할 때 억지로 붙여 놓은 공통 코드에 if와 특수 케이스가 덕지덕지 붙기 시작한다.

예를 들어 두 화면이 지금은 똑같은 형태의 레코드를 보여 준다고 하자.

// PageController — 조회 결과를 화면 레코드로 옮긴다
class PageController {
    View createView(Query q) {
        Row row = db.fetch(q);
        return new View(row.id, row.name, row.amount); // 지금은 세 필드
    }
}
 
// InvoiceController — 우연히 지금은 똑같아 보인다
class InvoiceController {
    View createView(Invoice inv) {
        Row row = db.fetch(inv);
        return new View(row.id, row.name, row.amount); // 형태만 같을 뿐
    }
}

createView는 지금 판박이다. 여기서 “중복이니 공통 메서드로 빼자”고 합치면, 청구서 화면에 세금 항목이 추가되고 조회 화면에는 정렬 순서가 추가되는 순간, 하나로 합쳤던 메서드는 두 요구를 다 만족시키려 플래그와 분기로 뒤엉킨다. 애초에 이 둘은 서로 다른 이유로 바뀔 코드였다 — 닮은 것은 우연이었다.

판단 기준: 중복을 발견하면 합치기 전에 물어라 — “이 두 코드는 언제나 함께 바뀔 운명인가, 아니면 지금만 닮았고 앞으로 각자 다른 이유로 바뀔 것인가.” 함께 바뀔 것이면 진짜 중복이니 합치고, 따로 바뀔 것이면 우발적 중복이니 놔둬라. 함정: DRY를 기계적으로 적용해 “닮았으면 무조건 합친다”는 태도는 우발적 중복을 진짜 중복으로 오인한다 — 성급히 합친 코드는 두 유스케이스의 결합을 다시 이어 붙여, 애써 세운 세로 경계를 무너뜨린다. 잠깐의 중복이 잘못된 추상보다 싸다.

결합 분리에는 세 가지 모드가 있다

계층과 유스케이스를 가르기로 했다면, 그다음 질문은 “얼마나 멀리 가를 것인가”다. 마틴은 결합 분리의 강도를 세 단계의 모드로 놓는다.

소스 수준 분리(source level). 소스 모듈 사이의 의존성만 통제하고, 모두 같은 프로세스·같은 주소 공간에서 함께 돈다. 모놀리스다. 모듈들은 한 실행 파일 안에서 단순 함수 호출로 소통하므로 통신 비용이 거의 없다. 결합은 소스 코드의 의존 방향으로만 관리된다.

배포 수준 분리(deployment level). 배포 단위(.jar, DLL, 공유 라이브러리) 사이의 의존성을 통제한다. 여전히 한 프로세스에서 돌 수도 있지만, 각 컴포넌트를 독립적으로 배포할 수 있다. 한 컴포넌트를 고치면 그것만 다시 배포하면 된다.

서비스 수준 분리(service level). 의존하는 컴포넌트를 아예 별도의 서비스로 갈라, 오직 네트워크 패킷으로만 소통하게 한다. 각 서비스는 독립적으로 개발·배포되고 독립적으로 확장된다. 결합은 가장 약하지만, 통신 비용과 운영 복잡도는 가장 크다.

세 모드는 왼쪽에서 오른쪽으로 갈수록 결합이 약해지고 독립성이 커지는 대신, 통신 오버헤드와 개발·배포의 복잡도라는 대가가 커진다. 어느 모드가 옳은가에 정답은 없다 — 다시 트레이드오프다.

판단 기준: 결합 분리 모드는 프로젝트가 감당할 수 있는 가장 싼 단계에서 시작하라 — 대부분은 소스 수준이면 충분하다. 서비스 수준의 독립성이 실제로 필요해질 때 그 유스케이스만 골라 오른쪽으로 밀어라. 함정: 마이크로서비스가 곧 좋은 아키텍처라는 믿음에 이끌려 처음부터 서비스 수준으로 가르면, 아직 필요도 없는 네트워크 지연·분산 트랜잭션·운영 복잡도를 선불로 치른다 — 27장이 경고하듯 서비스 경계는 결합 분리를 저절로 주지 않으면서 비용만 먼저 청구한다.

결합 분리 모드는 시간에 따라 옮겨 간다

마틴이 이 장에서 남기는 가장 실천적인 통찰은, 어느 모드가 옳은지가 프로젝트 생애 동안 바뀐다는 것이다. 초기에는 소스 수준 모놀리스가 개발과 배포에 유리하다. 그러나 시스템이 자라 운영 부하가 커지고 팀이 늘면, 어떤 컴포넌트는 배포 수준으로, 나아가 서비스 수준으로 밀어내는 편이 나아지는 순간이 온다. 반대로, 서비스로 갈라 두었던 것이 통신 비용만 크고 이득이 없다고 판명되면 다시 안으로 당겨 오는 편이 나을 수도 있다.

그래서 좋은 아키텍트는 지금의 모드를 영구히 고정하지 않는다. 대신 결합 분리 모드를 나중에 바꿀 수 있도록 시스템을 배치해 둔다. 소스 수준으로 잘 갈라 둔 컴포넌트는 나중에 배포 단위나 서비스로 밀어내기 쉽고, 그 반대로 당겨 오기도 쉽다. 핵심은 소스 코드의 의존 방향을 처음부터 옳게 관리해 두는 것이다 — 그러면 모드 전환은 배치의 문제이지 재설계의 문제가 아니게 된다.

판단 기준: 지금 어느 모드를 고를지보다, “부하나 조직이 바뀌어 모드를 옮겨야 할 때 얼마나 싸게 옮길 수 있는가”를 물어라. 소스 의존성만 옳게 관리돼 있으면 모드 전환은 값싸다. 함정: 초기의 편의만 보고 계층·유스케이스 경계 없이 뒤엉킨 모놀리스를 짜면, 나중에 서비스로 떼어내려 할 때 경계가 없어 통째로 다시 짜야 한다 — 모드는 미룰 수 있어도, 경계를 긋는 결정은 미룰수록 비싸진다.

  • 좋은 아키텍처는 유스케이스·운영·개발·배포를 동시에 떠받쳐야 하며, 네 요구는 대체로 같은 방향 — 결합 분리 — 을 가리킨다.
  • 결합은 두 방향으로 가른다. 계층(서로 다른 속도로 바뀌는 UI·업무 규칙·DB)과 유스케이스(서로 다른 이유로 바뀌는 세로 조각). 이 둘이 개발 독립성과 배포 독립성을 낳는다.
  • 중복을 만나면 합치기 전에 진짜 중복인지 우발적 중복인지 물어라. 지금만 닮았고 따로 바뀔 코드를 성급히 합치면 결합이 되살아난다.
  • 결합 분리에는 소스·배포·서비스 세 모드가 있고, 오른쪽으로 갈수록 독립성과 비용이 함께 커진다. 옳은 모드는 프로젝트 생애 동안 옮겨 가므로, 소스 의존성을 옳게 관리해 모드 전환을 값싸게 유지하는 것이 아키텍트의 일이다.

15장이 “선택지를 열어 둔다”고 말한 것을, 16장은 “결합을 어느 축으로 어느 강도까지 가를지, 그리고 그 강도를 나중에 어떻게 바꿀지”라는 구체적 결정으로 번역했다. 다음 장은 이 결합 분리를 한 발 더 밀어붙여, 아키텍처란 결국 어디에 선을 긋고 무엇을 미룰 것인가의 문제임을 정면으로 다룬다.

다음장으로 17장