
소프트웨어 아키텍처는 구성 요소 및 그들 간의 디펜던시(dependency, 의존관계)로 엮인 고수준의 구조물
마이크로서비스 패턴, 그리스리처드슨 저, 이일웅 역
아키텍처가 중요한 이유는 소프트웨어의 품질 속성, 즉 '~성'으로 기술되는 지표가 아키텍처에 의해서 결정되기 때문이다. 즉, 애플리케이션에서 필요로 하는 기능들, 즉 계산기에서 4칙 연산을 처리한다던가, 예측 알고리즘에서 95%의 정확도로 예측을 성공해 낸다던가 하는 것들은 어떠한 아키텍처를 갖더라도 구현이 가능할 수 있다. 그러나 4칙 연산에 사인, 코사인 계산을 추가하기 위한 확장성이나, 95%의 정확도 예측을 일 1회에서 매 시간단위로 확대하는 성능확장성이라던가, 매 배포때마다 수행하는 검증을 최소화하는 테스트성이라던가, 배포 시마다 수시간 단위의 서비스 중단을 경험해야 하는 배포성 등의 문제들은 기능 자체 보다는 아키텍처에 의해서 결정되는 부분들이라고 볼 수 있다. 마이크로서비스 아키텍처는 관리성, 테스트성, 배포성 등의 '~성' 지표들이 높은 성과를 낼 수 있는 애플리케이션을 구축하기 위한 아키텍처 스타일이다. (즉, 관리성, 테스트성, 배포성 등이 중요하지 않은 애플리케이션이라면 그냥 모놀리식으로 만드는 것이 합리적일 수도 있다는 뜻이다!)
컴퓨팅 시스템의 소프트웨어 아키텍처는 소프트웨어 엘리먼트(element)와 그들 간의 관계, 그리고 이 둘의 속성(property)으로 구성된 시스템을 추론하는데 필요한 구조(structure)의 집합이다.
소프트웨어 아키텍처 문서화(Documenting Software Architectures), Len Bass 외
결국, 애플리케이션은 구성 요소(element)의 묶음(혹은 분해)와 구성 요소간의 관계, 그리고 그 관계를 정의하는 속성들로 되어 있다는 의미이다. 이를 통해 다음 두가지 이득을 얻을 수 있다.
| 뷰 | 이해당사자 | 관점 | 설명 |
|---|---|---|---|
| 논리 뷰(logical view) | 분석가, 설계자 등 | 시스템 구조 | 개발자가 작성한 소프트웨어 엘리먼트, 클래스 등으로, 상속(inheritance), 연관(association), 의존(depends) 등의 관계 |
| 구현 뷰(implementation view) | 개발자 | 클래스, 패키지 등의 요소 | 빌드 결과물로, 모듈과 컴포넌트(하나 이상의 모듈로 구성되어 실행/배포가 가능한 단위)로 구성된 결과물. Java의 경우 JAR 또는 WAR 형태로 패키징된 결과물이며, 각 모듈간 의존성도 포함 |
| 프로세스 뷰(process view) | 시스템 통합자, 운영자 등 | 성능, 확장성 등 | 런타임 컴포넌트로, 각 엘리먼트가 개별 프로세스로 동작하며, IPC(Inter Process Communiation)은 프로세스간의 관계 |
| 배포 뷰(deployment view) | 시스템 엔지니어 등 | 형상, 배포 방식 등 | 프로세스가 머신에 매핑되는 방법으로, 엘리먼트는 HW인프라(VM 등) 및 프로세스가 되며, 엘리먼트간의 관계는 네트워킹으로 기술. 각 프로세스가 HW에 매핑되는 관계도 포함 |
| 시나리오(scenario) | 최종 사용자 | 고객 요구 기능 | +1 view에 해당하는 관점으로, 일반적으로 유스케이스 뷰(usecase view)라고도 말하며, 시나리오가 특정 뷰에서 각 뷰의 요소들이 동작하여 업무를 달성하는지를 나태느는 뷰, end user의 입장에서 필요한 요구사항 및 각 기능에 대한 내용 기술 |
N계층 아키텍처는 애플리케이션을 논리적 레이어와 물리적 계층으로 나누어 바라보는 스타일이다. 아래 그림은 MS Azure 페이지에서 가져온 그림으로 Client 가 WEB/Middle/Data tier로 연동하는 그림을 보여준다. 일반적으로 3계층 아키텍처 스타일을 많이 사용하고 있으며 필요에 따라 중간에 계층이 추가되는 형태로 주로 사용된다.
육각형 아키텍처는 애플리케이션의 표현계층을 대신하여, 비즈니스 로직을 호출하는 외부 요청 처리용 인바운드 어댑터와, 영속화 계층 대신 비즈니스 로직에 의해 호출되는 외부 애플리케이션이나 DB를 처리하는 아웃바운드 어댑터로 구성되는 아키텍처 스타일이다. 이러한 육각형 아키텍처 스타일은 비즈니스 로직이 어댑터에 전혀 의존하지 않는 것이 특장점이다.

비즈니스 로직에는 하나 이상의 포트가 있으며, 이 포트는 비즈니스 로직이 외부세계와 상호 작용하는 방법이 정의된 작업(operation)이다. 포트는 입력(inbound)과 출력(outbound)의 방향성을 가질 수 있으며, 입력 포트는 비즈니스 로직이 외부에 공개하는 API형태로, 외부 애플리케이션이 이 inbound 포트를 통해 애플리케이션에 요청을 하게 되는 공개(public) 메서드가 된다. 반면에 출력 포트는 비즈니스 로직이 외부에 오픈된 애플리케이션(DB 혹은 다른 애플리캐이션의 API 등)을 호출하는 역할을 수행한다.
어댑터는 비즈니스 로직을 감싸고 있으면서 포트와 유사하게 입력/출력으로 각자 동작한다. 예를 들어, 외부에서 들어오는 요청을 비즈니스 로직이 받아들이기 위한 API의 Rest endpoint, 메시지 브로커에서 메시지를 수신하기 위한 consumer나, 비즈니스로직이 DB 접근을 위한 데이터 접근 객체(Data Access Object - DAO)나, 원격 애플리케이션의 서비스를 호출하는 proxy 클래스, 혹은 메시지 브로커에 메시지를 발행하는 등의 어댑터로 구성된다.
육각형 아키텍처 스타일의 가장 큰 장점은 비즈니스 로직에 있던 표현/데이터 접근 로직이 어댑터와 분리되어 의존하지 않는다는 점으로 비즈니스 로직만 별도로 분리하여 개발, 테스트, 배포하는 등의 독립적 구현 상태를 반영하기 유리하다. 예를 든 육각형 아키텍처 스타일이나, 계층별 아키텍처 스타일 모두 3계층으로 논리뷰를 구성하고, 구성 요소를 정의했으며, 관계를 정의한 것은 비슷하지만, 육각형 아키텍처가 어댑터를 사용한 표현이 가능하기에 보다 로직과 관계를 명확하게 표시할 수 있게 된다.
모놀리식 아키텍처는 구현 뷰를 단일 컴포넌트(하나의 실행 파일 또는 WAR 파일)로 구성한 아키텍처 스타일을 의미한다. 육각형 아키텍처로 구성한 논리 뷰를 가질 수 있다.
패턴: 모놀리식 아키텍처
애플리캐이션을 실행/배포 가능한 단일 컴포넌트로 구성한다.
마이크로서비스 아키텍처는 구현 뷰를 다수의 컴포넌트(여러 실행 파일이나 WAR 파일)로 구성한 아키텍처 스타일을 의미힌다. 컴포넌트는 개별 서비스로 매핑할 수 있으며, 각 컴포넌트 별 잔체 논리뷰에 따른 아키텍처를 가지고 있을 수 있고, 커넥터는 각 컴포넌트 혹은 서비스간의 통신 프로토콜을 의미하며 이러한 커넥터와 local(private) 데이터를 이용하여 느슨하게 결합하여 서비스가 운영된다.
패턴: 마이크로서비스 아키텍처
애플리캐이션을 느슨하게 결합된, 독립적으로 배포 가능한 여러 서비스로 구성한다.
서비스는 어떤 기능이 구현되어 단독 배포가 가능한 소프트웨어 컴포넌트로 정의되며, 클라이언트로부터 애플리캐이션의 기능(functionalities)에 접근할 수 있도록 API를 제공하는 형태이다. Command, Query의 형태로 (HTTP POST/PUT/DELETE 또는 GET 등) 나누어서 볼 수 있으며, 서비스가 이벤트(오더가 생성되었음, 오더가 수락되었음 등)를 발행하기도 한다.
서비스 API는 기능의 내부의 상세 로직, 구현 등은 감추고 필요한 입력/출력 등만 보이도록 하는 캡슐화를 수행한다. 모놀리스 방식으로 개발이 진행되는 경우 다른 클래스를 상속하는 등의 절차로 직접 접근이 가능할 수 있었으나, 마이크로서비스 방식에서는 개별 서비스가 컴포넌트로 분리되어 있기 때문에 API를 우회하여 직접 클래스 등에 접근하는 코드는 작성이 불가능하게 된다.

느슨하게 결합된 서비스는 API가 유지되는 상태에서 내부 구현 코드를 변경 가능하다. 따라서 느슨하게 결합된 서비스는 유지보수성, 테스트성(~성, ~lities)를 달성하게 해 준다. 더욱이 직접DB를 접근하는 일을 수행하지 않으며 클래스의 필드 같은 서비스의 영속적 데이터는 private하게 유지해야 서비스의 DB 스키마 등을 변경할때 해당 데이터를 참조하는 서비스에 독립적으로 개발이 가능하게 된다. 또한 DB를 공유하지 않기 때문에 DB Lock을 획득하기 위한 블로킹, 경쟁 등이 불필요해지는 장점이 있다. 그럼에도 불구하고, 각 서비스가 관리하는 private(local) DB의 정합성을 유지하는 것은 더 큰 복잡한 문제를 야기하게 되며, 이 내용은 SAGA 패턴 등을 사용하여 구현하게 된다.
마이크로서비스 아키텍처에서도 공유 라이브러리 사용에 대하여는 주의가 필요하다. 코드의 중복성은 막아줄 수 있으나, 의도치않은 서비스간 결합이 발생하게 될 수 있다. 따라서 매우 일반적으로 사용되는 클래스 들은 공유 라이브러리로 구성하는 것이 필요하지만, 어느 수준까지 공유 라이브러리로 구현할 것인지는 충분한 검토가 필요하다.
애플리캐이션의 아키텍처는 다음의 3단계 절차를 따라 설계/정의 할 수 있다.

이처럼 서비스를 분해하는 과정을 따르다보면, 모놀리식 아키텍처에 대비하여 네트워크 지연, 비동기 통신으로 인한 가용성, 데이터 일관성 확보 및 만능 클래스(god class)에 대한 의존 등 다양한 현안들이 발생하게 된다.
시스템 작업은 2단계 프로세스를 기반으로 정의가 가능하다.

고수준의 도메인 모델은 주로 스토리의 명사에서 도출하며, 시스템 작업은 주로 동사에서 도출된다. 하나 이상의 도메인의 객체와, 객체간의 관계로 기술하며, 도메인 모델을 생성, 수정, 삭제하거나 관계를 생성, 삭제 할 수 있는 형태이다. 각 서비스는 자체 도메인 모델을 소유하고, 동작을 기술하는데 필요한 용어가 되는 핵심 클래스를 정의하게 된다.
위 작업을 통해, 소비자와, 음식점, 그리고 주문, 신용카드, 배달, 배달원 등의 클래스의 필요가 도출된다. 각 스토리를 유시하게 반복해서 진행하다보면, 다음과 같은 클래스 다이어그램을 도출하게 된다.

각 클래스가 도출되었다면 애플리캐이션은 클래스가 어떤 요청을 처리할지 정의해야 한다. RESTful API를 사용한다고 하면, Command(데이터의 생성, 수정, 삭제)와 Query(데이터 조회)에 대하여 시스템 작업을 추상화해야 한다. 앞서 정의한 스토리, 시나리오에서 필요한 동사로 기술된 항목들에 대해서 분석이 필요하다.
| 수행자 | 스토리 | Command | 설명 |
|---|---|---|---|
| 소비자 | 주문 생성 | createOrder() | 주문을 생성한다. 생성할때 주문에 line item도 필요하다 |
| 음식점 | 주문 접수 | acceptOrder() | 주문이 접수되엇고, 주방에서 주문에 맞는 음식을 조리한다. |
| 음식점 | 주문 준비완료 | noteOrderReadyForPickup() | 접수된 주문이 픽업 가능한 상태로 완료됨을 통보 -> 통보 받는 주체는 배달원이 될 수 있음 |
| 배달원 | 위치 업데이트 | noteUpdateLocation() | 현재 위치를 업데이트 |
| 배달원 | 배달 픽업 | noteDeliveryPickedUp() | 배달원이 주문/준비된 음식을 배달을 위해 픽업 완료 |
| 배달원 | 주문 배달 완료 | noteDeliveryDelivered() | 배달원이 픽업한 음식을 배달 완료 |
이러한 작업들을 통해서 FTGO 의 애플리케이션은 클래스를 정의하고 상호간 연관 관계를 정의하게 되면, 해당 겨로가를 기준으로 비즈니스에 따른 분해가 필요하다. 아래 그림은 비즈니스 능력을 기준으로 서비스로 분해한 예를 보여준다.

패턴: 하위도메인에 따라 분배
DDD 하위 도메인별로 서비스를 정의한다.

DDD에 따른 분해 패턴은 그 자체로 학습이 필요한 주제이므로, 여기서는 DDD 기반으로 서비스를 분해하는데 필요한 지침들만 추가로 확인하고 넘어가자.
클래스는 오직 하나의 변경 사유를 가져야 한다.
로버트 C. 마틴
클래스와 마찬가지로 마이크로서비스도 하나의 책임만을 가진 작고 응집된 서비스로 정의해야 한다.
패키지의 클래스들은 동일한 유형의 변경에 대해 닫혀 있어야 한다. 패키지에 영향을 주는 변경은 그 패키지에 속한 모든 클래스에 영향을 끼친다.
로버트 C. 마틴
만약 어떤 두 클래스가 동일한 사유로 인해 동시에 변경이 발생되어야 한다면, 두 클래스는 동일한 패키지에 있어야 한다는 것을 의미하는 것으로, 개발자는 가급적이면 소수의 패키지에 있는 코드만 책임지고 수정할 수 있도록 하는 원칙을 의미힌다. 클래스를 마이크로서비스 단위로 확장해보면, 동일한 사유로 변경되는 컴포넌트를 모두 같은 서비스로 묶는다 로 볼 수 있다.
Chap.1 에서 먼저 살펴보았지만, 저자는 5가지의 인력과 5가지의 척력으로 설명하였다. 여기서는 다음 5가지 내용을 분해의 장애물로 표기하고 있다.
이 중 만능 클래스, 즉 God class는 애플리케이션 곳곳에서 두루 쓰이게 되어 연관관계가 너무 복잡해져서 더이상 분해가 될 수 없는 수준이 된 클래스를 의미힌다. 이런 클래스는 하나의 기능(클래스)가 유사한 업무를 추가적으로 수행하기에 적합하든, 개발 할 조직이 그 곳 뿐이 없었든, 어떤 사유로든 지속적으로 동일 기능에 고도화가 이뤄지고, 연관관계를 갖는 기능들이 늘어나다보니 더이상 손을 쓸 수 없고, 작은 변경도 애플리케이션 전반에 영향을 주게 되는 상황이 되는 클래스를 의미하게 된다. FTGO 애플리케이션의 Order 클래스는 주소, 배달원, 소비자, 식당, 결제 등 모든 영역에 걸쳐서 업무가 이뤄지는 핵심 클래스 혹은 마이크로서비스가 된다. 따라서 이러한 문제를 해결하기 위해서는 도메인 단위로 분리하고, 각 도메인에서 필요한 클래스를 구축하는 형태로 만드는 것이 필요하다. 배달 서비스는 결제나, 식당의 메뉴에는 관심이 없고 필요한 경우 오더의 키 정보를 통해서 해당 정보를 조회하는 것 만으로도 충분할 수 있다.

시스템 작업과 서비스를 분해한 이후, 각 서비스별 API를 정의해야 한다. 서비스 API는 마이크로서비스 외부에서 호출하는 작업으로, 서비스 이벤트는 타 서비스와 협동하기 위해 발행되는 형태로 구성된다. 각각의 이벤트를 발생시키거나, 요청을 수락하는 API 들을 식별하고, 매핑하는 것이 필요하나, 본 교재가 DDD 등을 설명하는 내용이 아니다보니 해당 내용에 구체적인 내용들은 부족함이 있다. 따라서 추후 별도로 학습하는 것이 필요하다.
