2장 분해 전략
2.1 마이크로서비스 아키텍처란 무엇인가?
- 소프트웨어 아키텍처는 구성 요소 및 디펜던시로 엮인 고수준 구조물
- 아키텍처는 다차원적이므로 기술하는 방법도 다양하다.
- MSA는 관리성, 테스트성, 배포성이 높은 애플리케이션을 구축하는 아키텍처 스타일
2.1.1 소프트웨어 아키텍처의 정의와 중요성
소프트웨어 아키텍처의 정의
- 소프트웨어 아키텍처 문서화, 바스(Bass) 등
컴퓨팅 시스템의 소프트웨어 아키텍처는 소프트웨어 엘리먼트와 그들 간의 관계, 그리고 이 둘의 속성으로 구성된 시스템을 추론하는 데 필요한 구조의 집합이다.
- 즉 핵심은 여러 파트로의 분해와 이 파트 간의 관계라는 것
- 업무와 지식을 분리. 덕분에 전문 지식을 보유한 사람들이 함께 생산적으로 애플리케이션 작업이 가능
- 소프트웨어 엘리먼트가 어떻게 상호 작용하는지 밝힘
소프트웨어 아키텍처 4+1 뷰 모델
- 필립 크러첸이 발표한 논문
- 4+1 뷰 모델은 소프트웨어 아키텍처를 바라보는 상이한 4뷰를 정의한다.
- 논리 뷰: 개발자가 작성한 소프트웨어 엘리먼트. 클래스, 패키지 그리고 상속, 연관, 의존 등의 관계를 뜻함
- 구현 뷰: 빌드 시스템의 결과물.
- 모듈(패키징된 코드)과 컴포넌트(하나 이상의 모듈로 구성된 실행/배포 가능 단위)로 구성
- 자바에선 보통 JAR
- 모듈 간 디펜던시와 컴포넌트/모듈 간 조합 관계도 포함
- 프로세스 뷰: 런타임 컴포넌트.
- 각 엘리먼트는 개별 프로세스고 IPC는 프로세스 간 관계를 나타낸다.
- 배포 뷰: 프로세스가 머신에 매핑되는 방법.
- 4뷰 외에도 뷰를 구동시키는 ‘시나리오’가 있다.
- 각 시나리오는 특정 뷰 내에서 얼마나 다양한 요소가 협동하여 요청을 처리하는지 기술
아키텍처의 중요성
- 애플리케이션 요건은 크게 두 가지 존재
- 애플리케이션이 해야 할 일을 정의한 기능 요건
- 유스케이스, 사용자 스토리
- 기능 요건은 어느 아키텍처에서든 구현 가능
- ‘~성’으로 끝나는 서비스 품질 요건
- 확장성, 신뢰성 등
- 아키텍처 선택에 따라 만족 가능한 품질 요건이 달라짐
2.1.2 아키텍처 스타일 개요
- 애플리케이션은 대부분 아키텍처 스타일을 조합해서 사용한다.
- 모놀리식은 구현 뷰를 하나의 컴포넌트로 구성한 스타일
- MSA는 애플리케이션을 느슨하게 결합된 여러 서비스로 구성하는 스타일
계층화 아키텍처 스타일
- 계층화 아키텍처
- 소프트웨어 엘리먼트를 계층별로 구성
- 계층 간 디펜던시는 아키텍처로 제한
- 3계층 아키텍처
- 표현(프레젠테이션) 계층: 사용자 인터페이스, 외부 API가 구현된 계층
- 비즈니스 로직 계층: 비즈니스 로직이 구현된 계층
- 영속(퍼시스턴스) 계층: DB 상호 작용 로직이 구현된 계층
- 계층화 아키텍처의 단점
- 표현 계층이 하나뿐이다.
- 영속화 계층이 하나뿐이다.
- 비즈니스 로직을 계층을 영속화 계층에 의존하는 형태로 정의한다.
육각형 아키텍처 스타일
- 육각형 아키텍처
- 논리 뷰를 비즈니스 로직 중심으로 구성
- 표현 계층 대신 어댑터를 둔다.
- 외부 요청을 처리하는 인바운드 어댑터(들)
- 외부 애플리케이션을 호출하는 아웃바운드 어댑터(들)
- 비즈니스 로직이 어댑터에 전혀 의존하지 않고 어댑터가 비즈니스 로직을 의존
- 비즈니스 로직에는 하나 이상의 포트가 존재
- 비즈니스 로직이 표출된 API로서의 인바운드 포트
- 비즈니스 로직이 외부 시스템을 호출하는 방법에 관한 아웃바운드 포트
- 인바운드 어댑터는 외부 요청을 인바운드 포트를 호출해 처리
- 아웃바운드 어댑터는 비즈니스 로직에서 들어온 요청을 애플리케이션/서비스를 호출해서 처리
- 육각형 아키텍처의 장점
- 비즈니스 로직이 표현/데이터 접근 로직 어디에도 의존하지 않는다.
- 비즈니스 로직만 따로 테스트하기 쉽다.
- 육각형 아키텍처는 MSA를 다루는 각 서비스 아키텍처를 기술하는 가장 좋은 방법
2.1.3 마이크로서비스 아키텍처는 일종의 아키텍처 스타일이다
- 모놀리식 아키텍처
- 구현 뷰를 단일 컴포넌트(하나의 실행 파일)로 구성한 아키텍처 스타일
- 모놀리식은 육각형 아키텍처 방식으로 구성한 논리 뷰를 가질 수 있다.
- 마이크로서비스 아키텍처
- 구현 뷰를 다수의 컴포넌트로 구성
- 컴포넌트는 곧 서비스이며 각 서비스는 자체 논리 뷰를 가진다. (전형적인 육각형 아키텍처)
- 커넥터는 각 서비스가 협동할 수 있게 해주는 통신 플토콜
서비스란 무엇인가
- 서비스는 기능이 구현되어 단독 배포가 가능한 소프트웨어 컴포넌트
- 서비스 API는 내부 구현 상세를 캡슐화한다.
- API를 우회하여 서비스에 접근할 수 없기에 MSA에서 서비스 모듈성이 보장됨
- 각 마이크로서비스는 자체 아키텍처와 기술 스택을 독자적으로 구축 가능
- 작업 어댑터는 API 요청을 받고 비즈니스 로직을 호출
- 비즈니스 로직 호출 후 이벤트 어댑터가 이벤트를 발행
- 서비스마다 독립적 배포가 가능해야 한다.
느슨한 결합
- 서비스는 클라이언트에 영향을 주지 않고 내부 구현을 변경할 수 있다.
- 느슨한 결합은 유지보수성, 테스트성, 생산성을 높여준다.
- DB도 서비스마다 다르게 사용하기에 스키마 변경, 락 획득 등과 같은 사건에 다른 팀으로부터 자유롭다.
- 다만 여러 서비스에 걸쳐 데이터를 쿼리할 때 복잡해진다.
공유 라이브러리 역할
- 코드 중복 방지를 위해 재사용 가능한 라이브러리(모듈)로 패키징하는 것은 당연한 일
- 다만 서비스 간 의도치 않은 결합도를 유발할 가능성이 있다.
- 중복을 통합했다가 한 서비스에서만 그 기능을 다르게 써야한다면?
- 바뀔 일이 거의 없는 기능은 라이브러리에 담아 쓰는 것이 좋긴 하다.
서비스 규모는 별로 중요하지 않다
- 마이크로서비스는 크기보다 다음을 고려해야 한다.
- 작은 팀이 가장 짧은 시간에
- 다른 팀과 협동하는 부분은 최소로
- 개발 가능한 서비스
2.2 마이크로서비스 아키텍처 정의
- 애플리케이션 아키텍처를 정의하는 3단계 프로세스가 존재
- 시스템 작업 식별
- 애플리케이션 요건을 핵심 요청으로 추출
- 특정 기술이 아닌 추상적인 관념으로 생각
- 데이터를 업데이트하는 커맨드나 조회하는 쿼리
- 서비스 식별
- 어떻게 여러 서비스로 분해할지 결정
- 비즈니스 능력에 따라 혹은 DDD의 하위 도메인별로 서비스를 구성할 수 있다.
- 어떤 전략이든 기술 개념이 아닌 비즈니스 개념 중심으로 이루어진 서비스들이어야 함
- 서비스 API 및 협동 정의
- 1단계에서 식별된 시스템 작업을 각 서비스에 배정
- 여러 서비스가 협동하는 방식을 결정
- 분해 과정에는 장애물이 많다.
- 네트워크 지연
- 서비스 간 동기 통신으로 인한 낮은 가용성
- 여러 서비스에 걸친 데이터 일관성
- 애플리케이션 도처에 숨어 있는 만능 클래스
2.2.1 시스템 작업 식별
- 애플리케이션 아키텍처를 정의하는 첫 단추는 시스템 작업을 정의하는 일
- 시스템 작업은 주로 사용자 스토리 동사에서 도출
- 시스템 작업을 기술하기 위해 고수준 도메인 모델이 필요
- 도메인 모델은 주로 사용자 스토리 명사에서 도출
고수준 도메인 모델 생성
<주문하기 스토리>
전체(Given)
소비자가 있다.
음식점이 있다.
음식점은 소비자의 주소로 제시간에 음식을 배달할 수 있다.
주문 총액이 음식점 최소 주문량 조건에 부합한다.
조건(When)
소비자가 음식점에 음식을 주문한다.
결과(Then)
소비자 신용카드가 승인된다.
주문이 PENDING_ACCEPTANCE 상태로 생성된다.
생성된 주문이 소비자와 연관된다.
생성된 주문이 음식점과 연관된다.
- 위 사용자 스토리를 통해
Consumer
, Order
, Restaurant
, CreditCard
등 클래스를 도출할 수 있다.
시스템 작업 정의
- 시스템 작업은 크게 두 종류로 나뉜다.
- 커맨드: 데이터 생성, 수정, 삭제
- 쿼리: 데이터 읽기
- 시스템 커맨드를 식별하려면 사용자 스토리에 포함된 동사를 분석한다.
- 주요 시스템 커맨드 예시
액터 |
스토리 |
커맨드 |
Consumer |
주문 생성 |
createOrder() |
Restaurant |
주문 접수 |
acceptOrder() |
Courier |
배달 픽업 |
noteDeliveryPickedUp() |
- 커맨드는 매개변수, 반환값, 동작 방식 명세를 도메인 모델 클래스로 정의한다.
작업 |
createOrder(소비자 ID, 결제 수단, 배달 주소, 배달 시각, 음식저 ID, 주문 품목) |
반환 값 |
orderId, … |
선행 조건 |
소비자가 존재하고 주문할 수 있다, 주문 품목은 음식점 메뉴 항목에 들어 있다, 배달 주소/시각은 음식점에서 서비스할 수 있다. |
후행 조건 |
소비자 신용카드는 주문 금액만큼 승인 처리되었다, 주문은 PENDING_ACCEPTANCE 상태로 생성되었다. |
- 커맨드 외에도 사용자에게 필요한 정보를 UI에 제공해야 한다.
findAvailableRestaurants(deliveryAddress, deliveryTime)
: 장소, 시간으로 배달 가능 음식점 목록 조회
findRestaurantMenu(id)
: 메뉴 항목 등 음식점 정보 조회
- 쿼리는 성능이 중요
2.2.2 서비스 정의: 비즈니스 능력 패턴별 분해
- MSA 구축하는 첫 전략은 비즈니스 능력에 따라 분해하는 것
- ex) 보험 회사라면 증권 인수, 클레임 관리, 과금, 컴플라이언스 등의 능력
- ex) 온라인 쇼핑몰이면 주문 관리, 재고 관리, 선적 등의 능력
- 비즈니스 능력은 곧 조직이 하는 일이다
- 비즈니스 능력을 보면 그 조직의 비즈니스를 알 수 있다.
- 비즈니스 방법은 변하기도 하지만 비즈니스 능력은 크게 달라지지 않는다.
비즈니스 능력 식별
- 한 조직의 비즈니스 능력은 조직의 목표, 구조, 비즈니스 프로세스를 분석하여 식별한다.
- 비즈니스 능력 명세는 입력, 출력, SLA 등 다양한 컴포넌트로 구성된다.
- 비즈니스 능력은 특정 비즈니스 객체에 집중하며 여러 하의 능력으로 분해할 수 있다.
- ex) 클레임 비즈니스 객체는 클레임 관리 능력 중심으로 클레임 정보 관리, 클레임 검토, 클레임 지불 등의 하위 능력이 있다.
- FTGO 비즈니스 능력 도출
- 공급자 관리
- 소비자 관리: 소비자 정보 관리
- 주문 접수 및 이행
- 주문 관리
- 음식점 주문 관리
- 로지스틱스
- 배달원 가용성 관리
- 배달 관리
- 회계
비즈니스 능력을 여러 서비스로
- 비즈니스 능력 식별 후 능력에 따라 또는 연관된 능력 그룹에 따라 서비스를 정의한다.
- 최상위 능력이 서비스로 바로 매핑할 수도 있다.
- 하위 능력이 각 서비스로 매핑될 수도 있다.
- 능력 체계 어느 수준(level)을 서비스에 매핑할지 판단할지는 주관적
- 위 그림은 아키텍처 첫 번째 버전에 불과하고 도메인을 더 알수록 더 정교하게 발전한다.
- 가령 IPC가 너무 잦아 분해가 비효율적이라 재결합하는 경우도 있다.
2.2.3 서비스 정의: 하위 도메인 패턴별 분해
- DDD (Domain-driven Design)
- 객체 지향 도메인 모델 중심의 복잡한 소프트웨어 애플리케이션을 구축하는 방법
- DDD에는 MSA에 적용하면 유용한 개념이 존재
- 하위 도메인(sub-domain)
- 경계 컨텍스트(bounded context)
- DDD는 도메인을 구성하는 각 하위 도메인마다 도메인 모델을 따로 정의한다.
- 부서마다 상이한 개념을 동일한 용어로 표현하면서 도메인 모델이 혼란을 일으킬 때가 있다.
- 하위 도메인은 비즈니스 능력과 같은 방법으로 식별하므로 능력과 유사한 하위 도메인이 도출된다.
- ex) 주문 접수, 주문 관리, 주방 관리 등
- 도메인 모델의 범위를 경게 컨텍스트라고 한다.
- MSA에 DDD를 적용하면 각 서비스가 경계 컨텍스트가 된다.
- MSA의 서비스 자율 팀 개념은 도메인 모델을 개별 팀이 소유/개발한다는 DDD와 어울린다.
- 자체 도메인 모델을 가진 하위 도메인 개념 덕에 만능 클래스를 제거하고 서비스 분해가 쉬워진다.
2.2.4 분해 지침
- 비즈니스 능력, 하위 도메인에 ㄸ른 분해는 MSA를 정의하는 주요 수단
- 이 외에도 객체 지향 설계에 근거한 두 가지 원칙이 존재
- 단일 책임 원칙 (SRP, Single Responsibility Principle)
- 공동 폐쇄 원칙 (CCP, Common Closure Principle)
단일 책임 원칙
클래스는 오직 하나의 변경 사유를 가져야 한다. - 로버트 C. 마틴
- MSA에 SRP를 적용하면 하나의 책임만 가진 작고 응집된 서비스를 정의할 수 있다.
- FTGO의 경우 주문 접수부터 주문 준비, 배달에 이르기까지 하나하나 모두 개별 서비스가 맡아 처리한다.
공동 폐쇄 원칙
패키지 클래스들은 동일한 유형의 변경에 대해 닫혀 있어야 한다. 패키지에 영향을 주는 변경은 그 패키지에 속한 모든 클래스에 영향을 끼친다. - 로버트 C. 마틴
- 즉 어떤 두 클래스가 동일한 사유로 맞물려 변경되면 동일한 패키지에 있어야 한다는 것
- CCP를 적용한 MSA에선 동일한 사유로 변경되는 컴포넌트를 모두 같은 서비스로 묶을 수 있다.
- 가능하면 변경 영향도를 정확히 한 팀, 한 서비스에 국한시키는 것이 좋다.
2.2.5 서비스 분해의 장애물
- 네트워크 지연
- 늘어나는 서비스 간 왕복 네트워크 통신 횟수 급증
- 한 번에 여러 객체를 가져오는 배치 API를 구현하여 지연 시간을 줄임
- 비싼 IPC를 언어 수준 메서드나 함수로 호출하여 지연 시간을 줄임
- 동기 통신으로 인한 가용성 저하
- 동기 통신의 경우 타 서비스 중 하나라도 불능일 경우 로직을 진행시킬 수 없다.
- 비동기 메시징으로 강한 결합을 제거할 수 있다.
- 여러 서비스에 걸쳐 데이터 일관성 유지
- 과거에는 2PC (2단계 분산 트랜잭션)을 많이 사용
- 최근에는 사가 패턴으로 트랜잭션을 관리
- 데이터의 일관된 뷰 확보
- 여러 DB에 걸친 일관된 데이터 뷰 확보가 어렵다.
- 어떤 데이터를 일관된 뷰로 바라보려면 한 서비스 내부에 두어야 하는데 이는 분해의 걸림돌
- 다행히 실제로 이는 거의 문제가 되지 않음
- 분해를 저해하는 만능 클래스
- 애플리케이션 곳곳의 만능 클래스는 그 자체로 분해의 걸림돌
- 만능 클래스에는 애플리케이션 여러 측면에 관한 비즈니스 로직이 존재
- FTGO의 경우 Order 클래스가 만능이다.
- 주문을 배달하는 것이 시스템 목표이므로 거의 모든 시스템이 주문과 연관
- 가장 좋은 방법은 DDD를 적용하여 각 서비스를 자체 도메인을 가지는 하위 도메인으로 취급하는 것
- 즉 주문과 연관된 서비스는 모두 각자 버전의 Order 클래스를 가진 도메인 모델을 따로 두는 것
2.2.6 서비스 API 정의
- 서비스 API 작업은 둘 중 하나
- 외부 클라이언트 또는 타 서비스가 호출하는 시스템 작업
- 서비스 간 협동을 지원하기 위해 만든 작업
- 서비스 이벤트는 주로 타 서비스와 협동하기 위해 발행
- 이벤트로 사가를 구현
- 이벤트로 CQRS 뷰를 업데이트 하기도 함
- 이벤트로 외부 클라이언트에 알림을 보내는 용도로도 사용
- 서비스 API를 정의하려면
- 각각의 시스템 작업을 서비스로 매핑
- 어느 서비스가 서로 협동해야 하는지 파악
- 협동이 필요하다면 어느 서비스가 어느 API를 타 서비스에 제공할지도 결정