ETC

도메인 주도 설계로 시작하는 마이크로서비스 개발 Chapter 3

마이크로서비스 애플리케이션 아키텍처

Posted by Damin on October 15, 2021

3장 : 마이크로서비스 애플리케이션 아키텍처

3.1 : 비즈니스 로직은 어디에? - 관심사의 분리

  • 비즈니스 로직 : 시스템의 목적인 비즈니스 영역의 규칙, 흐름, 개념을 표현하는 용어

  • 관심사의 분리 : 시스템의 각 영역이 처리하는 관심사가 분리되어 잘 관리돼야 한다는 의미

설계 원칙 중 관심사의 분리를 통해 시스템을 이해하고 변경하기 쉽게 만들 수 있다

비즈니스를 표현하는 비즈니스 로직 영역과 기술 문제를 처리하기 위한 기술 영역은 철저히 분리하는 것이 좋다

비즈니스 로직이 기술보다는 오랫동안 지속되고 안정적이어야 할 애플리케이션의 핵심 영역이기에 기술에 영향을 적게 받도록 설계하는 것

이를 통해 복잡성이 낮아지고 유지보수성도 높아진다

애플리케이션의 유지보수성이 높다는 의미는 특정 개인에 의존하기 보다는 어느 누구라도 손쉽게 애플리케이션을 이해하고 유지보수할 수 있음을 의미한다

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

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

비즈니스 민첩성을위해서는 유연성과 확장성이 중요하다

비즈니스의 특정 기능을 위해 읽기에 최적화된 NoSQL 저장소로 교체하려고 한다면, 해당 시스템 구조에서는 저장소를 변경하려고 해도 쉽게 변경할 수가 없다

저장 기술과 비즈니스 로직이 끈끈하게 붙어 있기 때문에 저장소를 변경했을 때 모든 것을 다시 구현해야 한다

또한 이러한 구조는 대부분의성능을 데이터베이스에 의존한다

클라우드 인프라의 가장 큰 장점인 사용량에 유연하게 대응하는 자동 스케일 아웃이 효과를 보기 어렵다

바쁜 것은 데이터베이스이기 때문에 애플리케이션을 아무리 스케일 아웃해봐야 거둘 수 있는 효과가 미미하다

클라우드의 풍부한 자원 환경에서는 애플리케이션 자체의 성능보다는 애플리케이션의 확장성과 유연함이 더 중요하다

따라서 비즈니스 로직 처리와 데이터 처리를 철저히 분리하는 것이 반드시 필요하다

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

레이어드 아키텍처

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

레이어드 아키텍처 규칙

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

이러한 아키텍처에는 문제가 있다

하위 계층의 유형이 추가되어 확장될 때 닫혀 있어야 할 상위 계층이 하위 계층에서 정의한 특성에 영향을 받게 된다

인터페이스의 위치가 문제가 되는데, 데이터 액세스 인터페이스는 데이터 액세스 계층에 존재한다

비즈니스 로직은 데이터 액세스 인터페이스를 의존하는데, 인터페이스가 변경되면 비즈니스 로직까지 변경 되어야 한다

이를 위해 데이터 액세스 계층이 구현해야 할 인터페이스를 비즈니스 로직 계층에서 정의하게 함으로 위에서 아래로 흘렀던 의존 관계를 역전 시킨다

헥사고날 아키텍처

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

단방향 계층구조에서는 이러한 점을 지원하기 힘들다

헥사고날 아키텍처는 이러한 문제점을 해결할 수 있다

고수준의 비즈니스 로직을 표현하는 내부 영역과 인터페이스 처리를 담당하는 저수준의 외부 영역으로 나눈다

  • 내부 영역 : 순수한 비즈니스 로직을 표현하는 기술 독립적인 영역
  • 외부 영역 : 외부에서 들어오는 요청을 처리하는 인바운드 어댑터, 비즈니스 로직에 의해 호출되어 외부와 연계되는 아웃바운드 어댑터로 구성

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

클린 아키텍처

  • 엔티티 - 유스케이스 - 컨트롤러 - UI

모든 시스템에는 해당 도메인의 업무를 규정하는 핵심 업무 규칙이 존재한다. 그리고 핵심 업무 규칙은 보통 데이터를 요구한다

엔티티 : 핵심 규칙과 데이터는 본질적으로 결합돼 있기 때문에 객체로 쉽게 만들 수 있다

유스케이스 : 자동화된 시스템을 사용하는 처리 절차

유스케이스는 애플리케이션에 특화된 업무 규칙을 표현하며, 엔티티 내부의 핵심 업무 규칙을 호출하며 시스템을 사용하는 흐름을 담는다

이때 엔티티는 유스케이스 영역을 알게 해서는 안된다. 엔티티는 간단한 객체여야 하며, 프레임워크 데이터베이스 또는 기타 복잡한 것에 의존해서는 안되고 유스케이스객체를 통해서만 조작해야 한다

엔티티와 유스케이스를 제외한 나머지 영역이 세부사항이다

이러한 세부사항들과 유스케이스의 관계를 의존 관계 역전의 원칙을 이용해 플러그인처럼 유연하게 처리해야 한다

이러한 명확한 결합의 분리는 테스트 용이성 및 개발 독립성, 배포 독립성을 강화할 수 있다

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

바람직한 마이크로서비스의 내부 아키텍처 : 클린 마이크로서비스

마이크로서비스의 내부 구조를 정의할 때 폴리글랏한 내부 구조를 가지기 때문에, 내부 구조가 다양할 수 있는 것을 고려해야 한다

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

내부 영역 - 업무 규칙

  • 서비스 인터페이스

외부 영역이 내부 영역에 대해 너무 많이 알지 못하게 하는 역할

서비스 인터페이스가 없다면 추이 종속성이 발생할 수 있다 (정보 은닉 효과)

  • 레포지토리 인터페이스, 도메인 이벤트 인터페이스, API 프록시 인터페이스

의존 관계 역전의 원칙을 지원한다

더 안정된 곳인 고수준 영역에 인터페이스가 존재하고 저수준의 외부 어댑터가 이러한 인터페이스를 구현하게 한다

트랜잭션 스크립트 패턴

비즈니스 개념을 표현하는 도메인 객체가 행위를 가지고 있지 않다

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

서비스가 비즈니스 절차에 따라 절차적으로 도메인 객체를 이용해 모든 처리를 수행한다

비즈니스가 복잡해질 경우 서비스 코드의 양이 증가하고, 중복되는 코드가 생겨날 수 있다. 따라서 간단한 비즈니스를 처리할 때 적용하는 것이 좋다

도메인 모델 패턴

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

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

서비스의 책임들이 도메인으로 적절히 분산 -> 서비스가 비대해지지 않고 서비스 메서드가 단순해진다

복잡한 비즈니스 로직을 처리하는 데 유용하며, 코드의 양을 줄이고 재사용성도 높인다

외부영역 - 세부사항

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

플러그인처럼 언제든지 교체되거나 확장될 수 있어야 한다

API 퍼블리싱 어댑터

  • REST API를 발행하는 인바운드 어댑터

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

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

API 프록시 어댑터

  • 다른 서비스의 API를 호출하는 아웃바운드 어댑터

내부 영역에 정의된 프록시 인터페이스를 구현, 기술에 맞는 적절한 통신 방법 구현

저장소 처리 어댑터

  • 트랜잭션 스크립트 패턴을 사용할 경우 SQL 매핑 방식, 도메인 모델 패턴을 사용할 경우 OR 매핑 방식

SQL 매핑 방식의 경우 SQL 질의문을 수동으로 작성해야 하므로 세밀한 SQL 제어가 필요할 경우 유용하다

OR 매핑 방식은 OR 매퍼가 런타임 시 저장소에 따라 자동으로 질의문을 생성 -> SQL 작성에 따르는 개발자의 작업량을 줄인다

설정 내용에 따라 손쉽게 저장소를 변경할 수 있어서 SQL 매퍼 방식보다 유연한 메커니즘이다

도메인 이벤트 발행 어댑터

  • 어떤 사건에 따른 상태의 변경 사항

“주문됨”, “주문 취소됨” 등의 명칭을 갖는 클래스로 구현, 컨슈머에게 전달되기 위해 도메인 이벤트 발행 어댑터를 통해 발행된다

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

도메인 이벤트 핸들러

  • 외부에서 발행된 도메인 이벤트를 구독해서 내부 영역으로 전달하는 일

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