MSA 한번에 이해하기
기타 공부

MSA 한번에 이해하기

MSA의 등장 배경

MSA의 등장은 모놀리식 아키텍처(Monolithic Architecture)에서 부터 시작됩니다. 

Monolithic이라는 단어는 "하나의 암석으로 된", "한 덩어리의"라는 뜻을 가지고 있습니다.

그동안 대부분의 소프트웨어들은 모놀리식 아키텍처로 개발되었습니다.

이들은 단단한 암석이지만,

하나의 저장소에 모든 코드가 담겨있어 획일화 된 구조를 갖고 있었습니다.

 

모놀리식 아키텍처

 

모놀리식 아키텍처의 장점은 다음과 같습니다.

  1. 프로그램 전체의 개발 스택이 동일하여 복잡하지 않습니다.
  2. 고가용성 서버 환경을 쉽게 만들 수 있습니다.
  3. End-to-End 테스트가 용이합니다.
  4. 전통적인 소프트웨어 아키텍처로 개발자들에게 익숙합니다.
  5. 작은 규모의 서비스에서부터 개발하기가 쉽습니다. 

 

단점은 다음과 같습니다.

  1. 프로그램 규모가 커짐에 따라 앱 구동시간이 늘어나고, 빌드, 배포 시간이 길어집니다.
  2. 조그마한 수정 사항이 있어도 전체를 다시 빌드하고 배포를 해야합니다.
  3. 일부분의 오류가 전체에 영향을 미칩니다.
  4. 모든 서비스들을 만족시키는 기술 스택을 사용하기 까다롭습니다.

 

오래전부터 꾸준히 개발되었던 프로그램들은

시간에 지남에 따라 프로그램 크기가 공룡처럼 커지고,

다양한 개발 스타일들이 기록되었기 때문에,

다른 개발자가 코드들을 읽기 힘든 경우가 잦아졌습니다.

 

이들은 유지보수 난이도 상승과

장애 대처 속도 지연을 발생시켰습니다.

기업들의 입장에서는

큰 손해 비용을 감당해야하는 상황으로 이어지는 상황이었기 때문에,

문제 해결 방법을 찾기 위해 노력하고 있었습니다.

 

이러한 배경에서 아마존이 등장하게 됩니다.

 

MSA의 등장

 

아마존 닷컴 이사회 의장, 제프 베조스, Jeffrey Preston Bezos

 

아마존은 모놀리식 아키텍처의 문제점들을 해결하기 위한 방법을 제시하였습니다.

아마존 역사에서 제일 중요한 제프 베조스의 2002년 메일 내용입니다.

 

  1. 모든 팀은 서비스 인터페이스로 데이터와 기능을 공개하세요.
  2. 팀들은 이 인터페이스로 통신 하세요.
  3. 직접 링킹, 다른팀 저장소에 직접 억세스, 공유메모리, 백도어 등, 다른 어떤 통신방법도 허용되지 않습니다. 네트워크를 통한 서비스 인터페이스 호출만 허용합니다.
  4. 어떤 기술을 사용하는가는 중요하지 않습니다. HTTP, Corba, Pubsub, 커스텀 프로토콜 다 괜찮습니다.
  5. 모든 서비스 인터페이스는 예외없이 기초부터 모두 외부에서 사용 가능하도록 설계되어야 합니다. 즉, 팀들은 인터페이스를 외부 개발자가 이용가능하도록 계획하고 설계해야 한다는 것입니다. 예외는 없습니다.
  6. 이를 지키지 않는 사람은 해고 될것입니다.
  7. 고맙습니다. 좋은 하루 되세요!

 

메일에는 MSA의 본질적인 내용들이 담겨있습니다.

이러한 약속들을 바탕으로 모놀리식 아키텍처의 문제점들을 해결하였습니다.

시간이 흘러 2006년 플랫폼을 공개하였고, 바로 그 유명한 아마존 웹 서비스(AWS)입니다.

 

아마존은 잘 구축된 아키텍처와 기술과 문화를 기반으로

물류 기업에서 타 기업들이 선망하는 1등 IT기업으로 성장할 수 있게 되었습니다.

 

그렇다면 모두가 좋다고 시도하는 MSA란 도대체 무엇일까요?

 

MSA의 목적

 

 

위 사진은

모놀리식 아키텍처와 마이크로 서비스 아키텍처의 차이점을 잘 보여주고 있습니다.

거대한 하나의 모놀리식과 달리

마이크로 서비스는 여러 컨테이너로 조합되어 사용자의 목적을 이루고 있습니다.

 

상황에 맞춰 물류의 크기와 적재 방식을 변경할 수 있고,

불량이 섞인 컨테이너를 찾아내고 처리하는 과정이 더욱 용이합니다.

 

 

위 개념을 소프트웨어 아키텍처에 적용하면 어떻게 될까요?

 

MSA

우리는 같은 비지니스 성격을 갖는 도메인들끼리 묶어

서버와 데이터베이스를 독립적인 하나의 서비스로 구성할 수 있습니다.

이렇게 된다면 각각의 서비스에서 장애가 발생하더라도

해당 서비스만 관리하면 되기 때문에,

다른 서비스가 받는 장애 전파의 영향을 최소화 할 수 있습니다.

 

MSA의 목적(장점)은 다음과 같습니다.

  1. 시스템 전체를 종료하지 않고 서비스 별로 배포 가능하여, 요구사항이 신속하게 반영될 수 있다.
  2. 특정 서비스에 대해서만 자원 확장성을 통해 투자할 수 있다. (클라우드와 잘 어울림)
  3. 부분적 장애를 격리하여, 시스템 전체로 확장되는 것을 예방할 수 있다.

이러한 이점들을 기대하며 많은 기업들이 MSA를 시도하고 있습니다.

 

MSA의 적용

 

이 글에서는 MSA를 적용하기 위한 3가지 핵심적인 내용에 대해 설명하겠습니다.

  1. 마이크로 서비스 분류
  2. 백엔드 아키텍처
  3. 프론트엔드 아키텍처

1. 마이크로 서비스 분류

마이크로 서비스를 성공적으로 분류하기 위해서는

판단하기 위해 충분한 양과 정확한 비지니스 분석 자료들이 필요합니다.

 

자료 수집의 방법으로는

현업 인터뷰,

AS-IS 시스템 코드를 분석,

오랜기간 비즈니스에 익숙한 개발자 피드백 등의 방법들이 있습니다.

 

충분한 자료가 준비되었다면,

이를 바탕으로 비즈니스를 분리를 시작할 수 있습니다.

주로 도메인 주도 설계(DDD: Domain Driven Design)가 사용됩니다.

 

도메인 주도 설계(DDD: Domain Driven Design)

DDD는 기존의 어플리케이션들이

비즈니스에 대한 이해가 부족한 상태에서 개발되었다는 반성에서 출발했습니다.

그래서 DDD는 데이터가 아닌 비즈니스 서비스가 중심이 되어 설계해야 합니다.

 

첫번째로 해야하는 일은 비즈니스를 도메인들로 분리하는 일 입니다.

도메인의 사전적인 의미는 "집합"으로써,

DDD에서의 도메인은 유사한 비즈니스 업무들의 집합을 의미합니다.

 

사용자에게 방송을 노출시키는 비즈니스를

DDD를 통해 마이크로 서비스를 도출하는 과정을 예시로 들어보겠습니다.

 

 

 

 

 

❗❗❗ 이해를 돕기 위한 간단한 예시일 뿐 ❗❗❗

❗❗❗ 실제로는 더욱 정확하고 자세한 설계가 필요합니다. ❗❗❗

도메인 분류

방송 비즈니스를 위해서 필요한 업무들을 간단하게

편성, 기획, 제작의 최상위 도메인들로 나눠 볼 수 있습니다.

 

 

 

 

 

도메인 이벤트 작성

각각의 도메인들에서 발생하는 이벤트들을 작성합니다.

이벤트들은 Actor가 Action을 해서 발생한 결과이기 때문에 과거형 문장을 사용합니다.

도메인 전문가들이 이벤트들에 대해 피드백을 하고 내용을 보완합니다.

 

 

 

 

 

 

Command 정의

도메인 이벤트들을 발생시키는 Command를 정의합니다.

커맨드 하나에 1개 이상의 Event가 발생할 수 있습니다.

 

 

 

 

 

서브 도메인 분류

비슷한 업무 성격의 이벤트들을 묶습니다. 이렇게 묶여진 이벤트들을 서브 도메인이라고 합니다.

서브 도메인은 3가지 성격으로 나뉩니다.

  1. Core sub domain: 비즈니스 목적 달성을 위한 핵심 도메인
  2. Supporting sub domain: 코어 도메인을 지원하는 도메인
  3. Generic sub domains: 비즈니스에서 공통적으로 사용되는 도메인

 

 

 

 

 

 

entity 도출

커맨드로 발생된 이벤트가 어떤 데이터를 필요로 하는지 기록합니다.

동일한 데이터를 필요로 하거나,

어떤 데이터가 다른 데이터에 포함이 될 수 있다면 하나로 묶습니다.

이렇게 묶여진 데이터를 Entity라고 합니다.

엔티티들은 고유의 비즈니스 목적 수행을 위한 데이터 객체입니다.

엔티티들은 데이터베이스 테이블과 매핑되어 데이터를 CRUD할 수 있습니다.

 

도메인 경계 내부의 모든 엔티티들을 Aggregate라고 합니다.

Aggregate에는 엔티티 뿐만 아니라

엔티티를 지원하는 VO가 존재할 수 있습니다.

 

위 그림의 방송 기획 entity의 경우,

편성된 프로그램 데이터가

엔티티의 멤버 변수 VO의 형태로 표현될 수 있습니다.

 

 

 

 

 

Bounded Context

domain, actor, event, command, entity의 내용들을

고유한 비즈니스 목적별로 그룹핑하는데, 이를 Bounded Context라고 합니다.

바운디드 컨텍스트는 하나 이상의 마이크로 서비스가 될 수 있습니다.

 

 

 

 

 

 

Context Map

 

Shared Kernel

Bounded Context간의 관계를 그린 것을 Context Map이라고 합니다.

Bounded Context 뿐만 아니라 외부 시스템과의 관계도 그릴 수 있습니다.

 

Context Map에서 표현되는 특성은 다음과 같습니다.

  • Shared Kernel: 공통으로 사용하는 Bounded Context간의 관계
  • Upstream-Downstream: Publisher(Upstream)와 Subscriber(Downstream) 관계 
  • Open Host Service: 여러 종류의 Downstream BC를 고려하여 설계하는 Upstream BC
  • Anti Corruption Layer: 다른 BC에서 받는 데이터의 통신 스펙을 변환해주는 모듈을 갖는 BC

 

마이크로 서비스를 어떻게 정의하는지는

프로젝트 전체에 영향을 미치는 아주 중요한 단계입니다.

 

개발 과정이 까다로워져 생산성이 낮아질 수 있고(코드 복잡성 증가),

운영에서 사용하는 자원의 양이 늘어날 수도 있고(마이크로 서비스간의 통신),

에러를 다루는 데에 (화면에서부터 어디 마이크로 서비스에 도달하는가에 대한 과정 파악)

큰 영향을 미치기 때문입니다.

 

비지니스 영역을 잘 구분하여 결합성을 분리하고,

최대한 서로가 독립적인 존재이게끔 해야합니다.

마이크로 서비스를 무조건 작게 쪼개는 것만이 좋은 것은 아닙니다.

분리로 인해 들어가는 불필요한 자원이 너무 많이 투입이 된다면

분리된 마이크로 서비스를 다시 합치는 방향을 고려해봐야합니다.

 

설계된 마이크로 서비스에서 다음과 같은 4가지 항목들을 검토해야 합니다.

 

비지니스 측면에서 얼마나 관련이 있는가?

쇼핑몰에서 주문하기 부분과, 카트에 넣기를 같은 서비스로 넣을 것인지

다른 서비스로 분리할 것인지는 그 비즈니스나 시스템의 특성에 따라 정의되어야 합니다.

 

DB를 분리하였을 때 JOIN을 위해 얼마나 많은 자원이 사용되는가?

하나였던 DB를 분리한 것이기 때문에,

기존에 JOIN을 통해 처리하는 데이터들은 처리하기가 매우 까다롭습니다.

만약 api를 통해 처리한다면 사용되는 자원들을 최소화 하게끔 설계해야 합니다.

 

마이크로 서비스간의 통신을 어떻게 설계할 것인가?

계층을 분리하는 방법,

api를 통해 직접 연결하는 방법,

이벤트 처리를 하는 방법 등이 있습니다.

개발과 운영의 복잡성을 낮추기 위해서 최선의 마이크로 서비스가 설계되어야 합니다.

 

논리적인 계층을 어떻게 가져갈 것인가?

마이크로 서비스가 독립적으로 존재함으로 인해

필요에 의해 여러개의 마이크로 서비스에 접근하는 컨트롤러가 필요한지는

마이크로 서비스의 설계에 따라 결정될 수 있습니다.

 

2. 프론트엔드 아키텍처

프론트엔드 아키텍처는

프로젝트를 하나로 개발하는 방법과 분리하여 개발하는 방법이 있습니다.

마이크로 서비스 VS 마이크로 프론트엔드

마이크로 서비스의 개념을 프론트엔드까지 확장한 것을 마이크로 프론트엔드라고 합니다.

마이크로 서비스마다 프론트엔드 프로젝트를 만들어

컴포넌트를 개발한 후, 화면에서 조립하는 방식입니다.

 

마이크로 프론트엔드를 사용했을 때의 장점은 다음과 같습니다.

  1. 화면 영역까지 서비스를 독립시켰기 때문에, 더 완전한 MSA라고 부를 수 있습니다.
  2. 마이크로 프론트엔드 각각마다 다른 고유한 기술 스택을 사용할 수 있습니다.
  3. 화면을 점진적으로 개발, 분리 배포하는데 수월해집니다.
  4. 마이크로 서비스 팀 조직하는데에 용이해집니다.

 단점은 다음과 같습니다.

  1. 마이크로 프론트엔드간에 너무 많은 종속성을 요구한다면, 빌드/배포 지옥이 될 수 있습니다.
  2. 에러를 추적하는 디버깅 과정이 복잡하여 까다롭습니다.
  3. 컴포넌트의 적절한 개발을 위해, 비즈니스와 프로그램 아키텍처에 대한 깊은 이해가 필요합니다.
  4. 모든 페이지의 UX/UI 일관성이 지켜지지 않을 수 있습니다.

사용자에게 화면을 제공하기 위해서는,

마이크로 서비스 별로 분리되어 개발된 컴포넌트들을 통합하는 과정이 필요합니다.

 

일반적으로 마이크로 애플리케이션을 두고,

그 안에는 하나의 컨테이너를 두어 컴포넌트들을 합쳐 화면을 개발합니다.

 

마이크로 프론트엔드를 통합하기 위해서는 5가지 방법이 있습니다.

 

1. 서버사이드 템플릿 통합

서버 측 템플릿에서 렌더링할 때 컴포넌트들을 조합하는 방법입니다.

서버 사이드 렌더링의 장단점을 그대로 상속받게 됩니다.

분리된 프로젝트를 import하는 과정에서 빌드 시간이 더 소요될 수 있습니다.

 

2. 빌드타임 통합

마이크로 앱의 package.json을 통해 통합하는 방법입니다.

하나의 마이크로 앱에서 종속성을 관리하기 때문에,

중복되는 내용을 줄이고 메모리를 아낄 수 있습니다.

 

그렇지만 마이크로 프론트엔드에서 부분 수정이 일어났을 때,

마이크로 앱 전체를 다시 빌드해야한다는 결합성의 문제가 있습니다.

 

사실상 하나의 마이크로 앱으로 존재한다고 볼 수 있어서

마이크로 프론트엔드 개념에 적합하지는 않습니다.

 

빌드 타임이 아닌 런타임에서 화면을 통합하는 방법을 찾아야 합니다.

 

3. ifram을 통한 런타임 통합

ifram html 태그를 이용하는 방법입니다.

마이크로 프론트엔드들을 ifram 하위 페이지로 만들어 마이크로 앱을 조합할 수 있습니다.

간단하게 구현하기 쉽고 높은 고립성을 갖고 있다는 ifram의 특성이 있습니다.

 

하지만 ifram으로 분리된 페이지기 때문에

컨테이너 마이크로 앱과 컴포넌트 마이크로 프론트엔드가 통신하기 위한 규약이 필요하고,

스타일 적용, 라우팅, 데이터 관리 등의 구현이 복잡해질 수 있다는 단점이 있습니다. 

 

4. Javascript를 통한 런타임 통합

컨테이너 마이크로 앱에

마이크로 프론트엔드 번들을 script 태그를 이용하여 가져오는 방법입니다.

 

ifram과 달리 유연한 통합이 가능하여

현실적으로 가장 많이 사용하는 방법이라고 합니다.

 

5. Web Components를 통한 런타임 통합

마이크로 프론트엔드와 html 커스텀 엘리멘트 통해 웹 컴포넌트를 만들고

마이크로 앱의 Element.appendChild()를 통해 화면을 조합하는 방법입니다.

 

static한 통합도 되고 Javascript를 통한 런타임 통합도 가능합니다.

웹 브라우저 기술 스택을 적극 활용한다면 좋은 방법입니다.

3. 백엔드 아키텍처

서버 계층 구조

DDD를 바탕으로 설계된 마이크로 서비스의

서버를 이루는 기본 아키텍처는 다음과 같습니다.

 

  • Presentation:
    API Controller 역할을 하는 계층입니다. (@Controller, @RestController)

  • Service:
    도메인과 데이터 Class들 간의 제어를 하는 계층입니다. (@Service)

  • Domain Objects:
    도메인 엔티티, 도메인 비즈니스 로직이 구현된 계층입니다. Aggregate와 Logic 계층을 분리하여 설계해야 합니다. (Aggregate, @Service)

  • Data Mapper:
    비즈니스 도메인 엔티티를 데이터베이스 테이블과 매핑시켜주는 계층입니다. (Domain Table Entity)

  • Data Access:
    데이터베이스와의 CRUD를 처리하는 계층입니다. (@Repository)

 

 

JOIN 구현 방법

MSA는 데이터베이스가 분리되어 있습니다.

자기의 마이크로 서비스의 영역을 넘어간다면,

이전의 모놀리식 방식처럼 테이블 JOIN을 통해 요청을 해결할 수 없습니다.

 

이 문제를 해결하는 방법을

편성된 프로그램을 기획하는 CRUD 예시를 통해 설명하겠습니다.

 

1. CREATE, UPDATE, DELETE

 

프로그램과 기획을 JOIN하여 INSERT

방송을 기획하기(기획 테이블에 INSERT) 위해서는 프로그램 데이터가 필요합니다.

하지만 데이터베이스가 분리(편성, 기획)되어 있기 때문에, JOIN을 할 수 없는 상황입니다.

마이크로 앱에서 이를 해결하기 위한 시나리오는 아래와 같습니다.

 

사용자가 프로그램 목록을 요청합니다.

편성 컴포넌트를 통해서 편성 BFF에 프로그램 목록 데이터를 요청합니다.

사용자가 원하는 프로그램을 화면에서 선택합니다.

화면의 기획 컴포넌트에 프로그램 데이터가 입력됩니다. (programId, programName)

 

주의해야 할 점은 편성의 모든 데이터가 아니라,

필요 최소한의 데이터만을 보내야됩니다.

 

사용자는 방송 기획에 필요한 나머지 데이터를 추가 입력하고 저장합니다.

기획 BFF는 요청을 받고 해당 테이블에 Insert 합니다.

 

UPDATE와 DELETE도 비슷하게 처리되며, 메인 BFF에서 트랜잭션을 관리합니다.

 

2. READ

편성 프로그램 기획 리스트 READ

기획 BFF에서 사용자 요청을 받습니다.

편성과 기획의 모듈에 직접 접근하여 데이터 목록을 얻어옵니다.

기획 BFF가 PGM_ID를 통해서 두 영역의 데이터를 조합합니다.

화면에게 조합된 데이터들을 돌려줍니다. 

 

BFF의 크기와 복잡성 증가 문제

하지만 제공해야하는 데이터 양이 많거나

조합 쿼리가 복잡한 경우에는

API 통신만으로 요청을 감당하기 어려울 수가 있습니다.

 

만약 4개의 테이블을 JOIN하기 위해 4개의 마이크로 서비스에 접근해야할 때,

데이터 요청과 조합으로 인해 BFF 코드 로직이 매우 복잡해져

 

결합성이 커져 무거워지고,

가독성이 떨어져 유지보수가 어렵고,

새로운 CRUD를 추가해야 할 때 더욱 어려울 수 있습니다.

 

또한 여러번 접근하는 횟수가 증가하기 때문에

WAS와 데이터베이스에 대한 부하도 증가할 수 있습니다.

 

MSA는 이를 방지하기 위해서

이벤트 활용이 기반이 되는 아래의 방법들을 사용합니다.

 

1. Table 복제

이벤트 스트림을 통한 복제 테이블 INSERT

편성 프로그램 기획 목록이 필요할 때마다 서비스 모듈에 api 요청을 보내는 것이 아니라,

기획 마이크로 서비스 영역에 프로그램 테이블을 복제하는 방법입니다.

복제는 부담이 크기 때문에, 전체가 아닌 필요한 컬럼만으로 구성합니다. 

 

프로그램 테이블에 데이터가 추가될 때마다

편성 BFF가 이벤트를 발행합니다.

 

그럼 이를 구독하고 있는 마이크로 서비스(기획)들이

이벤트를 받아 같은 영역안에 있는 테이블에 똑같이 INSERT 합니다.

 

이제 같은 영역의 데이터베이스이기 때문에,

조합된 데이터를 요청할 경우 데이터베이스의 테이블 JOIN을 통해 해결할 수 있습니다.

 

2. CQRS + Materialized View(구체화된 뷰)

CQRS(Command and Query Responsibility Segregation)

명령(Command)과 조회(Query)의 책임 분리라는 뜻을 갖고 있습니다.

명령은 데이터의 상태를 변화시키고,

조회는 데이터의 상태를 알려줍니다.

책임을 분산시켜 코드의 복잡성과 부하를 줄이는 것이 목적입니다.

 

3가지 방법의 분리가 가능합니디.

  1. 코드의 분리: 
    • JOIN 로직이 복잡한 MSA의 특성상, CRUD가 추가되면 모듈의 복잡성이 증가하고 거대해져서 본래의 객체 목표를 유지하기 어려울 수 있습니다. 이에 Query와 Command 역할 클래스를 분리하여 유지보수 측면에서 이점을 볼 수 있습니다. 
  2. WAS의 분리:
    • 코드의 분리를 했지만 하나의 WAS에서 실행된다면 자원 부하가 몰릴 수 있습니다. 그렇기 때문에 Query용 서버와 Command용 서버를 분리 실행시켜 자원 부하를 분산시킬 수 있습니다.
  3. 데이터베이스 분리:
    • Query는 Command보다 많은 자원을 사용합니다. 데이터베이스 자체를 분리하면 이에대한 자원 부하를 분산시킬 수 있습니다. 또한 Query마다 최적화된 DBMS나 스키마를 사용할 수 있습니다. 분리된 데이터베이스이기 때문에 동기화에 대한 신경을 써줘야 합니다. 이벤트 소싱의 방법으로 해결할 수 있습니다.

 

Materialized View(구체화된 뷰)

데이터베이스를 분리시킬 경우, 구체화된 뷰를 사용할 수 있습니다.

 

마이크로 서비스에 Command 이벤트가 발생했을 때

이벤트 저장소 모듈은 이를 구독하여

JOIN이 된 데이터를 이벤트 저장소에 미리 만들어 놓습니다.

 

 

MSA는 위의 두가지 패턴을 사용하여 READ 문제를 해결합니다.

그 결과로 사용자 측면에서는

복잡하거나 부하가 심한 요청에 대해 빠르게 데이터를 응답받을 수 있습니다.

 

또한 서버 측면에서는

복잡한 JOIN Queary와 Command가 분리되어 코드 복잡성을 줄이고

자원 부하량을 줄일 수 있습니다.