[DDD&MSA] 마이크로서비스 애플리케이션 아키텍처
Book/DDD & MSA

[DDD&MSA] 마이크로서비스 애플리케이션 아키텍처

관심사의 분리, Separation of Concerns

  • 비즈니스 로직: 보통 시스템의 목적인 비즈니스 영역의 업무 규칙(Rule), 흐름(Flow), 개념(Concept)을 표현하는 용어다.

개발자의 역할은 문제 영역의 비즈니스 로직을 분석 및 이해하고 프로그래밍 언어라는 도구로 기능을 잘 동작하게 하고 이해하기 쉽고 변경하기 쉬운 시스템을 만드는 일이다.

 

관심사의 분리 원칙은 애플리케이션 아키텍처 설계 원칙 중 하나이다.

이것은 시스템의 각 영역이 처리하는 관심사가 분리되어 잘 관리돼야 한다는 의미이고, 시스템을 이해하고 변경하기 쉽게 만들어준다. 이 원칙에 따라 각 영역은 고유 관심사에 의해 분리되고 집중돼야 한다.

 

기술과 비즈니스 로직을 분리했을 때, 시스템을 이해하기 쉬워지므로 복잡성이 낮아지고 유지보수성도 높아진다.

바람직한 마이크로서비스는 기술과 비즈니스 로직을 분리해서 기술의 변화에도 비즈니스 로직이 영향받지 않고 쉽게 변경 및 확장이 가능하여야 한다.


데이터베이스 중심 아키텍처의 문제점

데이터베이스 중심 아키텍처란 특정 관계형 데이터베이스에 의존한 데이터 모델링을 수행한 다음, 이 물리 테이블 모델을 중심에 두고 애플리케이션을 구현하기 위한 사고를 하는 방식이다.

실무에서 일반적으로 빈번하게 사용하던 데이터 중심의 서비스 내부 구조이다.

일반적으로 스프링 프레임워크를 사용한다면 컨트롤러(Controller), 서비스(Service), DB I/O(Database Input/Output), DTO(Data Transfer Object)로 구성되고, 데이터 처리는 SQL 매핑 프레임워크인 마이바티스(MyBatis)를 사용한다.

 

이러한 구조에서 일반적으로 비즈니스 로직은 서비스에 존재해야 한다고 말하지만, 서비스에 존재하게 될 로직은 흐름 제어 로직밖에 없다. 그 밖의 비즈니스 개념과 규칙들은 테이블과 SQL 질의에 존재한다. DTO는 질의를 통해 가져오는 정보 묶음(Information holder)의 역할밖에 할 수 없다.

 

이 구조는 간단한 처리 로직의 경우에는 편하지만, 업무가 복잡해지면 점점 복잡성을 제어할 수 없게 된다. 또한 업무 개념이 특정 저장 기술인 관계형 데이터베이스 테이블로 표현되고 업무가 복잡해질수록 업무 규칙이 데이터 질의 언어인 SQL과 섞여 표현된다.

 

또한 성능 측면을 보면, 이같은 구조는 대부분의 성능을 데이터베이스에 의존한다. 그리고 다른 저장소로의 확장도 불편하다.

이러한 이유로 비즈니스 로직 처리와 데이터 처리를 철저히 분리하는 것이 반드시 필요하다.


헥사고날 아키텍처와 클린 아키텍처

애플리케이션의 구조를 유연하게 하기 위한 아키텍처들을 살펴보자.


레이어드 아키텍처

레이어드 아키텍처(계층형 아키텍처)를 구성하는 레이어(Layer)는 물리적인 티어의 개념과는 달리 논리적인 개념이다.

티어(Tier)는 물리적인 장비나 서버 컴퓨터 등의 물리층을 의미하고, 레이어는 티어 내부의 논리적인 분할을 의미한다.

 

아래 그림처럼, 물리적인 서버 티어의 레이어를 프레젠테이션, 비즈니스 로직, 데이터 액세스의 3개의 논리적인 레이어로 구분할 수 있다.

위에서 보이는 논리적인 3계층(프레젠테이션, 비즈니스 로직, 데이터 액세스)가 레이어드 아키텍처 패턴의 전형적인 유형이다. 각 관심사는 다음과 같다.

  • 프레젠테이션: 화면 표현 및 전환 처리
  • 비즈니스 로직: 비즈니스 개념 및 규칙, 흐름 제어
  • 데이터 액세스: 데이터 처리

 

레이어드 아키텍처는 레이어 간 응집성을 높이고 의존도를 낮추기 위해 다음과 같은 규칙을 둔다.

  • 상위 계층이 하위 계층을 호출하는 단방향성을 유지한다. (역 호출은 불가능)
  • 상위 계층은 하위의 여러 계층을 모두 알 필요 없이 바로 밑의 근접 계층만 활용한다.
  • 상위 계층이 하위 계층에 영향을 받지 않게 구성해야 한다.
  • 하위 계층은 자신을 사용하는 상위 계층을 알지 못하게 구성해야 한다.
  • 계층 간의 호출은 인터페이스를 통해 호출하는 것이 바람직하다. (구현 클래스에 직접 의존하지 않음으로써 약한 결합을 유지해야 한다.)

 

특히 인터페이스를 통한 의존성 분리는 인터페이스를 구현하는 구현체를 다양하게 해주는 다형성을 추구함으로써 제어 흐름을 간접적으로 전환하게 해준다.

 

아래 그림처럼 상위 계층은 직접적으로 하위 계층을 호출하지 않고 추상적인 인터페이스에 의존한다. 이 경우 하위 계층에서는 추상적 인터페이스를 만족하는 다양한 방식의 구현체를 선택할 수 있다.

이러한 방식은 의존석 역전 원칙(DIP; Dependency Inversion Principle): 유연성이 극대화된 시스템에서는 소스코드 의존성이 추상에 의존하며, 구현체에 의존하지 않아야 한다. 을 만족하는 것처럼 보인다.

 

하지만 개방 폐쇄의 원칙(OCP; Open-Closed Principle): 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. 은 위배한다.

그 이유는 모든 계층이 각기 자신이 제공하는 기능에 대한 추상적인 인터페이스를 직접 정의하고 소유하고 있는 구조이기 때문이다. 이런 구조에서는 제어 흐름(flow of control)이 상위 계층에서 하위 계층으로 흐르게 되고, 이에 따른 소스코드의 의존성은 제어 흐름의 방향대로 따를 수 밖에 없다.

 

위의 그림을 보면, 데이터 액세스 계층의 구현체가 클래스 C에서 D로 변경된다면 비즈니스 로직 계층은 영향을 받지 않지만, 데이터 액세스 계층의 인터페이스 B가 변경되면 비즈니스 로직 계층의 클래스 A가 데이터 액세스 계층의 인터페이스에 의존하기 때문에 변경의 영향을 필연적으로 받는다. 데이터 액세스 인터페이스의 위치 때문에 상위 계층이 하위 계층에 의존하게 된다.

 

그렇기 때문에 다음 그림처럼, 데이터 액세스 계층에서 정의한 인터페이스를 경계를 넘어 비즈니스 로직 계층으로 옮긴다. 그러면 데이터 액세스 계층의 구현체는 비즈니스 로직 계층의 인터페이스를 바라보게 된다.

 

즉, 데이터 액세스 계층이 구현해야 할 인터페이스를 좀 더 고수준의 비즈니스 로직 계층에서 정의하게 함으로써 기존의 위에서 아래로 흘렀던 의존 관계를 역전시키고 고수준 영역이 저수준 영역의 변경에 영향을 받지 않게 된다.


헥사고날 아키텍처

현대 어플리케이션에서는 프레젠테이션 계층, 데이터 액세스 계층 말고도 다양한 인터페이스를 필요로 한다.

즉, 애플리케이션을 호출하는 다양한 시스템의 유형과 애플리케이션과 상호작용하는 다양한 저장소가 존재한다.

단방향 계층구조에서는 이러한 점을 지원하기 힘들다. 다방면으로 열려있는 헥사고날 아키텍처는 이러한 문제점을 해결할 수 있다.

헥사고날 아키텍처는 '포트 앤드 어댑터 아키텍처(ports and adapters architecture)' 라고도 한다.

  • 헥사고날 아키텍처는 고수준의 비즈니스 로직을 표현하는 내부영역과 인터페이스 처리를 담당하는 저수준의 외부 영역으로 나뉘어진다.
    • 내부 영역은 순수한 비즈니스 로직을 표현하는 기술 독립적인 영역이다. 그리고 외부 영역과 연계되는 포트를 가지고 있다.
    • 외부 영역은 외부에서 들어오는 요청을 처리하는 인바운드 어댑터(Inbound Adapter)와 비즈니스 로직에 의해 호출되어 외부와 연계되는 아웃바운드 어댑터(Outbound Adapter)로 구성된다.
  • 헥사고날 아키텍처는 고수준의 내부 영역이 외부의 구체 어댑터에 전혀 의존하지 않게 한다. 내부 영역에 구성되는 포트가 이를 가능케 한다.
    • 인바운드 포트는 내부 영역의 사용을 위해 표출된 API이며, 외부 영역의 인바운드 어댑터가 호출한다.
    • 아웃바운드 포트는 내부 영역이 외부를 호출하는 방법을 정의한다. DIP 원칙과 같이 아웃바운드 어댑터가 아웃바운드 포트에 의존해서 구현된다.

 

인바운드 어댑터로는 REST API를 발행하는 컨트롤러, 웹 페이지를 구성하는 스프링 MVC 컨트롤러, 커맨드 핸들러, 이벤트 메시지 구독 핸들러 등이 될 수 있고, 아웃바운드 어댑터로는 데이터 액세스 처리를 하는 DAO, 이벤트 메시지를 발행하는 클래스, 외부 서비스를 호출하는 프록시 등이 될 수 있다.


클린 아키텍처

클린 아키텍처는 여러 겹으로 둘러싸인 영역으로 표현되며, 중앙에서부터 밖으로 엔티티, 유스케이스, 그 외 세부사항으로 구분된다.

  • 가장 안쪽에는 엔티티가 있다. 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차이다. 모든 시스템에는 해당 도메인의 업무를 규정하는 핵심 업무 규칙이 존재하고, 보통 데이터를 요구한다. 업무 규칙에서 요구하는 데이터를 객체화시킨 것이 엔티티 객체이다.
  • 엔티티를 감싸는 객체는 유스케이스(use case)이다. 유스케이스는 자동화된 시스템을 사용하는 처리 절차를 기술한다. 유스케이스는 애플리케이션에 특화된 업무 규칙을 표현하며, 엔티티 내부의 핵심 업무 규칙을 호출하며 시스템을 사용하는 흐름을 담는다.
    • 이때 엔티티같은 고수준 영역은 저수준의 유스케이스 영역을 알게 해서는 안된다.
    • 엔티티는 간단한 객체여야 하며, 프레임워크 데이터베이스 또는 기타 복잡한 것에 의존해서는 안되고 유스케이스 객체를 통해서만 조작해야 한다.
  • 유스케이스를 감싸는 나머지 모든 영역이 세부사항이다. 세부사항으로는 입출력 장치, 저장소, 웹 시스템, 서버, 프레임워크, 통신 프로토콜이 될 수 있으며, 세부 사항과 유스케이스의 관계를 DIP 원칙을 이용해 플러그인처럼 유연하게 처리해야 한다.

마이크로서비스의 내부 구조 정의

레이어드 아키텍처, 헥사고날 아키텍처, 클린 아키텍처는 기존 모노리스 애플리케이션 유형에도 통용되는 아키텍처이다.

하지만 최근 강조되는 이유는 마이크로서비스가 지향하는 유연성, 확장성을 지원하는 구조이기 때문이다.


클린 마이크로서비스

마이크로서비스를 이루고 있는 내부 구조는 위 그림처럼 자율적인 마이크로서비스 팀에 의해 폴리글랏하게 구성될 수 있다.

 

따라서 마이크로서비스 아키텍처에서 각 서비스는 각기 목표와 활용도에 따라 명확하게 분리돼야 하고, 각 서비스의 목적에 따라 적절한 개발 언어 및 저장소, 내부 아키텍처를 정의하는 것이 좋다.

 

조회나 아주 간단한 기능의 경우를 제외하고는 헥사고날 아키텍처나 클린 아키텍처의 구조를 기반으로 서비스를 정의하는 것이 바람직하다.

 

레이어드 아키텍처, 헥사고날 아키텍처, 클린 아키텍처 이 3가지 아키텍처가 지향하는 원칙들은 다음과 같다.

  • 지향하는 관심사에 따라 응집성을 높이고 관심사가 다른 영역과는 의존도를 낮추게 해야 한다.
  • 업무 규칙을 정의하는 비즈니스 로직 영역을 다른 기술 기반 영역으로부터 분리하기 위해 노력한다.
  • 세부 기술 중심, 저수준의 외부 영역과 핵심 업무 규칙이 정의된 고수준의 내부 영역으로 구분한다.
  • 고수준 영역은 저수준 영역에 의존하지 않게 해야 하며, 저수준 영역이 고수준 영역에 의존하게 해야 한다.
  • 저수준 영역은 언제든지 교체, 확장 가능해야 하며, 이같은 변화가 고수준 영역에 영향을 줘서는 안 된다.
  • 자바처럼 인터페이스 및 추상 클래스를 지원하는 언어의 경우, 저수준 영역의 구체 클래스가 고수준 영역의 추상 인터페이스에 의존하게 하는 의존성 역전의 원칙(DIP)를 적용한다.
  • 인터페이스는 고수준의 안정된 영역에 존재해야 하며, 저수준의 어댑터가 이를 구현한다.

위 그림은 마이크로서비스의 내부 구조 예시이다.

 

내부 영역에서는 맨 안쪽에 도메인이 존재하고 도메인을 서비스가 감싼다. 도메인에는 핵심 비즈니스 개념과 규칙을 구현하며, 서비스에서는 도메인을 호출해서 업무를 처리하는 절차를 기술한다. 또한 외부 영역과 연계하기 위한 서비스 인터페이스를 보유한다. 서비스 인터페이스는 외부에서 내부 영역을 사용할 수 있도록 API를 제공하고 서비스가 이를 구현한다.

 

내부 영역에 있는 레포지토리(Repository) 인터페이스는 저장소 처리를 위한 인터페이스다. 레포지토리 인터페이스는 외부 영역에서 정의하지 않고 내부 영역에서 정의하는데, 비즈니스를 처리하는 데 필요한 기본적인 저장소 처리 사항을 추상화해 정의한다. 그렇게 하면 외부 영역의 저장소 어댑터는 이 레포지토리 인터페이스를 각 저장소에 맞게 저장소 처리 세부 기술로 구현하게 된다.

 

외부 영역에는 저장소 처리 어댑터뿐만 아니라 다양한 인바운드, 아웃바운드를 처리하는 어댑터가 위치한다. REST API를 처리하는 어댑터, 이벤트 메시지를 처리하는 어댑터, 메시지를 구독하는 메시지 컨슈머 어댑터, 다른 마이크로서비스의 API를 호출하는 프록시 어댑터 등이 위치한다. 이런 아웃바운드 어댑터는 DIP를 적용하여 외부 영역에서 내부 영역에 의존하도록 설계한다.


내부 영역 - 업무 규칙

업무 규칙을 정의하는 내부 영역에는 서비스 인터페이스, 서비스 구현체, 도메인, 레포지토리 인터페이스, 도메인 이벤트 인터페이스, API 프록시 인터페이스가 존재한다.

각 내부 영역 구현에 필요한 패턴은 다음과 같다.

 

서비스 인터페이스는 외부 영역이 내부 영역에 대해 너무 많이 알지 못하게 하는 역할을 한다. 이는 서비스를 사용하는 클라이언트의 사용 편의성을 높여준다. 서비스 인터페이스가 없다면 추이 종속성이 발생할 수 있다. (정보 은닉 효과도 있다.)

 

레포지토리 인터페이스, 도메인 이벤트 인터페이스, API 프록시 인터페이스는 의존 관계 역전의 원칙을 지원한다. 더 안정된 곳인 고수준 영역에 인터페이스가 존재하고, 저수준의 외부 어댑터가 이러한 인터페이스를 구현하게 한다.

 

서비스와 도메인은 클린 아키텍처의 유스케이스와 엔티티의 역할과 같다. 도메인은 비즈니스 개념을 표현하고 서비스는 도메인을 활용해 시스템 흐름 처리를 수행한다.

 

특히 서비스와 도메인의 관계를 구현할 때 유용한 패턴이 아래에서 언급될 트랜잭션 스크립트 패턴도메인 모델 패턴이다.


트랜잭션 스크립트 패턴

트랜잭션 스크립트(Transaction Script) 패턴에서는 비즈니스 개념을 표현하는 도메인 객체가 행위를 가지고 있지 않다.

따라서 모든 비즈니스 행위, 즉 무엇인가를 수행하는 책임은 서비스에 있다.

 

서비스가 비즈니스 절차에 따라 절차적으로 도메인 객체를 이용해 모든 처리를 수행하며, 이런 방식에서는 시간이 지남에 따라 서비스가 비대해질 경우, 도메인 객체는 정보 묶음의 역할만 수행하게 된다.

 

또한 서비스는 유스케이스 처리의 단위이고, 대부분의 비즈니스 로직 처리가 서비스에서 이뤄지므로 비슷한 유스케이스의 경우 서비스에 중복되는 코드가 계속 생기게 된다. 이는 유지보수를 어렵게 한다.

트랜잭션 스크립트 패턴은 절차식 프로그래밍 방식과 같기 때문에 객체지향 지식이 없어도 일반적으로 쉽게 이해할 수 있고, 데이터베이스 중심 아키텍처에 익숙하면 더 쉽게 적응할 수 있다.

 

이 패턴은 간단한 비즈니스의 경우에는 쉽게 적용할 수 있지만, 비즈니스가 복잡해질 경우, 위에서 언급한 데이터베이스 중심 아키텍처에서 만났던 문제점이 발생할 가능성이 크다.


도메인 모델 패턴

도메인 모델(Domain Model) 패턴은 도메인 객체가 데이터뿐만 아니라 비즈니스 행위를 가지고 있으며, 도메인 객체가 소유한 데이터는 도메인 객체가 제공하는 행위에 의해 은닉된다.

 

도메인 객체는 각 비즈니스 개념 및 행위에 대한 책임을 수행하고, 서비스는 비즈니스 유스케이스를 구현하기 위해 서비스의 행위를 도메인 객체에 일부분 위임해서 처리한다.

 

서비스의 책임들이 도메인으로 적절히 분산되기 때문에 서비스가 비대해지지 않고 서비스 메소드는 단순해진다. 도메인 모델 패턴의 도메인 모델은 객체지향 설계의 객체 모델이다. 거대한 서비스 클래스 대신 각기 적절한 책임을 가진 여러 클래스로 구성되므로 이해하기 쉽고 관리 및 테스트하기 쉽다.


도메인 주도 설계의 애그리거트 패턴

애그리거트 패턴은 점점 복잡해질 수 있는 객체 모델링의 단점을 보완한 패턴이다.

도메인 모델이 커짐에 따라 발생하는 문제들을 개선할 방안으로, 최상위에 존재하는 엔티티(Root Entity)를 중심으로 개념의 집합을 분리한 것이 애그리거트 패턴이다.

 

아래 그림처럼 복잡한 모델을 세 덩어리의 개념으로 분리한 예를 보면, 1개 이상의 엔티티와 값 객체(VO; Value Object)로 구성되는데, 개념적으로 묶인 엔티티의 모음 전체를 애그리거트라고 한다.

애그리거트 패턴에서는 애그리거트를 한 단위로 일관되게 처리하기 위해 다음과 같은 규칙을 부여한다.

  • 애그리거트 루트만 참조한다.
  • 애그리거트 내 상세 클래스를 바로 참조하지 않고 루트를 통해 참조해야 한다. (수정할 때도 마찬가지)
  • 애그리거트 간의 참조는 객체를 직접 참조하는 대신 기본키를 이용한다.
  • 기본키를 사용하면 느슨하게 연관되고, 수정이 필요하지 않은 애그리거트를 수정하는 실수를 방지한다.
  • 하나의 트랜잭션으로 하나의 애그리거트만 생성/수정한다.

외부 영역 - 세부사항

외부 영역은 내부 영역의 서비스 인터페이스를 사용하는 인바운드 어댑터와 내부 영역에서 선언한 아웃바운드 인터페이스를 구현하는 다양한 어댑터로 구성한다.

어댑터는 플러그인처럼 교체와 확장이 자유로워야 하며, 내부 영역이 먼저 정의된 후에 외부 영역의 세부사항은 늦게 정의돼도 상관없도록 해야 한다.


API 퍼블리싱 어댑터

API 퍼블리싱 어댑터는 REST API를 발행하는 인바운드 어댑터이다.

내부 영역의 서비스 인터페이스를 호출해서 REST 형식의 API를 제공한다.

 

명시적인 REST 리소스 명칭을 정의하고, 각 REST 메소드가 의도에 맞게 서비스 인터페이스를 호출한다.

엔티티를 직접 제공하지 않고 API의 필요에 맞는 DTO를 생성해서 엔티티를 변환 및 매핑해서 전달하는 것이 바람직하다.


API 프록시 어댑터

API 프록시 어댑터는 다른 서비스의 API를 호출하는 아웃바운드 어댑터이다.

 

내부 영역에 정의된 프록시 인터페이스를 구현하며, 다른 서비스의 API는 REST API가 될 수도 있고 소켓이나 SOAP 프로토콜을 사용하는 API일 수도 있으므로, 기술에 맞는 적절한 통신 방법 구현이 필요하다.


저장소 처리 어댑터

저장소 처리 어댑터는 데이터 처리 메커니즘를 OR 매핑 방식과 SQL 매핑 방식 중 하나를 선택하여 구현한다.

일반적으로 트랜잭션 스크립트 패턴을 사용할 경우 SQL 매핑 방식을 사용하고, 도메인 모델 패턴을 사용할 경우 OR 매핑 방식을 많이 사용한다.

 

SQL 매핑 방식의 프레임워크로는 마이바티스(MyBatis)가 가장 대표적이고, OR 매핑 방식의 프레임워크로는 JPA나 스프링 데이터(Spring Data)가 많이 사용된다. SQL 매핑 방식의 경우 SQL 질의문을 수동으로 작성해야 하므로 세밀한 SQL 제어가 필요한 경우 유용하다.

 

OR 매핑 방식은 SQL 매핑 방식보다 좀 더 유연한 메커니즘으로, OR 매퍼가 런타임 시 저장소에 따라 자동으로 질의문을 생성한다. 따라서 SQL 작성에 따르는 개발자의 작업량을 줄일 수 있다. 또한 설정 내용에 따라 손쉽게 저장소를 변경할 수 있다.


도메인 이벤트 발행 어댑터

도메인 이벤트는 외부 아키텍처에서 서비스 간 비동기 메시지 통신에서 전달 대상이 되는 정보이다.

 

도메인 이벤트는 어떤 사건에 따른 상태의 변경 사항을 말하는데, '주문됨', '주문취소됨' 등의 명칭을 갖는 클래스로 구현되며, 컨슈머(Consumer)에게 전달되기 위해 도메인 이벤트 발행 어댑터를 통해 발행 된다.

 

애그리거트 패턴을 적용할 경우 도메인 이벤트는 애그리거트에서 발생한 사건이 된다.

 

실제로 도메인 이벤트가 생성되는 위치는 내부 영역이며, 도메인 이벤트 발행 어댑터는 내부 영역의 이벤트 인터페이스를 구현해서 아웃바운드로 특정 메시지 큐나 스트림 저장소에 발행하는 역할을 수행한다.


도메인 이벤트 핸들러

도메인 이벤트 핸들러는 외부에서 발행된 도메인 이벤트를 구독해서 내부 영역으로 전달하는 일을 수행하는 인바운드 어댑터이다.

 

이벤트 상태에 따라 적절한 서비스 인터페이스를 호출해서 내부 영역에 이벤트를 전달한다.


Refer to

 

마이크로 서비스 지향 애플리케이션 디자인

컨테이너화된 .NET 애플리케이션용 .NET 마이크로 서비스 아키텍처 | 마이크로 서비스 지향 애플리케이션의 장점과 단점을 이해하면 최신 정보에 입각한 결정을 내릴 수 있습니다.

docs.microsoft.com