heegeon
Last updated
Last updated
프레임워크는 사용하는 도구일 뿐, 아키텍처가 준수해야 할 대상이 아니다.
주택으로 예시를 들면, 아키텍트가 주목하는 관심사는 주택이 거주하기에 적합한 공간임을 확실히 하는 것이며, 주택이 어떤 재료로 지어지는 지를 확인하는 것이 아니다.
그렇기에 좋은 소프트웨어 아키텍처는 프레임워크, 데이터베이스, 웹 서버 그리고 여타 개발 환경 문제가 도구에 대해서는 결정을 미룰 수 있도록 만든다.
좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합을 분리시킨다.
프레임워크가 모든 것을 담당하게 하지 말고 비판적으로 바라보며 유스케이스에 중점을 두어야 한다.
아키텍처가 유스케이스를 최우선으로 한다면, 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대해 단위 테스트를 할 수 있어야 한다.
DB, Web과 같은 지엽적인 것이 필요하여 테스트가 불가능한 상황이 생기면 안 된다.
시스템 아키텍처의 공통점이 있다면 모두 관심사의 분리를 실현하는 것이다.
소프트웨어를 계층으로 분리하여 관심사를 분리한다.
각 아키텍처는 최소한의 업무 규칙을 위한 계층 하나와, 사용자와 시스템 인터페이스를 위한 또 다른 계층 하나를 반드시 포함한다.
위의 사진에서 바깥 원은 메커니즘으로 저수준이며 안쪽 원은 정책으로 고수준이다.
소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.
내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못한다.
따라서 외부 원에 위치한 어떤 것도 내부 원에 영향을 주면 안 된다.
엔티티는 전사적인 핵심 업무 규칙을 캡슐화한다.
운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안 된다.
유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함한다.
또한, 유스케이스 계층의 소프트웨어는 시스템의 모든 유스케이스를 캡슐화하고 구현한다.
유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정하며, 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다.
유스케이스 계층도 다른 저수준의 변경에 영향을 받으면 안되며 유스케이스가 변경되어도 엔티티에 영향을 주어서는 안 된다.
하지만, 애플리케이션이 변경되면 유스케이스는 영향을 받게 된다.
어댑터는 데이터를 유스케이스와 엔티티에게 가장 편리한 형식에서 데이터베이스나 웹 같은 외부 에이전시에게 가장 편리한 형식으로 변환한다.
프레임워크와 드라이버 계층은 모든 세부사항이 위치하는 곳이다.
이들을 외부에 위치시켜서 변경의 영향을 최소화한다.
경계를 횡단할 때는 간단한 데이터 구조로 이루어져야 하며 엔티티 객체나 데이터베이스의 행을 전달하면 안 된다.
데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태로 이동해야 한다.
험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었다.
행위들을 두 개의 모듈 또는 클래스로 나눈다. 이들 모듈 중 하나는 험블로써, 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 모듈에는 테스트 하기 쉬운 행위를 모두 옮긴다.
뷰는 험블 객체이고 테스트하기 어렵다. 데이터를 GUI로 이동시키지만 데이터를 직접 처리하지는 않는다.
프레젠터는 테스트하기 쉬운 객체다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것이다.
이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만든다.
유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이가 위치한다.
이 게이트웨이는 다형적 인터페이스로, 애플리케이션이 데이터베이스에 수행하는 생성, 조회, 갱신, 삭제 작업과 관련된 모든 메서드를 포함한다.
유스케이스 계층은 SQL을 허용하지 않기 때문에 필요한 메서드를 제공하는 게이트웨이 인터페이스를 호출한다.
인터랙터는 애플리케이션에 특화된 업무 규칙을 캡슐화하기 때문에 험블 객체가 아니며 스텁이나 테스트 더블로 교체가 가능하여 테스트하기 쉽다.
ORM(데이터 매퍼)는 데이터베이스 계층에 속하며 게이트웨이 인터페이스와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성한다.
부분적 경계를 생성하는 방법 하나는 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후, 단일 컴포넌트에 그대로 모아만 두는 것이다.
이 방법의 장점은 다수의 컴포넌트를 관리하는 작업이 필요하지 않으며 추적을 위한 버전 번호도 없으며 배포 관리 부담도 없다.
완벽한 형태의 아키텍처 경계는 양방향으로 격리된 상태를 유지해야 하므로 쌍방향 Boundary 인터페이스를 사용한다.
이렇게 양방향으로 격리된 상태를 유지하려면 초기 설정할 때나 지속적으로 유지할 때도 비용이 많이 든다.
Service Boundary 인터페이스는 클라이언트가 사용하며 ServiceImpl 클래스가 구현한다.
Client를 ServiceImpl로부터 격리시키는데 필요한 의존성 역전이 마련되었기 떄문에 미래에 필요할 아키텍처 경계를 위한 준비가 되었다.
하지만 이러한 분리는 매우 빠르게 붕괴될 수 있다.
쌍방향 인터페이스가 없고 아키텍트가 항시 확인하지 않는다면 점선과 같은 통로가 생길 수 있다.
의존성 역전을 희생하고 경계는 Facade 클래스로만 간단하게 정의된다.
Facade 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달한다.
아키텍트에서 모든 흐름이 상단의 단일 컴포넌트에서 만난다고 생각할 수 있지만 살펴보면 더 높은 수준에는 또 다른 정책 집합이 존재할 수 있다.
일반적으로 업무 규칙이 최상단에 위치하여 컨트롤하게 되는데 여러 기능이 추가되다보면 타 업무규칙과 연결되고 업무규칙 사이의 아키텍처 경계를 발견할 수 있다.
아키텍처 경계는 어디에나 존재한다.
아키텍트는 아키텍처 경계가 언제 필요한지를 신중하게 파악해야 하고 그러한 아키텍처 경계를 무시하면 다시 추가하는 비용이 크다는 사실을 인지해야 한다.
YANGNI의 철학으로서 경계를 무시하면 안되지만 추상화를 미리 예측해서는 안 된다.
오버 엔지니어링이 언더 엔지니어링보다 나쁠 때가 훨씬 많다.
프로젝트 초반에는 구현할 경계가 무엇인지 무시할 경계인지 쉽게 결정할 수 없다.
대신 지켜봐야 하며 시스템이 발전함에 따라 주의를 기울여 경계가 필요해지는 순간을 관찰해야 한다.
모든 시스템에는 최소한 하나의 메인 컴포넌트가 존재하고 이 컴포넌트는 나머지 컴포넌트를 생성하고 조정하며 관리한다.
메인 컴포넌트는 궁극적인 세부사항으로 가장 낮은 수준의 정책이다.
메인 컴포넌트는 모든 팩토리와 전략, 그리고 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘기는 역할을 한다.
의존성 주입은 이 메인 컴포넌트에서 이루어져야 한다.
추후 완벽한 형태의 경계로 확장할 수 있는 공간을 확보하고자 할 때 활용할 수 있는 패턴으로 "Strategy 패턴"이 있다.
더 단순한 경계로는 퍼사드 패턴이 있다.