[디자인패턴] Design Patterns for Microservices

8 minute read

  • 이 포스팅은 Rajesh Bhojwani의 Design Patterns for Microservices을 정리한 것입니다.
  • 내용에 대한 조언 및 의견은 작성자에게 큰 도움이 됩니다
  • 모든 저작권과 권리는 Rajesh Bhojwani에 있습니다.

마이크로서비스 아키텍처 도입

마이크로서비스 아키텍처(Microservice architecture)는 사실상 현대의 응용 프로그램 개발 기술로 자리 잡고 있다. 이 기술에는 몇 가지 단점과 부딫혀야 할 많은 문제들이 있고, 마법같이 쉬운 솔루션은 아니다. 그래서 이 기술의 도입으로 인해 발생할 수 있는 문제들의 패턴들을 학습하고, 재사용 가능한 솔루션으로 만들 필요성, 즉, 마이크로서비스를 위한 디자인 패턴이 논의될 필요성이 있다. 그리고 이 디자인 패턴들에 대해 고민하기 전에 우리는 다음의 마이크로서비스 아키텍처가 구축된 원칙들에 대한 이해가 필요하다.

  1. Scalability (확장성)
  2. Availability (가용성)
  3. Resiliency (탄력성)
  4. Independent (독립성), autonomous (자율성)
  5. Decentralized governance (분산된 관리)
  6. Failure isolation (장애 격리)
  7. Auto-Provisioning (자동 프로비저닝)
  8. Continuous delivery through DevOps (DevOps를 통한 지속적 배포)

위의 모든 원칙을 적용하다 보면 몇 가지 문제점들을 마주친다. 아래에서 마주칠 문제들과 해결책을 논의해보자.

1. Decomposition Patterns

a. Decompose by Business Capability (비즈니스 기능 분해)

Problem

마이크로 서비스는 단일 책임 원칙을 적용해서 모든 서비스들을 느슨한 관계로 만든다. 하지만 응용 프로그램을 작은 조각으로 나누는 것은 논리적으로 수행돼야 한다. 어떻게 어플리케이션을 작은 서비스로 분해할 수 있을까?

Solution

한 전략은 비즈니스 기능으로 분리하는 것이다. 가치 창출을 위해 비즈니스가 수행하는 비즈니스 기능들은 비즈니스의 유형에 따라 다를 수 있다. 예를 들어, 보험회사의 기능에는 일반적으로 영업, 마케팅, 가입, 청구 처리, 결제 등이 포함된다. 기술보단 비즈니스 지향적인 기능들을 제외하고, 기술적인 비즈니스 기능들을 각각의 서비스라고 생각할 수 있다.

b. Decompose by Subdomain (하위도메인 분해)

Problem

응용 프로그램을 비즈니스 기능을 사용해서 분해하는 것은 좋은 시작이 될 수 있다. 하지만 분해하기 쉽지 않은 여러 서비스에서 공통적으로 사용되는 (소위 “God Classes”) 영역이 있다. 예를 들어, 주문 클래스는 주문관리, 주문접수, 주문배달 등에 사용된다. 이것들을 어떻게 분리할 수 있을까?

Solution

“God Classes” 같은 문제에 대해서는 DDD(Domain-Driven Design, 도메인 주도 설계)가 해결방안이 될 수 있다. 이 방법은 하위 도메인과 bounded context 개념을 사용한다. DDD는 전체 도메인 모델을 하위 도메인으로 나눈다. 각 하위 도메인에는 모델이 있으며, 해당 모델의 범위를 bounded context라고 한다. 각 마이크로 서비스는 bounded context 중심으로 개발될 것이다.

NOTE: 하위 도메인 식별은 쉬운 작업이 아니다. 이는 비즈니스에 대한 이해를 요구한다. 비즈니스 기능과 마찬가지로 하위 도메인은 비즈니스와 조직 구조를 분석하고 전문 영역을 분석하여 식별될 수 있다.

c. Strangler Pattern

Problem

지금까지 우리는 새로 개발되는 어플리케이션에 대한 분해 디자인 패턴에 대해 이야기했다. 하지만, 우리가 수행하는 작업의 80%는 기존에 개발된 큰 모놀리식 어플리케이션이다. 라이브로 사용되고 있는 서비스를 작은 코드로 분할하는 것은 큰 작업이기 때문에, 위에서 다룬 디자인 패턴들을 적용하는 것은 어려울 것이다.

Solution

Strangler 패턴이 해결방안이 될 수 있다. Strangler 패턴은 감싸 인 나무를 질식시키는 포도나무에 비유해서 만들어졌다. 이 방법은 웹 어플리케이션에서 잘 적용되는데, 각 URI 호출마다 한 서비스는 여러 도메인으로 분리될 수 있고, 별도의 서비스로 호스팅 될 수 있다. 이 아이디어는 한 번에 하나의 도메인을 수행하는 것이다. 동일한 URI 공간에 나란히 두 개의 서로 다른 응용 프로그램을 만들고, 새롭게 리팩터링 된 응용 프로그램은 최종적으로 모놀리식 어플리케이션을 종료할 수 있을 때까지 점진적으로 대체한다.

2. Integration Patterns

a. API Gateway Pattern

Problem

어플리케이션이 작은 마이크로서비스로 분해 될 때, 몇 가지 해결해야할 문제가 있다.

  1. 어떻게 제공자 정보를 추상화해서 여러 마이크로서비스를 호출할것인가?
  2. 다른 채널(데스크톱, 모바일 및 태블릿)에서 UI가 다를 수 있으므로, 같은 백엔드 서비스에서 다른 데이터가 필요할 수 있다.
  3. 다른 소비자는 reusable한 마이크로서비스를 통해 다른 형식의 응답을 필요로 할 수 있다. 누가 데이터 변환이나 필드 조작을 할것인가?
  4. 제공자 마이크로서비스가 지원하지 않을수 있는 여러 유형의 프로토콜을 어떻게 처리할 수 있을까?

Solution

API 게이트웨이는 위의 문제들 뿐만 아니라, 마이크로서비스 구현에서 발생하는 많은 문제들을 해결하는데 도움을 준다.

  1. API 게이트웨이는 모든 마이크로서비스 호출에 대한 단일 진입점이다.
  2. 해당 마이크로서비스 요청을 제공자의 세부사항을 추상화하여 라우트하는 프록시 서비스 처럼 작동할수 있다.
  3. 한 요청을 여러 서비스로 fan out 하고, 결과를 aggregate 하여 소비자에게 보낼수 있다.
  4. 두루 적용되도록 만든 API는 모든 소비자의 요구 사항을 해결할 수 없다. API 게이트웨이는 각 특정 유형의 클라이언트에 대해 세분화 된 API를 작성할 수 있다.
  5. 제공자와 소비자가 처리 할 수 있도록 프로토콜 요청 (e.g. AMQP)을 다른 프로토콜 (e.g. HTTP)로 또는 그 반대로 변환 할 수 있다.
  6. 마이크로서비스의 인증/권한 책임을 줄일 수 있다.

b. Aggregator Pattern

Problem

우리는 API 게이트웨이 패턴에서 aggregate 데이터 문제를 해결하는 방법에 대해서 이야기했다. 여기서는 그 문제에 대해 전반적으로 살펴볼 것이다. 비즈니스 기능을 몇 개의 작은 논리적 코드로 분해할 때, 각 서비스에서 반환하는 데이터를 합치는 방법을 생각해 봐야 한다. 이 책임은 소비자에게 있을 수 없으므로, 제공자 어플리케이션의 내부 구현을 이해해야 할 수도 있다.

Solution

Aggregator 패턴은 이 문제를 해결하는 데 도움이 된다. 다른 서비스의 데이터를 aggreate 하고, 최종 응답을 소비자에게 보내는 방법에 대해 설명한다. 이것은 두 가지 방법으로 수행할 수 있다.

  1. composite 마이크로서비스는 필요한 모든 마이크로서비스를 호출하고 데이터를 통합하고 데이터를 변환하여 전송한다.
  2. 또한 API 게이트웨이는 요청을 여러 마이크로서비스로 분할하고 데이터를 aggregate 해서 소비자에게 전송할 수 있다.

비즈니스 로직을 적용한 이후에 composite 마이크로서비스를 선택하는 것이 좋다. 그렇지 않다면 API 게이트웨이가 안정된 솔루션이다.

c. Client-Side UI Composition Pattern

Problem

비즈니스 기능 또는 서브도메인을 분해하여 서비스를 개발할 때, UX를 담당하는 서비스는 여러 마이크로서비스에서 데이터를 가져와야 한다. 기존 모놀리식에서는, 데이터를 검색하고 UI 페이지를 Refresh/Submit 하기 위해 백엔드 서비스를 한 번만 호출했다. 하지만 마이크로서비스에서는 그렇지 않을 것이고, 어떻게 데이터를 가져오는지 이해가 필요하다.

Solution

마이크로서비스에서는 UI가 화면/페이지에서 여러 섹션/영역이 있는 형태로 설계되어야 한다. 각 섹션은 개별 백엔드 마이크로서비스를 호출하여 데이터를 가져온다. 이는 서비스와 관련된 UI 컴포넌트를 구성하는 것이고, Angular 및 React와 같은 프레임워크를 사용하면 쉽게 해결할 수 있다. 그리고 이러한 화면을 Single Page Applications (SPA) 라고 부른다. 이렇게 하면, 앱이 전체 페이지 대신 화면의 특정 영역을 새로 고칠 수 있게 된다.

3. Database Patterns

a. Database per Service

Problem

마이크로서비스에서 어떻게 데이터베이스 아키텍처를 정의하는가에 대한 문제에서, 해결해야 할 사항들은 다음과 같다.

  1. 서비스들이 느슨하게 결합되어야 한다. 이들은 독립적으로 개발, 배치 및 확장될 수 있다.
  2. 비즈니스 트랜잭션은 여러 서비스 사이에서 변함없이 실행돼야 한다.
  3. 일부 비즈니스 트랜잭션은 여러 서비스가 소유하는 데이터를 쿼리 해야 한다.
  4. 데이터베이스는 scale을 위해 replicated 되거나, sharded 돼야 한다.
  5. 다른 서비스가 다른 데이터 스토리지 요구 사항을 갖는다.

Solution

위의 문제를 해결하려면, 마이크로서비스 당 하나의 데이터베이스를 설계해야 한다. 데이터베이스는 해당 서비스 전용이어야 하고, 마이크로서비스 API를 통해서만 접근할 수 있도록 한다. 다른 서비스에서 직접적으로 접근할 수는 없어야 한다. 예를 들어, 관계형 데이터베이스의 경우 서비스마다 서로 각각의 테이블이나 스키마 또는 데이터베이스 서버를 사용하도록 할 수 있다. 각 마이크로서비스는 접근이 분리된 데이터베이스 id를 가져야 하고, 다른 서비스 테이블로부터의 접근을 차단할 수 있도록 한다.

b. Shared Database per Service

Problem

우리는 한 서비스당 하나의 데이터베이스가 마이크로서비스에 이상적이라고 이야기 했다. 이것은 어플리케이션이 개발 초기이고, DDD로 개발될 때 가능하다. 하지만 만약 어플리케이션이 모놀리식한 상황에서 마이크로서비스로 쪼개는걸 시도한다면, denormalization이 쉽지 않을 것이다. 이 경우 적합한 아키텍처는 무엇일까?

Solution

여러 서비스들이 공유하는 데이터베이스는 이상적이지는 않지만, 위의 시나리오에서 작동하는 솔루션이다. 대부분의 사람들은 이것을 마이크로서비스의 anti-pattern 이라고 생각하지만, 기존에 개발된 어플리케이션의 경우에 이것은 어플리케이션을 더 작은 논리적 조각으로 나눌 수 있는 좋은 시작이 될것이다. 만약 개발 초기라면, 이 방법을 적용하지 말아야 한다. 이 패턴에서 하나의 데이터페이스는 하나 이상의 마이크로서비스와 결합될 수 있다. 그리고 확장성, 자율성 그리고 독립성을 이루기 위해 서비스는 최대 2~3개로 제한되어야 한다.

c. Command Query Responsibility Segregation (CQRS)

Problem

서비스당 데이터베이스를 갖는 구조로 구현을 하고 나면, 여러 서비스의 공동 데이터를 필요로 하는 하는 쿼리가 요구될 수 있지만, 이것은 불가능하다. 어떻게 마이크로서비스 아키텍처에서 쿼리들을 구현할 수 있을까?

Solution

CQRS는 어플리케이션을 커맨드, 쿼리 두개의 파트로 분할하는 것을 제안한다. 커맨드 파트는 생성, 업데이트, 삭제 요청을 담당하고, 쿼리 파트는 Materialized view를 사용하여 쿼리 부분을 처리한다. 이벤트 소싱 패턴이 주로 데이터 모든 변경에 대한 이벤트를 만들때 CQRS와 함께 사용된다. Materialized view는 이벤트 스트림의 구독을 통해 갱신된 상태로 유지한다.

d. Saga Pattern

Problem

각 서비스에 자체 데이터베이스가 있고 비즈니스 트랜잭션이 여러 서비스에 걸쳐있는 경우 서비스간에 데이터 일관성을 어떻게 보장할 수 있을까? 예를 들어 고객이 신용 한도가 있는 e-commerce 어플리케이션의 경우 응용프로그램은 새 주문이 고객의 신용 한도를 초과하지 않도록 해야한다. 이때, Orders와 Customers는 다른 데이터베이스에 있기 때문에 어플리케이션은 단순히 로컬 ACID 트랜잭션을 사용할 수 없다.

Solution

여러 서비스에 걸쳐있는 각 비즈니스 트랜잭션을 하나의 saga로 구현할 수 있다. saga는 일련의 로컬 트랜잭션이며, 각 로컬 트랜잭션은 데이터베이스를 업데이트하고 다음 로컬 트랜잭션을 트리거하는 메시지 또는 이벤트를 publish한다. 비즈니스 규칙을 위반하여 로컬 트랜잭션이 실패한 경우, saga는 이전 로컬 트랜잭션이 수행 한 변경 사항을 실행 취소하는 일련의 보상 트랜잭션을 실행한다. saga는 다음의 두 가지 방법으로 구현할 수 있다.

  1. Choreography - 각 서비스는 다른 서비스에게 이벤트를 publish하고, 응답을 받고, 어떤 행동을 할지 결정한다.
  2. Orchestration - 오케스트레이터 객체가 SAGA의 의사 결정 및 비즈니스 로직 시퀀싱에 대한 책임을 갖는다.

4. Observability Patterns

a. Log Aggregation

Problem

여러 머신에서 돌고 있는 여러 서비스의 인스턴스들로 이루어진 어플리케이션의 경우를 생각해보자. 요청은 일반적으로 여러 서비스 인스턴스에 걸쳐 있고, 각 서비스 인스턴스는 표준화된 형식으로 로그 파일을 생성한다. 우리는 어떻게 각각의 요청에 대한 로그를 가지고 어플리케이션의 동작을 이해할 수 있을까?

Solution

사용자가 로그를 검색하고 분석할 수 있는, 각 서비스 인스턴스의 로그를 집계하는 중앙 집중식 로깅 서비스가 필요하다. 이 서비스는 특정 메시지가 로그에 나타날 때 트리거되는 경고를 구성할 수 있다. 예를 들어 PCF에서의 Loggregator와 AWS Cloud Watch 같은 서비스가 있다.

b. Performance Metrics

Problem

마이크로서비스 아키텍처로 인해 서비스 포트폴리오가 많아지면, 문제가 발생했을 때의 패턴이 모니터링되고 경고를 전송할 수 있도록 트랜잭션을 감시하는 것이 중요해진다. 어플리케이션의 성능 모니터링을 위한 metrics를 어떻게 수집할 수 있을까?

Solution

메트릭 서비스는 개별 작업에 대한 통계를 수집하는 데 필요하고, 보고 및 경고를 제공하는 응용 프로그램 서비스의 메트릭을 집계한다. 측정 항목을 집계하는 데는 두 가지 모델이 있다.

  1. Push - 서비스는 메트릭을 메트릭 서비스에 푸시 한다. ex) NewRelic, AppDynamics
  2. Pull - 측정 항목 서비스에서 측정 항목을 가져온다. ex) Prometheus

c. Distributed Tracing

Problem

마이크로서비스 아키텍처에서 요청은 종종 여러 서비스에 걸쳐 있다. 그리고 각 서비스는 여러 서비스에서 하나 이상의 작업을 수행하여 요청을 처리한다. 우리는 문제를 트러블슈팅하기 위해 어떻게 엔드 투 엔드 요청을 추적할 수 있을까?

Solution

우리는 다음의 서비스가 필요하다.

  1. 각 외부 요청에 unique id를 할당한다.
  2. unique id가 모든 서비스를 통과한다.
  3. unique id를 모든 로그 메시지에 포함한다.
  4. request와 operation에 대한 정보를 중앙화된 서비스에 기록한다.

Spring Cloud Sleuth와 Zipkin server가 일반적인 구현된 솔루션이다.

d. Health Check

Problem

마이크로서비스 아키텍처를 구현할 때, 서비스는 가동 중이지만 트랜잭션을 처리하지 못할 가능성이 있다. 이 경우, 어떻게 요청이 실패한 인스턴스로 전달되지 않도록 할 수 있을까?

Solution

각 서비스에는 /health와 같이 응용 프로그램의 상태를 확인하는 데 사용할 수 있는 endpoint가 있어야 한다. 그리고 이 API는 호스트의 상태, 다른 서비스/인프라에 대한 연결 및 로직 등을 체크해야 한다.

5. Cross-Cutting Concern Patterns

a. External Configuration

b. Service Discovery Pattern

c. Circuit Breaker Pattern

d. Blue-Green Deployment Pattern