Ch25. 계층과 경계
시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽다.
단순한 시스템이라면 그렇지만, 대다수의 시스템에서 컴포넌트의 개수는 이보다 훨씬 많다.
[움퍼스 사냥 게임]
컴퓨터 게임 예제에 살을 좀 붙여보자.
텍스트를 기반으로 하는 움퍼스라는 게임은 GO EAST와 SHOOT WEST와 같은 매우 단순한 명령어를 사용한다.
플레이어는 명령어를 입력하면 컴퓨터는 플레이어가 보고, 냄새 맡고, 듣고, 경험할 것들로 응답한다.
만약 이 게임을 다양한 언어로 발매할 수 있게 만든다고 가정해보자.
그럼 이러한 구조로 만들면 게임 규칙이 어떤 언어를 사용하던, 어떤 저장소를 사용하던 상관 없이 모두 적용할 수 있다.

[클린 아키텍처?]
분명하게도 이 예제의 맥락이라면 클린 아키텍처 접근법을 적용해서 유스케이스, 경계, 엔티티, 그리고 관련된 데이터 구조를 모두 만드는 일도 쉬운 일이다.
그런데 중요한 아키텍처 경계를 정말 모두 발견할 것일까?
아닐수도 있다. 따라서 다음과 같은 그림을 생각해 볼 수 있다.

이러한 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하면 다이어그램을 단순화할 수 있다.
그림 25.4가 그 결과다.

그림 25.4의 다이어그램은 모든 화살표가 위를 향하도록 맞춰졌다는 점에 주목하자.
그 결과 GameRules는 최상위에 놓인다.
GameRules는 최상위 수준의 정책을 가지는 컴포넌트이므로 이치에 맞는 배치이다.
[흐름 횡단하기]
이 예제처럼 데이터 흐름은 항상 두 가지일까? 절대로 아니다.
만약 움퍼스 사냥 게임을 네트워크상에서 여러 사람이 함께 플레이할 수 있게 만든다고 해보자.
이 경우 25.5에서 보듯이 데이터 흐름이 세 개의 흐름으로 분리하며, 이들 모두 GameRules가 제어한다.

따라서 시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리될 것이다.
[흐름 분리하기]
이쯤되면 모든 흐름이 결국에는 상단의 단일 컴포넌트에서 서로 만난다고 생각할 수 있다.
현실에서는 훨씬 복잡하다.
아래 그림과 같이 훨씬 더 고수준의 정책이 있어 플레이어를 관리한다.

이것이 아키텍처 경계일까?
MovementManagement와 PlayerManagement를 분리하는 API가 필요할까?
대규모의 플레이어가 동시에 플레이할 수 있는 버전의 움퍼스 사냥 게임이 있다고 가정해보자.

다음과 같은 그림이 될 것이고,
이것은 MovementManagement와 PlayerManagement 사이에 완벽한 형태의 아키텍처 경계가 존재한다라고 볼 수 있다.
[결론]
이 모든 것이 의미하는 바는 무엇인가?
아키텍처 경계가 어디에나 존재한다는 사실이다.
또한 우리는 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 알고 있어야 한다.
일반적으로 오버 엔지니어링이 언더 엔지니어링보다 나쁠 때가 훨씬 많다.
우리의 목표는 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 바로 그 변곡점에서 경계를 구현하는 것이다.
Ch26. 메인 컴포넌트
모든 시스템에는 최소한 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성하고, 조정하며, 관리한다.
나는 이 컴포넌트를 메인(Main)이라고 부른다.
[궁극적인 세부사항]
메인 컴포넌트는 궁극적인 세부사항으로, 가장 낮은 수준의 정책이다.
메인은 시스템의 초기 진입점이다.
의존성 주입 프레임워크에서 의존성을 주입하는 일도 바로 메인에서 이뤄줘야 한다.
메인을 지저분한 컴포넌트 중에서도 가장 지저분한 컴포넌트라고 생각하자.
결론적으로 메인은 클린 아키텍처에서 가장 바깥 원에 위치하며, 지저분한 저수준 모듈이다.
메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘긴다.
[결론]
메인을 애플리케이션의 플러그인이라고 생각하자.
메인은 플러그인이므로, 메인 컴포넌트를 애플리케이션의 설정별로 하나씩 두도록 하여 둘 이상의 메인 컴포넌트를 만들수도 있다.
메인을 아키텍처 경계 바깥에 위치하면 설정 관련 문제를 훨씬 쉽게 해결할 수 있다.
Ch27. ‘크고 작은 모든’ 서비스들
서비스 지향 ‘아키텍처’와 마이크로서비스 ‘아키텍처’는 최근에 큰 인기를 끌고 있다.
그 이유는 다음과 같다.
- 서비스를 사용하면 상호 결합이 철저하게 분리된 것처럼 보인다. 사실, 일부만 맞는 말이다.
- 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다. 이 역시도 일부만 맞다.
[서비스 아키텍처?]
먼저 서비스를 사용한다는 것이 본질적으로 아키텍처에 해당하는지에 대해 생각해보자.
이 개념은 명백히 사실이 아니다.
시스템의 아키텍처는 의존성 규칙을 준수하며, 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다.
단순히 애플리케이션의 행위를 분리하는 것은 중요하다고 볼 수는 없다.
[서비스의 이점?]
서비스의 이점을 한번 따져보자.
결합 분리의 오류
시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점이다.
이 말에는 어느 정도 일리는 있지만, 꼭 그런 것만은 아니다.
프로세서 내의 또는 네트워크 상의 공유 자원 때문에 결합될 수도 있고, 서로 공유하는 데이터의 의해 이들 서비스는 강력하게 결합되어 버린다.
개발 및 배포 독립성의 오류
또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점이다.
전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있다.
이러한 개발 및 배포 독립성은 확장 가능한것으로 간주된다.
이러한 믿음에도 어느 정도 일리가 있지만, 극히 일부일 뿐이다.
첫째로, 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 역사적으로 증명되어 왔다.
둘째, 데이터나 행위에도 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야 한다.
[야옹이 문제]
택시 통합 시스템을 한번 살펴보자.
이 시스템은 해당 도시에서 운영되는 많은 택시 업체를 알고 있고, 고객은 승차 요청을 할 수 있다는 점을 상기하자.
고객은 다양한 기준에 따라 택시를 선택할 수 있다고 가정하자.
확장 가능한 시스템을 구축하고 싶었기에, 수많은 마이크로 서비스를 기반으로 구축하기로 결정했다.

각 컴포넌트 별 역할을 살펴보면
- TaxiUI: 고객을 담당, 고객은 모바일 기기를 통해 택시를 호출함
- TaxiFinder: 여러 TaxiSupplier의 현황을 검토하여 사용자에게 적합한 택시 후보들을 선별. 그 후, 해당 사용자에게 할당된 단기 데이터 레코드에 후보 택시들의 정보를 저장
- TaxiSelector: 사용자가 지정한 비용, 시간, 고급 여부 등의 조건을 기초로 후보 택시 중에서 적합한 택시를 선택한다. 그 후, TaxiSelector가 해당 택시를 TaxiDispatcher로 전달.
- TaxiDispathcer: 해당 택시에 배차 지시
이제 이 시스템을 일 년 이상 운영해 왔다고 가정해보자.
그러다 어느날, 마케팅 부서에서 야옹이를 배달하는 서비스를 제공하겠다는 계획을 발표한다.
만약 이를 구현하려고 한다면 이들 서비스 중 어디를 변경해야 할까? 전부다.
다시 말해 이 서비스들은 모두 결합되어 있어서 독립적으로 개발하고, 배포하고, 유지될 수 없다.
이게 바로 횡단 관심사가 지닌 문제다.
그림 27.1과 같은 서비스 다이어그램에서는 새로운 기능이 기능적 행위를 횡단하는 상황에 매우 취약하다.
[객체가 구출하다]
컴포넌트 기반 아키텍처에서는 이 문제를 어떻게 해결할까?
SOLID 설계 원칙을 이용하면, 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 할 수 있다.
그림 27.2의 다이어그램은 이 전략을 보여준다.

컴포넌트들에 있는 추상 기반 클래스를 템플릿 메서드나 전략 패턴 등을 이용해서 오버라이드한다.
이렇게 하면 야옹이 기능은 결합이 분리되며, 독립적으로 개발하여 배포할 수 있게 된다.
[컴포넌트 기반 서비스]
컴포넌트 기반 서비스로도 위와 같이 구성할 수 있다.
자바의 경우, 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각하자.
새로운 기능 추가 혹은 기능 확장은 새로운 jar 파일로 만든다.
이때 새로운 jar 파일을 구성하는 클래스들은 기존 jar 파일에 정의된 추상 클래스들을 확장해서 만든다.
다시 말해 새로운 기능을 추가하는 행위가 개방 폐쇄 원칙을 준수하게 된다.
[횡단 관심사]
지금까지 배운 것은 아키텍처 경계가 서비스 사이에 있지 않다는 사실이다.
오히려 서비스를 관통하며, 서비스를 컴포넌트 단위로 분할한다.
모든 주요 시스템이 직면하는 횡단 관심사를 처리하려면, 그림 27.4처럼 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계되어야 한다.

[결론]
서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아니다.
시스템 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.
Ch28. 테스트 경계
테스트는 시스템의 일부이며, 아키텍처에도 관여한다.
[시스템 컴포넌트인 테스트]
테스트에 관련하여 상당한 혼동이 있다. 테스트는 시스템의 일부인가?
아키텍처 관점에서는 모든 테스트가 동일하다.
테스트는 태생적으로 의존성 규칙을 따른다.
테스트는 세부적이며 구체적인 것으로, 의존성은 항상 테스트 대상이 되는 코드를 향한다.
테스트는 아키텍처 가장 바깥원에 위치하며 시스템의 컴포넌트를 향해, 항상 원의 안쪽으로 의존한다.
또한 테스트는 독립적으로 배포가 가능하다.
그리고 테스트는 시스템 컴포넌트 중 가장 고립되어 있다.
테스트의 역할은 운영이 아니라 개발을 지원하는데 있다.
[테스트를 고려한 설계]
테스트가 지닌 극단적인 고립성이 테스트는 대체로 배포하지 않는다는 사실과 어우러지며,
개발자는 종종 테스트가 시스템의 설계 범위 밖에 있다고 여긴다. 이 관점은 치명적이다.
테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.
만약 테스트가 깨지기 쉬워진다면 시스템을 뻣뻣하게 만들어 개발자로 하여금 변경하기 싫도록 만든다.
이 문제를 해결하려면 테스트를 고려해서 잘 설계해야 한다.
소프트웨어 설계의 첫 번째 규칙은 항상 같다. 변동성이 있는 것에 의존하지 말라.
[결론]
테스트는 시스템 외부에 있지 않다. 오히려 시스템의 일부다.
테스트를 시스템의 일부로 설계하지 않는다면 테스트는 깨지기 쉽고, 유지보수하기 어려워 진다.
'Book Notes' 카테고리의 다른 글
| [개발서적] 도메인 주도 설계 첫걸음 1부(ch1~2) 요약 (1) (1) | 2026.01.10 |
|---|---|
| [개발서적] Clean Architecture 6부(ch30~34) 요약 (4) (0) | 2025.12.15 |
| [개발서적] Clean Architecture 5부(ch20~24) 요약 (2) (1) | 2025.12.10 |
| [개발서적] Clean Architecture 5부(ch15~19) 요약 (1) (0) | 2025.12.10 |
| [개발서적] Clean Architecture 4부(ch12~14) 요약 (0) | 2025.11.26 |