본문 바로가기
Engineering/SW Architecture

클린 아키텍쳐 (3) - 아키텍처 [독립성 / 선긋기 / 경계 해부학 / 정책과 수준/ 업무 규칙]

by 쿨쥰 2023. 3. 19.

이전 글 : 컴포넌트 설계 원칙

2023.03.16 - [Engineering/SW Architecture] - 클린 아키텍쳐 (2) - 컴포넌트 원칙

 

클린 아키텍쳐 (2) - 컴포넌트 원칙

이전 글 : SW개발 패러다임과 기본 설계 원칙 2023.03.12 - [Engineering/SW Architecture] - 클린 아키텍쳐 (1) - 오버뷰, 패러다임, 설계원칙 SOLID 클린 아키텍쳐 (1) - 오버뷰, 패러다임, 설계원칙 SOLID 로버트

skidrow6122.tistory.com

 


 

< 아키텍처란? >

SW 아키텍처는 무엇보다도 프로그래머다.

코드에서 탈피하여 고수준에 문제에서만 집중하면 안되고, 코드와 동떨어져서는 안된다.

다른 팀원들 만큼 코드를 많이 작성 하지 않을 수는 있으나 지속적으로 프로그래밍 작업에 참여함과 동시에 나머지 팀원들의 생산성을 극대화 할 수 있는 설계를 하도록 방향을 이끌어 준다.

 

SW 시스템의 아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태이다.

그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴포넌트를 배치하는 방법, 컴포넌트가 서로 의사소통 하는 방식에 따라 정해진다.

그리고 그 형태는 아키텍처 안에 담긴 SW시스템이 쉽게 개발, 배포, 운영, 유지보수 되도록 만들어진다.

따라서 아키텍처의 주된 목적은 시스템의 생명주기를 지원하는 것이며, 시스템의 수명과 관련된 비용은 최소화하고, 프로그래머의 생산성을 최대화 하는데 있다.

개발

시스템 아키텍쳐는 개발팀이 그 팀의 조직적 상황에 맞게 시스템을 쉽게 개발할 수 있도록 뒷받침 해야한다.

소규모 팀이라면 잘 정의된 컴포넌트나 인터페이스가 없는 모노리틱 시스템으로 출발할 수도 있고, 모듈화된 팀조직을 복수개 보유한 조직이라면 잘 설계된 컴포넌트 단위 분리된 아키텍쳐로 출발 할 수 있다.

배포

SW시스템이 사용되려면 결국 배포될 수 있어야 하며, 당연히 배포 비용이 높을 수록 시스템의 유용성은 떨어진다.

보통 초기 개발 단계에서는 배포전략을 거의 고려하지 않지만, 계속 이를 외면한다면 개발하기는 쉽지만 배포하기는 상당히 어려운 아키텍처가 만들어진다. 이는 SW의 주요한 임무와 목표에 실패하는 것이다.

배포를 쉽게 하는데에 역시 조직의 상황이나 시스템의 요구사항에 기반하여 마이크로 서비스가 될 수 도 있고, 적절한 서비스 컴포넌트 단위로 융합하여 좀더 통합된 도구를 사용하여 상호 연결 한 배포 체계를 잡을 수 도 있다.

운영

사실 아키텍처가 시스템 운영에 미치는 영향은 개발/배포/유지보수에 미치는 영향보다는 적은 편이다. 왜냐하면 운영에서 겪는 대다수의 어려움은 SW아키텍처에 큰 변화 없이 단순히 하드웨어를 더 투입해서 해결할 수 있기 때문인다.

우리는 하드웨어는 값싸고 인력은 비싸진 시대에 살고 있다. 그렇다고 시스템을 쉽게 운영하게 해주는 아키텍처가 바람직하지 않은 것은 아니다. 다만 비용 공식 관점에서 운영보다는 개발/배포/유지보수 쪽으로 조금 더 신경을 써야 한다는 뜻이다.

유지보수

유지보수는 모든 측면에서 봤을때 비용이 가장 많이 들어가는 부분이다. 유지보수의 가장 큰 비용은 탐사비용인데, 이는 기존 SW에 새로운 기능을 추가하거나 결함을 수정할 때, 기존 코드를 분석하여 어디를 고치는 것이 최선인지, 어떤 전략을 쓰는것이 최적일지를 결정하는데 드는 비용이며 여기에 2차 결함 및 그에 대한 위험부담 비용또한 포함된다. 신중하게 아키텍처를 만들면 이 비용을 크게 줄일 수 있다.

시스템을 컴포넌트로 분리하고, 안정된 인터페이스를 두어 서로 격리한다면, 미래에 추가될 기능에 대해 미리 길을 터둘 수 있고 unkwon 장애 발생 위험성을 줄여준다.

정책 VS 세부사항

모든 SW시스템은 두가지 구성요소로 분해되는데 바로 정책과 세부사항이다.

정책 요소는 모든 업무 규칙과 업무 절차를 구체하하며 이는 곧 시스템의 진정한 가치가 살아 있는 곳이다.

세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소이지만 정책이 가진 행위에는 영향을 미치지 않는다. (ex, IO장치, DB, 웹, 서버, 프레임워크, 통신 프로토콜 등)

아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축하는데 있다.

따라서 구축 초반부터는 정책에 집중해야지 세부사항에 목메어 있을 필요가 없다. DB를 선택하고, 웹서버를 무엇을 쓸지, REST, 의존성 등을 초기부터 고민해서 선택사항을 미리 닫아버려 우리 시스템의 가능성 역시 미리 닫아버리지 말자. “좋은 아키텍트는 결정되지 않은 사항의 수를 최대화 한다”

이는 프로그램의 장치 독립성으로 곧 이어진다. 진정한 OCP 인 것이다.

좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리한다.

이를 통해 정책은 세부사항에 관한 어떠한 지식도 갖지 못하게 되며, 어떤경우에도 세부사항에 의존하지 않게 된다.

좋은 아키텍트는 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수있는 방향으로 정책을 설계 한다.

 

 

< 독립성 >

좋은 아키텍처는 유즈케이스, 운영, 개발, 배포를 지원해야 한다.

유즈케이스

시스템의 의도를 지원해야 한다는 뜻이다. 실제로 아키텍트의 최우선 관심사는 유스케이스이다. 아키텍처는 시스템의 행위에 그다지 큰 영향을 주지는 않지만, 적어도 좋은 아키텍처라면 행위를 지원하기 위해 유스케이스를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것이다.

좋은 아키텍처의 장바구니 어플리케이션이라면 누가봐도 장바구니 처럼 보일 것이며 해당 시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러날 것이다.

이는 곧 유스케이스가 클래스, 함수, 모듈로서 아키텍처 내에서 핵심적인 자리를 차지 한다는 것을 의미하고 자신의 기능을 분명하게 설명하는 이름을 가질 것이다.

운영

운영지원 관점에서 본다면, 조금 더 실질적인 역할을 맡을 수 있다. 시스템이 초당 십만 트래픽을 처리해야 한다면, 아키텍처는 이 요구와 관련된 유스케이스에 걸맞는 처리량과 응답시간을 보장하기 위해 이를 허용할 수 있는 형태로 아키텍처를 구조화 해야한다.

즉, 어떤 시스템에서는 병렬 프로세스, 멀티 스레드, 모노리틱 프로그램 등등의 전략을 선택할 수 있다.

이러한 결정은 열어두어야 하는 선택사항 중의 하나이다.

따라서 아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한해두지 않는다면, 시간이 지나 운영에 필요한 요건이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 훨씬 쉬워질 것이다.

개발

개발환경을 지원하는데 핵심적인 역할을 해야한다. (콘웨이 법칙 : 시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.)

많은 팀으로 구성되어 관심사가 다양한 조직에서 어떤 시스템을 개발해야한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 확보하여 개발하는 동안 팀들이 서로를 방해하지 않도록 지원해야한다.

즉, 격리수준을 확보하여 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할 할 수 있어야 한다.

배포

배포 용이성을 결정하는데 중요한 역할을 해야한다. 목표는 당연히 즉각적인 배포이며, 좋은 아키텍처는 수십개의 작은 설정 스크립트를 일일이 건드리는 것을 회피해야하며, 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 한다.

이 또한 시스템을 컴포넌트 단위로 적절하게 분리하고 격리시켜야 함을 의미한다.

선택사항 열여두기

핵심은 컴포넌트 구조와 관련된 관심사들 사이에서 균형을 맞추고 각 관심사를 모두 만족시키는 것이지만, 현실에서는 이러한 균형을 잡기가 매우 어렵다.

대부분의 경우 우리는 모든 유스케이스를 알 수 없으며, 운영하는데 따르는 제약사항, 팀 구조, 배포 요건도 알지 못하기 때문이다. 심지어 이러한 사항들은 시스템의 생명주기를 거치며 시시각각 변한다.

따라서 절대적으로 변하지 않는 설계원칙을 지키며 여러 선택사항들을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 한다.

계층 결합 분리

이미 OCP와 SRP 원칙을 돌아보며, 서로 같은 이유로 변경되는 것들을 묶고, 다른 이유로 변경되는 것들을 분리하는 방법을 알고있다.

시스템의 유스케이스는 너무도 다양하지만 서로 다른 이유로 변경되는 것들을 추려내기 위해 몇가지 분명한 사실이 있다.

예를 들어 사용자 인터페이스가 변경되는 이유는 업무 규칙과 아무런 관련이 없다. 데이터베이스, 쿼리, 스키마 등은 기술적인 세부사항이며 역시 업무규칙이나 UI와 관련이 없다.

결론적으로 아키텍트는 이들을 시스템의 나머지 부분으로 부터 분리하여 독립적으로 변경할 수 있도록 해야 한다.

유즈케이스 결합 분리

서로 다른 이유로 변경되는 것에는 유즈케이스 그 자체 또한 해당된다. 예를 들어 주문 입력 시스템에서 주문을 추가하는 유즈케이스는 주문을 삭제하는 유즈케이스와는 틀림 없이 다른 속도와 다른 이유로 변경되며, 이러한 유즈케이스는 시스템을 분할하는 매우 자연스러운 방법이 된다.

이와 동시에 유즈케이스는 시스템의 수평적인 계층을 가로지르도록 자른 수직적으로 분리할 수 있는 좁은 조각이기도 하다. 유즈케이스별로 UI의 일부, 업무규칙 일부, DB기능의 일부를 사용하기 때문이다. 이러한 유스케이스를 서로 다른 관점 (aspect) 기준으로 분리된다면 이는 광의의 의미에서 서비스화된 컴포넌트 라고도 할 수 있다.

실제 서비스라는 용어는 모호한 면이 많지만 이러한 서비스에 기반한 아키텍쳐가 그 유명한 SOA 다.

개발 독립성 / 배포 독립성 / 중복

컴포넌트가 완전 분리되면 팀 사이 간섭은 줄어들어 개발 독립성을 보장할 수 있고, 이는 배포 측면에서도 고도의 유연성을 확보할 수 있다.

독립성을 고려하다보면 당연히 중복이 발생할 수 있는데, SA는 중복을 최소화 해야 한다.

단, 중복에는 진짜 중복과 우발적 중복이 있는데, 진짜 중복은 당연히 명예를 걸고 제거해 나가야 하는 부분이며, 우발적 중복은 쉽사리 통합시키는 결정을 해서는 안된다. 우발적 중복은 이미 유즈케이스를 수직적으로 분리할때 의도와 관점 수준에서 이미 다르게 확장해나갈 것임을 알고 분리한 독립적 코드이기 때문이다.

좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로 서비스 수준까지 성장할 수 있도록 만들어져야한다. 또한, 이러한 변경으로 부터 소스코드를 대부분 보호할 수 있어야하며, 결합 분리 모드를 선택사항으로 남겨두어서 배포 규모에 따라 가장 적합한 모드를 선택해 사용할 수 있게 해야한다. 물론 이를 간단한 설정 하나 바꾸듯 쉽게 switch 할 수는 없겠지만, 적어도 이러한 변경을 예측하여 큰 무리 없이 대응할 수 있는 유연한 구조를 미리 확보해 두는 것이 중요하다.

 

 

< 경계 : 선 긋기 >

SW 아키텍처는 선을 긋는 기술이다. 이 경계는 SW요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소를 알지 못하도록 막는다. 어떤 선은 프로젝트 아주 초기에 그어지며, 어떤 선은 매우 나중에 그어진다. 보통 초기에 그어지는 선들은 가능한 한 오랫동안 결정을 연기 시키기 위해(바꿔 말하면 이른 결정을 하지 않도록), 그래서 이들의 결정이 핵심적인 업무로직을 오염시키지 못하게 만들려는 목적으로 쓰인다.

이른 결정이란, 시스템의 업무 요구사항인 유즈케이스와 아무런 연관이 없는 프레임워크, DB, 웹서버, 유틸리티 와도 같은 결정이 여기에 포함된다. 좋은 아키텍쳐는 이러한 결정이 부수적이라서 연기할 수 있어야 하며 이러한 결정에 의존하지 않는다.

저자가 wikiPage를 만드는 과정을 떠올려 보자. 이들은 개발 초기부터 업무규칙과 데이터베이스간 경계 선을 그었다. 개발 기간동안 데이터 접근 메서드를 wiKiPage 라는 이름의 그저 인터페이스 MockWikiPage 클래스에 두었고 상세 메서드는 그저 stub 으로 유지했다. 그러다가 실제 데이터로 연동 해야 하는 시점이 되었을때 InMemoryWikiPage 라는 식으로 실제 메서드를 파생 클래스로 구현했다. 결과적으로 이는 개발 기간동안 스키마 문제, 쿼리문제, DB서버문제 등에 대한 고민으로 부터 자유롭게 만들어 주었으며, 이러한 결정을 뒤로 지연시킨 효과로서 추후 MySQL 으로의 전환 검토 역시 굉장히 agile 하게 이루어질 수 있었다.

언제? 어떻게 선을 그을까?

관련이 있는 것과 없는 것 사이에 선을 긋는다.

GUI는 업무규칙과 관련없고, DB역시 GUI와 관련없고, DB는 업무 규칙과 관련없으므로 이들 사이에는 선이 있어야 한다.

사실 DB는 굉장히 업무규칙과 가깝거나, 업무 규칙이 구체화 된것이 바로 DB라고 생각하는 사람들이 많지만, 이는 잘못된 생각이다. DB는 업무 규칙이 간접적으로 사용할 수 있는 도구에 불과하며 업무 규칙은 스키마, 쿼리, 나머지 세부사항들에 대해 어던것도 알아서는 안된다. 즉, 업무 규칙이 알아야 할 것은 데이터를 가져오고 저장할 때 사용할 수 있는 함수 집합이 있다는 사실이 전부다.

그렇기 때문에 보통 DB는 인터페이스 뒤로 숨기곤 한다.

클래스간 경계

BusinessRules 는 Database Interface 를 사용해서 데이터를 로드하고 저장한다.

Database Access는 Database에 의존하여 상세를 구현하며 Database를 실제로 조작하는 역할과 책임을 맡는다.

실제 Database에 접근하는 Database Access 출발하는 두 화살표를 가진다.

이 화살표들은 클래스로부터 바깥을 향하며 이는 곧 이 다이어그램에서 DatabaseAccess 라는 클래스가 있다는 사실을 아는 클래스가 없다는 것을 의미한다.

따라서 경계선은 바로 상속 관계를 횡단하는 위치에 그어지는 것이다.

이 개념을 컴포넌트로 확장해 보면 아래와 같은 그림이 나온다.

컴포넌트간 경계

선의 방향을 참조해 보았을때 BusinessRules 컴포넌트에게 있어 Database 컴포넌트는 문제가 되지 않지만, Databse 컴포넌트는 BusinessRules 없이는 존재 할 수 없다.

Database 컴포넌트는 BusinessRules 가 만들어 낸 호출을 DB 쿼리 언어로 변환하는 코드를 담고 있다는 뜻이다.

이는 곧 BusinessRules 컴포넌트는 어떤 종류의 DB를 사용할 수 있다는 뜻이고 Database 컴포넌트는 다양한 구현체로도 구현될 수 있다는 뜻이며 이는 곧 DBMS 제품의 호환 확장성을 의미한다.

결국 이 경계선을 통해 우리는 업무 규칙과 관계없는 DB에 대한 결정을 지연 시키고 core에 더 노력을 쏟을 수 있는 것이다.

이는 DB 뿐만아니라 GUI와 업무규칙 간의 관계에도 동일하게 적용 될 수 있다.

플러그인 아키텍처

DB와 GUI에 대해 내린 두가지 결정을 하나로 합쳐서 보면 컴포넌트 추가와 관련한 일종의 패턴이 만들어진다.

이 패턴은 시스템에서 서드파티 플러그인을 사용할 수 있게 한 플러그인 아키텍쳐 패턴과 동일하다.

이 설계에서 사용자 인터페이스와 DB는 플러그인 형태로 고려되었기에 수많은 종류의 사용자 인터페이스(웹/클라이언트-서버/SOA/콘솔 등등), DB는 다양한 RDBMS, NoSQL 기반 등등을 매우 다양하게 플러그인 형태로 연결할 수 있다.

재미있는 예로 Resharper 와 Visual studio 를 생각해보자. Jetbrains 는 러시아에 있고 MS는 미국에 있지만, Resharper 가 visual studio 에 의존하고 있으므로 MS는 마음만 먹으면 언제든지 Resharper 팀을 무력화 할 수 있지만, 반대의 변경에 대해 MS는 아무런 영향이 없다. 이 관계는 상당히 비대칭 적이나 이것이 바로 우리가 시스템에서 갖추고자 하는 그런 관계에 해당한다.

누군가 웹 페이지나, DB 스키마를 변경하더라도 업무 규칙은 깨지지 않는 견고한 설계, 시스템을 플러그인 아키텍쳐로 배치 함으로써 변경이 전파 될 수 없는 방화벽을 생성할 수 있다. GUI가 업무 규칙에 플러그인 형태로 연결되면 GUI에서 발생한 변경은 절대로 업무 규칙에 영향을 미칠 수 없기 때문이다.

따라서 경계란 변경의 축 (axis of change)이 있는 지점에 그어진다. 경계의 한쪽에 위치한 컴포넌트는 경계 반대편의 컴포넌트와는 다른 속도로, 그리고 다른 이유로 변경된다.

이 역시도 SRP 의 사상이 깃들어있다. 결과적으로 SRP는 어디에 경계를 그어야 할지를 알려준다.

아키텍처 선긋기에 대한 결론

SW 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야하며,

이 분할된 컴포넌트 중 일부 컴포넌트를 핵심 업무 규칙 컴포넌트로 분류 해야한다.

나머지 컴포넌트는 플러그인으로, 핵심 업무와는 직접적인 관련이 없지만 필수 기능을 포함하게 한다.

그런 다음 컴포넌트 사이의 화살표가 핵심 업무를 향하도록 컴포넌트 소스를 배치한다.

이는 곧 DIP(의존성 역전 원칙) 과 SDP(안정된 추상화 원칙)를 응용한 것임을 눈치 챌 수 있어야한다.

의존성 화살표는 항상 저수준 세부사항에서 고수준의 추상화를 향하도록 배치 되기 때문이다.

 

 

 

< 경계 해부학 >

시스템 아키텍처는 일련의 컴포넌트와 그 컴포넌트들을 분리하는 경계에 의해 정의된다. 이러한 경계는 다양한 형태로 나타난다.

두려운 단일체

가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출이다.

이 경우 런타임 의존성과 컴파일 타임 의존성은 모두 같은 방향, 즉 저수준 컴포넌트에서 고수준 컴포넌트로 향한다.

런타임 의존성과 같은 방향으로 흐르는 횡단

고수준 클라이언트가 저수준 서비스를 호출해야 한다면 동적 다형성을 사용하여 제어흐름과는 반대 방향으로 의존성을 역전시킬 수 있다.

이렇게 하면 런타임 의존성은 컴파일타임 의존성과는 반대가 된다. 제어 흐름은 이전과 마찬가지로 왼쪽에서 오른쪽으로 경계를 횡단하지만, 고수준 client는 service interface를 통해 저수준인 serviceImpl 의 함수 f()를 호출한다.

여기서 주목할 점은 경계를 횡단할 때 의존성은 모두 오른쪽에서 왼쪽으로, 즉 고수준 컴포넌트를 향한다는 점이다.

이 경우 데이터 구조의 정의 역시 호출하는 쪽에 위치한다는 점도 다르다.

런타임 의존성과 반대 방향으로 흐르는 횡단 : 의존성 역전

정적링크 된 모노리틱 구조의 실행 파일이라도 이처럼 규칙적인 방식으로 구조를분리하면 프로젝트를 개발, 테스트, 배포하는데 작업에 큰 도움이 된다.

배포형 컴포넌트

아키텍처의 경계는 물리적으로 드러날 수도 있는데, 그 중 가장 단순한 형태는 동적 링크 라이브러리이다. 즉, dll, jar, gem 파일 등이 그 예이다. 컴포넌트는 바이너리와 같이 배포가능한 형태로 전달 되므로 컴포넌트를 이 형태로 배포하면 따로 컴파일하지 않고도 곧바로 사용할 수 있다.

이는 배포 수준 결합 분리 모드에 해당한다. 배포 작업은 단순히 이들 배포 가능한 단위를 좀 더 편리한 형태로 묶는 일에 지나지 않으며, 배포 과정만 차이가 날 뿐 배포 수준의 컴포넌트는 단일체와 동일하다.

로컬 프로세스

훨씬 강한 물리적 형태를 띄는 아키텍처 경계에 속한다. 로컬 프로세스를 일종의 최상위 컴포넌트라고 생각하자.

즉, 로컬 프로세스는 컴포넌트간 의존성을 동적 다형성을 통해 관리하는 저수준 컴포넌트로 구성된다.

로컬 프로세스 간 분리 전략은 단일체나 바이너리 컴포넌트의 경우와 동일하다.

소스 코드 의존성의 화살표는 단일체나 바이너리 컴포넌트와 동일한 방향으로 경계를 횡단한다.

즉, 항상 고수준 컴포넌트를 향한다. 저수준 프로세스가 고수준 프로세스의 플러그인이 되도록 만드는 것이 아키텍처 관점의 목표라는 사실을 기억하자.

로컬 프로세스 경계를 지나는 통신에는 운영체제 호출, 데이터 마샬링 및 언마샬링, 프로세스 간 문맥 교환 등이 있으며, 이들은 제법 비싼 작업에 속한다. 따라서 통신이 너무 빈번하게 이뤄지지 않도록 신중하게 제한해야 한다.

서비스

물리적인 형태를 띠는 가장 강력한 경계는 바로 서비스다. 서비스는 자신의 물리적 위치에 구애받지 않으며, 서로 통신하는 두 서비스는 물리적으로 동일한 프로세스나 멀티코어에서 동작할 수 도 있고 아닐수도 있다. 서비스들은 모든 통신이 네트워크를 통해 이뤄진다고 가정한다.

서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느리다. 이 수준의 통신에서는 지연에 따른 문제를 고수준에서 처리할 수 있어야 한다.

이를 제외하고는 로컬 프로세스에 적용한 규칙이 서비스에도 그대로 적용된다.

저수준 서비스는 반드시 고수준 서비스에 ‘플러그인’되어야 한다. 고수준 서비스의 소스코드에는 저수준 서비스를 특정 짓는 어떤 물리적인 정보(ex, URI)도 절대 포함해서는 안 된다.

단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용한다. 서비스 경계를 활용하는 시스템이라면 로컬 프로세스 경계도 일부 포함하고 있을 수 있다.

실제로 서비스는 상호작용하는 일련의 로컬 프로세스 퍼사드에 불과할 때가 많다.

또한 개별 서비스 또는 로컬 프로세스는 거의 언제나 소스코드 컴포넌트로 구성된 단일체이거나, 혹은 동적으로 링크된 배포형 컴포넌트의 집합이다. 즉, 대체로 한 시스템 안에서도 통신이 빈번한 로컬 경계와 지연을 중요하게 고려해야 하는 경계가 혼합되어 있음을 의미한다.

 

 

< 정책과 수준 > 

프로그램이란 각 입력을 출력으로 변환하는 정책을 상세히 기술한 설명서이며, 실제로 핵심부는 이게 전부다. 하나의 정책은 이 정책을 서술하는 여러개의 정책들로 쪼개질 수 있다.

SW 아키텍처링에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다. 이전 정리에서 주구장창 나온 이야기이긴 하지만 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 한다. 서로 다른 이유로, 혹은 다른 시점에 변경되는 정책은 다른 수준에 위치하며, 반드시 다른 컴포넌트로 분리해야 한다.

흔히 아키텍처 개발은 재편성된 컴포넌트들을 비순환 방향그래프로 구성하는 기술을 포함하며, 정점 node는 동일한 수준의 정책을 포함하는 컴포넌트, 간선 edge는 컴포넌트 사이의 의존성을 나타낸다.

역시 좋은 아키텍처라면 각 컴포넌트를 연결 할때 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 방향을 설정 해야한다.

수준

수준 level 을 엄밀하게 정의하면 ‘입력과 출력까지의 거리’ 이다. 시스템의 입력과 출력 모두로 부터 멀리 위치할수록 정책의 수준은 높아진다. 입력과 출력을 다루는 정책이라면 시스템에서 최하위 수준에 위치한다.

번역 컴포넌트는 이 시스템에서 최고수준의 컴포넌트이며, 입력과 출력에서 가장 멀리 떨어져 있다.

주목할 점은 데이터 흐름과 소스코드 의존성이 항상 같은 방향을 가리키지 않는다는 사실이다. 소스 코드 의존성은 그 수준에 따라 결합 되어야 하며, 데이터 흐름을 기준으로 결합되어서는 안된다.

Encrypt 클래스와 입출력을 관장하는 인터페이스 2개는 본 아키텍처의 경계이다. 이 경계를 횡단하는 의존성은 모두 경계 안쪽으로 향하고 있고 이 것은 바로 이 경계로 묶인 영역이 이 시스템에서 최고 수준의 구성요소라는 점을 뜻한다.

consoleReader 와 ConsoleWriter 클래스는 입력과 출력에 가깝기 때문에 저수준이다.

이 구조에서 고수준의 암호화 정책을 저수준의 입/출력 정책으로부터 분리시킨 방식 덕분에 이 암호화 정책은 더 넓은 맥락에서 보호받으며 자유롭게 사용할 수 있게 되었다.

정책을 컴포넌트로 묶는 기준은 정책이 변경되는 방식에 달려있다는 사실을 상기하자.

단일 책임 원칙 SRP 와 공통 폐쇄 원칙 CCP에 따르면 동일한 이유로 동일한 시점에 변경되는 정책은 함께 묶여야한다.

이처럼 모든 소스코드의 의존성의 방향이 고수준 정책을 향할 수 있도록 정책을 분리했다면, 사소한 이유의 변경의 영향도를 줄일 수 있다.

시스템의 최저 수준에서 중요하지는 않지만 긴급한 변경이 발생하더라도, 보다 높은 위치의 중요한 수준에 영향은 거의없게 되는 것이다.

이 논의는 결국 컴포넌트 레벨까지 확장하여, 저수준 컴포넌트가 고수준 컴포넌트에 플러그인 되어야 한다는 플러그인 아키텍쳐 관점으로도 해석 될 수 있다.

이 정책과 수준에 대한 논의는 결국 지금까지 소개된 SOLID 원칙들과 컴포넌트 설계 원칙들이 저변에 깔려있다 보면 된다.

 

 

< 업무 규칙 >

어플리케이션을 주요한 업무 규칙과 플러그인으로 구분하려면 업무 규칙이 실제로 무엇인지를 잘 이해해야만한다.

업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차이다.

사업 자체에 핵심적이며, 규칙을 자동화 하는 시스템이 없더라도 업무 규칙은 그대로 존재하여 사업적으로 조직의 존속을 가능하게 하는 것들을 핵심 업무 규칙 이라고 부른다.

그리고 이런 핵심 업무 규칙은 대출 잔액, 이자율, 지급 일정 등의 데이터를 요구하는데 이를 두고 핵심 업무 데이터라고 부른다.

핵심 업무 규칙과 핵심 데이터는 본질적으로 결합 되어 있기 때문에 객체로 만들 수 있는 좋은 후보가 되고 이러한 유형의 객체를 엔터티 entity 라고 한다.

엔티티

엔티티는 컴퓨터 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화 한다. 따라서 엔티티 객체는 핵심 업무 데이터를 직접 포함하거나 핵심 업무데이터에 매우 쉽게 접근할 수 있다. 엔티티의 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구현된다.

UML 클래스로 표현한 Loan 엔티티

Loan 엔티티는 세가지 핵심 업무 데이터를 포함하며, 데이터와 관련된 세 가지 핵심 업무 규칙을 인터페이스로 제공한다.

이 클래스는 업무 에서 핵심적인 개념을 구현하는 소프트웨어가 한데 모였으므로, 이 클래스는 업무의 대표자로서 독립적으로 존재한다.

즉, DB, 사용자 인터페이스, 서드파티 프레임워크에 대한 고려사항들로 오염되어 서는 절대 안된다.

유스케이스

모든 업무 규칙이 엔티티처럼 순수한 것은 아니다.

자동화된 시스템이 동작 하는 방법을 정의하고 제약 함으로써 수익을 얻거나 비용을 줄이는 업무 규칙도 있으며 이러한 규칙은 자동화 된 시스템의 요소로 존재해야만 의미가 있으므로 수동 환경에서는 사용될 수 없다.

이것이 바로 유스케이스이며, 유스 케이스는 사용자가 제공해야하는 입력, 사용자에게 보여줄 출력, 그리고 해당 출력을 생성하기 위한 처리 단계를 기술한다.

엔터티 내의 핵심 업무 규칙과는 반대로, 유스케이스는 어플리케이션에 특화 된 업무 규칙을 설명한다.

유스케이스는 엔터티 내부의 핵심 업무 규칙을 어떻게 그리고 어떻게 호출해야 할지를 명시하는 규칙이 담긴다.

또한 유스케이스는 사용자 인터페이스를 기술하지 않으므로 유스케이스만 봐서는 이 것이 웹을 통해 나가는지 콘솔인지 순서 서비스인지 구분하기 힘들다.

즉, 유스케이스는 사용자에게 어떻게 보이는지에 대한 설명 대신, 어플리케이션에 특화된 규칙을 설명하며 이를 통해 사용자와 엔터티 사이의 상호작용을 규정한다.

또한, 유스케이스는 객체이며, 어플리케이션에 특화된 업무 규칙을 구현하는 하나 이상의 함수를 제공한다.

엔티티와 유스케이스

엔터티는 자신을 제어하는 유스케이스에 대해 아무것도 알지 못한다.

즉, 엔터티와 같은 고수준 개념은 유스케이스와 같은 저수준 개념에 대해 아무것도 알지 못한다.

반대로 저수준인 유스케이스는 고수준인 엔티티에 대해 알고 있다.

따라서 유스케이스는 엔터티에 오직 의존하며, 엔티티는 유스케이스에 의존하지 않는다.

왜 엔티티는 고수준이며 유스케이스는 저수준일까?

  • 유스케이스는 단일 애플리케이션에 특화되어 있으며, 시스템의 출력에 보다 가깝게 위치하고 있기 때문이다.
  • 엔티티는 수많은 다양한 애플리케이션에 사용될 수 있도록 일반화된 것이므로, 각 시스템의 입력이나 출력에서 더 멀리 떨어져 있다.

요청 및 응답 모델

유스케이스는 입력 데이터를 받아서 출력 데이터를 생성한다.

그런데 제대로 구성된 유스케이스 객체라면 데이터를 사용자나 또 다른 컴포넌트와 주고 받는 방식에 대해서는 전혀 눈치챌 수 없어야 한다. 우리는 유스케이스 클래스의 코드가 HTML이나 SQL에 대해 알게 되는 일을 절대로 원치 않기 때문이다.

유스케이스는 단순한 요청 데이터 구조를 입력으로 받아들이고, 단순한 응답 데이터 구조를 출력으로 반환하며, 이들 데이터 구조는 어떤 것에도 의존하지 못한다.

왜냐하면 이들 데이터 구조는 httpRequest 이든, 웹이든, 그 어떤 사용자 인터페이스에도 종속되지 않기 때문이다.

이 요청 및 응답 모델이 독립적이지 않다면, 그 모델에 의존하는 유스케이스 역시 결국 해당 모델이 수반하는 의존성에 간접적으로 결합되게 되어버린다.

사실, 엔티티와 요청/응답 모델은 상당히 많은 데이터를 공유하므로 엔티티 객체를 가리키는 참조를 요청/응답 데이터 구조에 포함시켜 버리고자 하는 유혹이 실무에서는 있기 마련이다. 하지만 이들 두 객체의 목적은 완전히 다르므로 유혹을 떨쳐 내어야 한다.

 

업무 규칙은 바로 소프트웨어 시스템이 존재하는 이유 그자체에 해당한다. 업무 규칙은 조직의 가보이고 핵심적인 기능이기 때문이다.

업무 규칙은 저수준 관심사로 인해 오염되어서는 안되며, 원래 그대로의 모습으로 남아있어야 한다.

업무 규칙을 포함하는 코드는 반드시 시스템의 심장부에 위치해야 하며, 덜 중요한 코드는 이 심장부에 플러그인 되어야 한다.

업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용 될 수 있는 코드여야 한다.

 

댓글