정의
소프트웨어 아키텍처는 한 시스템에 관해 가장 먼저 내려지는 설계 결정들의 묶음이다. 이 결정들은 제대로 내리기가 가장 어렵고, 나중에 바꾸기도 가장 어려우며, 이후의 개발과 배포·유지보수에 가장 멀리까지 영향을 미친다.[1]
아키텍처가 정해지면 구현은 그 안에서만 움직인다. 구현은 규정된 구성 요소로 나뉘어야 하고, 구성 요소들은 규정된 방식으로 서로 상호작용해야 하며, 각 구성 요소는 아키텍처가 지운 책임을 다른 구성 요소에 대해 이행해야 한다.[1] 되돌리는 비용이 큰 것은 코드가 아니라 이 제약이다.
세 가지 선택 축
아키텍처가 고정하는 것은 크게 세 가지로 나누어 볼 수 있다. 시스템을 어떤 단위로 쪼갤 것인가, 쪼갠 단위들이 어떤 방식으로 말을 주고받을 것인가, 그리고 무엇이 실행을 촉발할 것인가다.
앞의 두 축은 아래에서 다루고, 실행 구조 축 — 태스크의 시작을 외부 이벤트가 촉발하는지 시간의 진행이 촉발하는지 — 는 시간 구동 아키텍처에서 다룬다.[2] 구성 단위를 계층으로 쌓는 방식과 컴포넌트로 조립하는 방식은 각각 계층 아키텍처와 컴포넌트 기반 아키텍처의 몫이다.
모듈 분해
모듈식으로 나눠 만들 때 기대하는 이익은 세 가지다. 여러 조직이 서로 거의 소통하지 않고도 각자의 모듈을 맡을 수 있어 개발 기간이 짧아지고, 한 모듈을 크게 뜯어고쳐도 다른 모듈은 건드리지 않을 수 있으며, 시스템을 한 번에 한 모듈씩 공부할 수 있다.[3] 이 이익은 저절로 따라오지 않고 어떤 기준으로 쪼갰는지에 달려 있다.
여기서 모듈은 서브루틴이 아니라 책임의 배정이다. 그래서 모듈 분해는 독립적인 작업이 시작되기 전에 내려져야 하는 설계 결정들 — 둘 이상의 모듈에 영향을 주는 시스템 수준 결정 전부 — 을 포함하며, 모듈 사이의 인터페이스는 작업이 시작되기 전에 모두 확정되어야 한다. 좋은 분해에서는 각 모듈의 정의가 가능한 한 적게 드러내도록 인터페이스를 고르고, 바뀔 가능성이 큰 결정은 모듈 안에 감춘다.
감춘다는 것은 곧 의존을 끊는다는 뜻이다. 인터페이스 추상은 프로그램 안의 모듈들 사이에 생기는 의존을 통제하는 장치이고, 클라이언트가 의존해도 되는 속성을 밝히는 명세가 API다 — 명세에 없는 속성에는 클라이언트가 기대서는 안 된다.[4]
인터페이스
인터페이스는 서로 독립적인 두 개체가 만나 상호작용하는 경계다.[5] 대부분의 상호작용은 제어나 데이터의 이동을 수반하며, 지역·원격 프로시저 호출, 데이터 스트림, 공유 메모리, 메시지 전달처럼 프로그래밍 언어가 이미 제공하는 형태를 띤다.
가장 작은 구성 단위인 함수에서 이 경계를 드러내는 것이 시그니처다. 시그니처는 인터페이스의 구문적 부분으로, 프로시저의 이름과 매개변수를 순서·자료형과 함께 정한다. 다만 시그니처가 맞는다는 것은 컴파일과 링크가 된다는 뜻일 뿐이다. 한쪽이 제공하는 read()와 다른 쪽이 기대하는 read()의 시그니처가 같아도, 제공 쪽이 읽은 데이터를 스트림에서 제거하는데 쓰는 쪽이 같은 데이터를 다시 읽을 수 있다고 가정하면 시스템은 링크만 될 뿐 제대로 돌지 않는다. 한 구성 요소가 하는 일 가운데 다른 구성 요소의 처리에 영향을 주는 것은 무엇이든 상호작용이고, 그것이 곧 인터페이스의 일부다. 상호작용은 무엇이 일어나는가에서 끝나지 않는다 — X가 Y를 호출한다면 Y가 X에게 제어를 돌려주기까지 걸리는 시간도 X의 처리에 영향을 주므로 X에 대한 Y의 인터페이스에 속한다. 그래서 인터페이스 문서는 성능이나 신뢰성 같은 품질 속성 특성까지 담아야 할 몫으로 규정된다.
통신 구조
구성 단위들이 말을 주고받는 방식은 크게 둘로 갈린다. 한쪽은 요청을 보내고 응답을 기다리는 호출이고, 다른 쪽은 관심을 등록해 둔 쪽에 사건을 배포하는 방식이다.
원격 프로시저 호출은 원격 상호작용을 지역 상호작용과 같은 모습으로 보이게 해 분산 프로그래밍을 쉽게 만들었지만, 동기적이라는 성질 때문에 호출자와 수신자를 강하게 묶는다.[6] 호출자는 응답이 올 때까지 진행하지 못하고(시간·동기화 결합), 호출하려면 상대의 참조를 쥐고 있어야 한다(공간 결합).
배포·구독 방식은 그 셋을 모두 풀어 놓는다. 구독자는 관심 있는 사건을 등록하고, 발행자는 사건을 낼 뿐 누가 몇 명이나 받는지 알지 못한다. 양쪽이 같은 시각에 활동하고 있을 필요도 없어서, 구독자가 끊겨 있는 동안 발행된 사건이나 발행자가 끊긴 뒤 도착하는 통지가 성립한다. 발행자는 사건을 내면서 블록되지 않고, 구독자는 다른 일을 하는 중에 콜백으로 통지를 받는다 — 생산과 소비가 양쪽의 주 제어 흐름에서 벌어지지 않으므로 동기적이지 않다.
두 방식의 차이는 곧 제어권의 문제다. 호출은 제어권을 넘기고 돌려받을 때까지 기다리게 하지만, 배포는 제어권을 넘기지 않으므로 송신자는 수신자가 언제 반응하는지 알지 못한다.