[책 내용 정리] 마이크로서비스 패턴

June·2021년 10월 29일
2

책 요약 및 정리

목록 보기
4/6

1장

모놀리식 아키텍처 단점

  • 너무 복잡하다. 버그를 고치고 새 기능을 정확하게 구현하는데 어렵다.
  • 개발이 더디다.
  • 커밋부터 배포까지 험난하다
  • 확장하기 어렵다.
    • 데이터 용량이 큰 음식점 데이터는 인메모리 db형태로 저장하는데, 이미지 처리 모듈은 cpu 코어 수가 많은 서버에 배호하는 것이 최적이다. 서버 구성 시 리소스 배분을 신경 써야 한다.
  • 모놀리스는 확실하게 전달하기 어렵다.
  • 갈수록 한물간 기술 스택에 발목이 잡힌다.

마이크로서비스 아키텍처가 답이다

확장큐브는 애플리케이션을 확장하는 세 가지 방법. x축 확장은 동일한 다중 인스턴스에 들어온 요청을 부하 분산. z축 확산은 요청의 속성에 따라 요청을 라우팅. y축 확장은 애플리케이션을 기능에 따라 서비스로 분해.

x축 확장은 일반적인 모놀리식 애플리케이션의 확장 수단. 부하 분산기 뒷면에 애플리케이션 인스턴스를 N개 띄어 놓고 들어온 요청을 인스턴스에 고루 분대

z축 확장: 모놀리식 애플리케이션의 다중 인스턴스를 실행하는 것은 x축 확장과 같지만, 인스턴스별로 주어진 데이터 하위 집합만 처리하도록 설정. 인스턴스 앞면에 위치한 라우터가 요청의 속성에 알맞은 인스턴스로 요청을 라우팅한다. 한 인스턴스는 userId a-h까지, 그 다음 인스턴스는 i~p까지 이런식이다.

x/z확장은 애플리케이션 능력과 가용성은 개선되지만, 점점 복잡해진다.

마이크로 서비스 아키텍처는 하나의 애플리케이션을 여러 서비스로 기능 분해하는 것이다.

마이크로서비스는 모듈성을 갖고 있다.

각 서비스는 다른 서비스가 함부로 규칙을 어기고 침투하지 못하게 API라는 경계선을 갖고 있다.

서비스마다 DB가 따로 있다.

개발 단계에서 다른 서비스 개발자와 협의하지 않고 스키마 변경 가능. 다른 서비스가 DB락을 획득해 내 서비스를 블로킹하는 일은 없다.

FTGO 마이크로서비스 아키텍처

퍼사드(관문) 역할을 하는 API 게이트웨이는 소비자, 배달원의 모바일 앱이 접속하는 REST API를 제공한다.

마이크로서비스 애플리케이션은 대게 가벼운 오픈 소스 기술을 사용하며, 메시지 브로커나 REST 또는 gRPC처럼 가벼운 프로토콜 위주의 덤 파이프를 통해 서비스간 통신

마이크로서비스 아키텍처의 장단점

장점

  • 크고 복잡한 애플리케이션 지속적 전달/배포
  • 서비스 규모가 작아 관리 쉬움
  • 서비스 독립적 배포/확장
  • 팀이 자율적으로 움직임
  • 결함 관리가 잘됨
  • 새로운 기술을 실험하고 도입하시 쉽다.

단점

  • 딱 맞는 서비스를 찾기가 쉽지 않다
  • 분산 시스템은 복잡해서 개발, 테스트, 배포가 어렵다
  • 여러 서비스에 걸친 기능을 배포할 때는 잘 조정해야 한다
  • 마이크로서비스 아키텍처 도입 시점을 결정하기 어렵다.

마이크로서비스 아키텍처 패턴 언어

상용 패턴의 구조

  • 강제 조항: 문제 해결을 위해 반드시 처리해야 할 이슈
    • 주어진 맥락에서 문제를 해결하고자 할 때 반드시 처리해야 할 강제 조항
  • 결과 맥락: 패턴 적용 결과
    • 장점, 단점, 이슈
  • 연관 패터니 다섯 가지 관계 유형
    • 선행자. 이 패턴을 필요하게 만든 선행 패턴
    • 후행자. 이 패턴으로 야기된 이슈를 해결하는 패턴
    • 대안. 이 패턴의 대체 솔루션을 제공하는 패턴
    • 일반화: 문제를 해결하는 일반적인 솔루션
    • 세분화: 특정 패턴을 더 세부적으로 나타낸 형태

마이크로서비스 너머: 프로세스와 조직

애플리케이션 아키텍처는 그것을 개발하는 조직의 구조를 그대로 반영한다. 따라서 역으로 말해 조직의 구조가 마이크로서비스 아키텍처에 고스란히 반영되도록 설계해야 하낟. 이렇게 하면 개발 팀과 서비스를 느슨하게 결합할 수 있다.

소프트웨어 개발/전달 프로세스

애자일 개발 프로세스를 도입하고 스크럽, 칸반 등을 실천해야 한다.

2장

소프트웨어 아키텍처의 정의

컴퓨팅 시스템의 소프트웨어 아키텍처는 소프트웨어 엘리먼트와 그들 간의 관계, 그리고 이 둘의 속성으로 구성된 시스템을 추론하는 데 필요한 구조의 집합이다.

육각형 아키텍처 스타일

육각형 아키텍처는 논리 뷰를 비즈니스 로직 중심으로 구성하는 계층화 아키텍처 스타일의 대안이다. 비즈니스 로직이 어댑터에 전혀 의존하지 않는다는 것이 이 아키텍처의 가장 중요한 특장점이다. 외려 어댑터가 비즈니스 로직에 의존한다.

서비스란 무엇인가?

서비스는 어떤 기능이 구현되어 단독 배포가 가능한 소프트웨어 컴포넌트다. 서비스는 클라이언트가 자신이 서비스하는 기능에 접근할 수있도록 커맨드, 쿼리, 이벤트로 구성된 API를 제공한다.

서비스 정의: 비즈니스 능력 패턴별 분해

마이크소버시르 아키텍처를 구축하는 첫 번째 전략은 비즈니스 능력에 따라 분해하는 것이다.

하위 도메인 패턴별 분해

DDD는 팀에서 사용할 공용 언어(유비쿼터스 언어)를 정의한다. DDD에는 마이크로서비스 아키텍처에 적용하면 정말 유용한 하위 도메인 (서브 도메인)과 경계 컨텍스트(바운디드 컨테스트) 개념이 있다.

도메인 모델의 범위를 DDD 용어로는 경계 컨텍스트(바운디드 컨텍스트)라고 한다. 경계 컨텍스트는 도메인 모델을 구현한 코드 아티팩트를 포홤하며, 마이크로서비스 아키텍처에 DDD를 적용하면 각 서비스가 경계 컨텍스트가 된다.

서비스 분해의 장애물

  • 네트워크 지연

    • 서비스 간 왕복 횟수가 급증하면 성능이 떨어진다. 배치 API를 구현하거나, 값비싼 API를 언어 수준의 메서드나 함수 호출로 대체하는 식으로 지연 시간을 줄인다.
  • 동기 통신으로 인한 가용성 저하

    • 강한 결합도로 인해 가용성이 떨어진다는 얘기다. 다른 서비스가 장애나면 영향을 받는다.
  • 여러 서비스에 걸쳐 데이터 일관성 유지

    • 과거에는 커밋 방식의 2단계 분산 트랜잭션을 썼지만 요즘 애플리케이션에는 안맞아서 사가(saga)라는 다른 방식을 쓴다. 사가는 메시징을 이용한 일련의 로컬 트랜잭션이다.
  • 데이터의 일관된 뷰 확보

    • 마이크로서비스 아키텍처처는 각 서비스의 db가 일관저이라 해도 전역 범위에서 일관된 데이터 뷰는 확보할 수 없다.
  • 분해를 저해하는 만능 클래스

    • 존재만으로 걸림돌이다. Order 같은 클래스를 말한다. DDD를 적용해서 각 서비스를 자체 도메인 모델을 갖고 있는 개별 하위 도메인으로 취급하는 것이 좋은 방법이다. 주문과 조금이라도 관련된 서비스는 모두 각자 버전의 주문 클래스를 가진 도메인 모델을 따로 두는 것이다.

3장

프로세스간 통신

마이크로서비스 아키텍처 IPC 개요

클라이언스/서비스 상호 작용 스타일은 다양하지만 두 가지 기준으로 분류할 수 있다.

일대일/일대다 여부
일대일: 각 클라이언트 요청은 정확히 한 서비스가 처리한다.
일대다: 각 클라이언트 요청을 여러 서비스가 협동하여 처리한다.

동기/비동기 여부
동기: 클라이언트는 서비스가 제시간에 응답하리라 기대하고 대기 도중 블로킹할 수 있다.
비동기: 클라이언트가 블로킹하지 않는다. 응답은 즉시 전송되지 않아도 된다.

일대일 상호 작용도 종류는 다양하다.

  • 요청/응답(request/response): 클라이언트는 서비스에 요청을하고 응답을 기다린다 클라이언트는 응답이 제때 도착하리라 기대하고 대기 도중 블로킹할 수 있다. 결과적으로 서비스가 서로 강하게 결합되는 상호 작용 스타일이다.

  • 비동기 요청/응답: 클라이언트는 서비스에 요청을 하고 서비스는 비동기적으로 응답한다. 클라이언트는 대기 중에 블로킹하지 않고, 서비스는 오랫동안 응답하지 않을 수 있다.

  • 단방향 알림: 클라이언트는 서비스에 일방적으로 요청만 하고 서비스는응답을 보내지 않는다.

일대다 상호 작용도 종류가 있다.

  • 발행/구독: 클라이언트는 알림 메시지를 발행하고, 여기에 관심 있는 0개 이상의 서비스가 메시지를 소비한다.

  • 발행/비동기 응답: 클라이언트는 요청 메시지를 발행하고 주어진 시간 동안 관련 서비스가 응답하기를 기다린다.

마이크로서비스 API 정의

어떤 IPC를 선택하든, 서비스 api를 IDL(Interface Definition Language)로 정확하게 정의해야 한다.

메시지 포맷

gRPC 같은 IPC는 메시지 포맷이 정해져있다.

텍스트 메시지 포맷

JSON 메시지는 네임드 프로퍼티, XML 메시지는 네이믇 엘리먼트와 그 값을 모아 놓은 구조다.

텍스트 메시지 포맷의 단점은 메시지가 다소 길다는 점이다. 덩치가 큰 메시지는 파싱 오버헤드도 있다. 따라서 효율/성능이 중요한 경우 이진 포맷을 고려해 봄직하다.

이진 메시지 포맷

프로토콜 버퍼와 아브로가 유명하다.

동기 RPI 패턴 응용 통신

RPI는 클라이언트가 서비스에 요청을 보내면 서비스가 처리 후 응답을 회신하는 IPC이다. 응답 대기 중에 블로킹하는 클라이언트도 있고, 리액티브한 논블로킹 아키텍처를 가진 클라이언트도 있지만 어쨌든 메시징으로 통신하는 클라이언트와 달리 응답이 제때 도착하리라 가정한다.

동기 RPI 패턴: REST

요청 한 번으로 많은 리소스를 가져오기 어렵다
REST 리소스는 Consumer, Order 같은 비즈니스 객체 중심이다. 따라서 REST API 설계시 어떻게 하면 클라이언트가 요청 한 번으로 연관된 객체를 모두 가져올 수 있을지 고민하게 된다. 예를 들어 특정 주문과 주문한 소비자를 REST로 조회하는 클라이언트가 있다고 하자. 순수 REST API라면 클라이언트는 저겅도 2회 요청(주문 1회, 소비자 1회)을 해야 한다.

이 문제를 해결하는 한 가지 방법은 클라이언트가 리소스를 획득할 때 연관된 리소스도 함께 조회하도록 API가 허락하는 것이다. 예를 들어 GET /orders/order-id-1345?expand=consumer 처럼 쿼리 매개변수로 주문과 함께 반환될 연관 리소스를 주문하면 주문, 소비자를 한꺼번에 조회할 수 있다. 하지만 시나리오가 복잡해지면 효율이 떨어진다.

이런 까닭에 데이터를 효율적으로 조회할 수 있게 설계된 GraphQL이나 넷플릭스 팔코 등 대체 API 기술이 각광받기 싲가했따.

작업을 http 동사에 매핑하기 어렵다
데이터를 수정할 때 PUT을 쓰지만, 주문 취소/변경 등 다양한 경로가 있다. 또 PUT 사용시 필수 요건인 멱등성이 보장되지 않는 업데이트도 있다.

해결 방법은 리소스의 특정 부위를 업데이트하는 하위 리소스를 주문하는 것이다. 가령 주문 서비스에 주문 취소 끝점 POST /orders/{orderId}/cancel 을 두는 것이다. 하지만 REST답지 않아서 gRPC 같은 REST 대체 기술이 인기를 점점 끌고 있다.

REST 장단점

장점

  • 단순하고 익숙하다
  • 포스트맨 같은 브라우저 플러그인이나 curl 등의 cli 도구를 사용해서 http api를 간편하게 테스트 할 수 있다.
  • 요청/응답 스타일의 통신을 직접 지원한다.
  • http는 방화벽 친화적이다.
  • 중간 브로커가 필요하지 않기에 시스템 아키텍처가 단순해진다.

단점

  • 요청/응답 스타일의 통신만 지원
  • 가용성이 떨어진다. 중간에서 메시지를 버퍼링하는 매개자 없이 클라이언트/서비스가 직접 통신하기 때문에 교환이 일어나는 동안 양쪽 다 실행중이어야 한다.
  • 서비스 인스턴스의 위치를 클라이언트가 알고 있어야 한다. 요즘은 서비스 디스커버리 메커니즘을 이용하기 때문에 큰 단점은 아님
  • 다중 업데이트 작업을 HTTP동사에 매핑하기 어려울 때 많다.

동기 RPI 패턴: gRPC

HTTP는 한정된 동사만 지원하기 때문에 다양한 업데이트 작업을 지원하는 REST API를 설계하기가 쉽지 않다. 그래서 등장한 기술이 gRPC이다. gRPC는 이진 메시지 기반의 프로토콜이므로 서비스를 API 우선 방식으로 설계할 수 밖에 없다. 클라이언트/서버는 프로토콜 버퍼 포맷의 이진 메시지를 HTTP/2를 통해 교환한다.

gRPC API는 하나 이상의 서비스와 요청/응답 메시지 데피니션으로 구성된다.

gRPC는 프로토콜 버퍼 메시지 포맷을 사용한다. 프로토콜 버퍼는 간결하고 효율적인 이진 포맷이다. 프로토콜 버퍼 메시지는 각 필드마다 번호가 매겨지고 타입 코드가 할당된다. 메시지 수신자는 자신이 필요한 필드만 추출하고 모르는 필드는 그냥 건너뛸 수 있기 때문에 하위 호호환성을 유지하며 API를 발전시킬 수 있다.

서비스 디스커버리 개요

서비스 인스턴스마다 네트워크 위치가 동적 배정되고, 서비스 인스턴스는 자동 확장, 실패, 업그레이드 등 여러 가지 사유로 계속 달라지므로 클라이언트 코드는 서비스 디스커버리를 사용할 수 밖에 없다.

서비스 ip 주소가 정적으로 구성된 클라이언트 대신 서비스 디스커버리 메커니즘을 사용해야 한다. 핵심은 애플리케이션 서비스 인스턴스의 네트워크 위치를 db화한 서비스 레지스트리이다.

플랫폼에 내장된 서비스 디스커버리 패턴 적용

도커나 쿠버네티스 등 최신 배포 플랫폼에는 대부분 서비스 레지스트리, 서비스 디스커버리 메커니즘이 탑재되어 있다. 배포 플랫폼이 서비스 등록, 서비스 디스커버리, 요청 라우팅을 전부 관장한다.

  • 서드파티 등록 패턴: 서비스가 자신을 서비스 레지스트리에 등록하는 것이 아니라, 배포 플랫폼의 일부인 등록기라는 서드 파티가 이 작업을 대체한다.

  • 서버 쪽 디스커버리 패턴: 클라이언트가 서비스 레지스트리를 질의 하지 않고 DNS명을 요청한다. 그러면 서비스 레지스트리를 쿼리하고 요청을 분산하는 요청 라우터로 해석된다.

비동기 메시징 패턴 응용 통신

메시징은 서비스가 메시지를 서로 비동기적으로 주고받는 통신 방식이다. 메시징 기반의 애플리케이션은 보통 서비스 간 중개 역할을 하는 메시지 브로커를 사용하지만 서비스가 직접 서로 통신하는 브로커리스 아키텍처도 있다.

메시지

메시지는 헤더와 본문으로 구성된다. 메시지 종류는 다양하다.

  • 문서: 데이터만 퐇마된 제네릭한 메시지.
  • 커맨드: RPC 요청과 동등한 메시지. 호출할 작업과 전달할 매개변수가 지정되어 있다.
  • 이벤트: 송신자에게 어떤 사건이 발생했음을 알리는 메시지.

메시지는 채널을 통해 교환된다. 송신자의 비즈니스 로직은 하부 통신 메커니즘을 캡슐화한 송신 포트 인터페이스를 호출한다. 이 인터페이스는 메시지 송신자 어댑터 클래스로 구현하며, 이 클래스는 메시징 인프라를 추상화한 메시지 채널을 통해 수신자에게 메시지를 전달한다. 수신자의 메시지 핸들러 어댑터 클래스는 메시지를 처리하기 위해 호출되고, 이 클래스는 컨슈머 비즈니스 로직으로 구현된 수신 포트 인터페이스를 호출한다.

채널은 두 종류가 있다.

  • 점대점 채널: 채널을 읽는 컨슈머 중 딱 하나만 지정하여 메시지를 전달한다.
  • 발행 구독 채널: 같은 채널을 바라보는 모든 컨슈머에게 메시지 전달.

메시징 상호 작용 스타일 구현

메시징은 원래 성격 자체가 비동기적이라 비동기 요청/응답만 제공하지만 응답을 수신할 때까지 클라이언트를 블로킹할 수도 있다.

본래 메시징으로 통신하는 클라이언트/서비스 간 상호작용은 비동기적이다. 이론적으로 클라이언트가 응답을 수신할 때까지 블로킹할 수는 있지만, 실제로 클라이언트는 응답을 비동기 처리하고 클라이언트 인스턴스 중 하나가 응답을 처리한다.

메시징 기술을 하나씩 살펴보자.

메시지 브로커

메시지 브로커는 서비스가 서로 통신할 수 있게 해주는 인프라 서비스다.

브로커리스 메시징도 있는데 서비스간 직접 통신하는 것이다.
브로커리스 장점

  • 송신자에서 수신자로 직접 전달되므로 네트워크 트래픽이 가볍다
  • 병목점이나 SPOF(Simple Point of Failure)가 될 일이 없다.
  • 메시지 브로커를 설정/관리할 필요가 없으므로 운영 복잡도가 낮다.

단점

  • 서비스가 서로의 위치를 알고 있어야 하므로 서비스 디스커버리 메커니즘 중 하나를 사용해야 한다.
  • 메시지 교환 시 송신자/수신자 모두 실행 중이어야 한다.
  • 전달 보장 같은 메커니즘을 구현하기가 더 어렵다.

엔터프라이즈 애플리케이션은 대부분 메시지 브로커 기반의 아키텍처를 사용한다.

브로커 기반 메시징 개요

메시지 브로커의 가장 큰 장점은 송신자가 컨슈머의 네트워크 위치를 몰라도 된다는 것이다. 또 컨슈머가 메시지를 처리할 수 있을 때까지 브로커에 메시지를 버퍼링할 수도 있다.

예를 들어 RabbitMQ, 아파치 카프카, AWS SQS 등이 있다.

메시징 순서 유지 및 확장성은 필수 요건이다.

점대점 채널만 지원하는 AWS SQS를 제외한 나머지 메시지 브로커들은 점대점, 발행/구독 채널 모두 지원한다.

브로커 기반 메시징 장점

  • 느슨한 결합
    • 클라이언트는 서비스 인스턴스를 몰라도 되므로 서비스 인스턴스 위치를 알려주는 메커니
      즘도 필요 없다.
  • 메시지 버퍼링
  • 유연한 통신
  • 명시적 IPC

단점

  • 성능 병목 가능성
  • 단일 장애점 가능성
  • 운영 복잡도 부가

수신자 경합과 메시지 순서 유지

메시지를 동시 처리하려면 각 메시지를 정확히 한 번만 순서대로 처리해야 한다. 예를 들어 동일한 점대점 채널을 읽는 서비스 인스턴스가 3개 있고, 송신자는 주문 생성됨, 주문 변경됨, 주문 취소됨 이벤트 메시지를 차례로 전송한다고 하자. 네트워크 이슈나 가비지 컬렉션 문제로 지연이 발생하고 메시지 처리 순서가 어긋나면 시스템이 오작동 할 수 있다.

그래서 아파치 카프카, AWS 키네시스 등 요즘 메시지 브로커는 샤딩된(파티셔닝) 채널을 이용한다.

솔루션은 다음 세 부분으로 구성된다.

  1. 샤딩된 채널은 복수의 샤드로 구성되며, 각 샤드는 채널처럼 작동한다.

  2. 송신자는 메시지 헤더에 샤드 키를 지정한다 (보통 무작위 문자열 또는 바이트). 메시지 브로커는 메시지를 샤드 키별로 샤드/파티션에 배정한다. 예를들어 샤드 키 해시 값을 샤드 개수로 나눈 나머지로 샤드를 선택.

  3. 메시징 브로커는 여러 수신자 인스턴스를 묶어 마치 동일한 논리 수신자처럼 취급한다. (아파치 카프카 용어로 컨슈머 그룹이라 한다). 메시지 브로커는 각 샤드를 하나의 수신자에 배정하고, 수신자가 시동/종료하면 샤드를 재배정한다.

주문별 이벤트는 각각 동일한 샤드에 발행되고, 어느 한 컨슈머 인스턴스만 메시지를 읽기 때문에 메시지 처리 순서가 보장된다.

중복 메시지 처리

중복 메시지를 처리하는 방법은 다음 두 가징다.

  • 멱등한 메시지 핸들러를 작성
  • 메시지를 추적하고 중복을 솎아 낸다.

멱등한 메시지 핸들러 작성

동일한 입력 값을 반복 호출해도 아무런 부수 효과가 없을 때 멱등하다(idempotent)고 말하낟. 애플리케이션 메시지 처리 로직이 멱등하다면 중복 메시지는 전혀 해롭지 않다.

하지만 멱등한 애플리케이션 로직은 별로 없다.

메시지 추적과 중복 메시지 솎아 내기

소비자 신용카드를 승인하는 메시지 핸들러가 있다고 하자. 주문받고 정확히 1회 신용카드를 승인해야 한다. 반드시 메시지 핸들러가 중복 메시지를 걸러 내서 멱등하게 동작하도록 해야 한다.

컨슈머가 메시지 id를 이용하여 메시지 처리 여부를 추적하면서 중복 메시지를 솎아내면간단히 해결된다. 이를 테면 컨슈머가 소비하는 메시지 id를 무조건 db테이블에 저장하면 된다.

트랜잭셔널 메시징

서비스는 보통 db를 업데이트하는 트랜잭션의 일부로 메시지를 발행한다. db 업데이트와 메시지 전송을 한 트랜잭션으로 묶지 않으면 db업데이트 후 메시지는 아직 전송되지 않은 상태에서 서비스가 중단될 수 있기 때문에 문제가된다.

예전에는 db와 메시지 브로커에 분산 트랜잭션을 적용햇지만, 요즘 애플리케이션에는 분산 트랜잭션은 더 이상 어울리지 않는다.

애플리케이션에서 메시지를 확실하게 발행하려면 어떻게 해야할까?

db 테이블을 메시지 큐로 활용
RDBMS 기반의 애플리케이션이라면 db 테이블을 임시 메시지 큐로 사용하는 트랜잭셔널 아웃박스 패턴이 가장 알기 쉽다. 메시지를 보내는 서비스에 outbox라는 db 테이블을 만들고, 비즈니스 객체를 생성, 수정, 삭제하는 db 트랜잭션의 일부로 outbox 테이블에 메시지를 삽입하낟. 로컬 acid 트랜잭션이기 때문에 원자성은 자동 보장된다.

outbox 테이블은 임시 메시지 큐 역할을 한다. 메시지 릴레이(중계기)는 outbox테이블을 읽어 메시지 브로커에 메시지를 발행하는 컴포넌트다.

메시지를 db에서 메시지 브로커로 옮기는 방법은 두 가지다.

이벤트 발행: 폴링 발행기 패턴

이벤트 발행: 트랜잭션 로그 테일링 패턴
메시지 릴레이로 디비 트랜잭션 로그를 테일링하는 방법이다. 로그를 읽어 변경분을 하나씩 메시지로 메시지 브로커에 발행하는 것이다.

마이크로서비스 아키텍처에는 아직 스프링처럼 널리 쓰이는 프레임워크가 없다. 이벤추에이트 트램 프레임워크를 안쓰자니 저수준 코드가 반복되어 썼다.

비동기 메시징으로 가용성 개선

동기 통신으로 인한 가용성 저하

REST는 너무나 대중적이지만, 동기 프로토콜이라는 치명적인 문제가 있다.

비동기 상호 작용 스타일

클라이언트/서비스는 메시징 채널을 통해 메시지를 전송해서 서로 비동기 통신한다. 이런 상호 작용 과정에서는 어느 쪽도 응답을 대기하며 블로킹되지 않는다.

이런 아키텍처는 메시지가 소비되는 시점까지 메시지 브로커가 메시지를 버퍼링하기 때문에 매우 탄력적이다. 하지만 REST 같은 동기 프로토콜을 사용하기 때문에 요청 즉시 응답해야 하는 외부 API를 가진 서비스도 있을 것이다 서비스에 동기 API가 있는 경우 데이터를 복제하면 가용성을 높일 수 있다.

데이터 복제

서비스 요청 처리에 필요한 데이터의 레플리카를 유지하는 방법이다. 데이터 레플리카는 데이터를 소유한 서비스가 발행하는 이벤트를 구독해서 최신 데이터를 유지할 수 있다. 가령 소비자/음식점 서비스가 소유한 데이터 레플리카를 주문 서비스가 이미 갖고 있다면 주문 서비스가 주문 생성을 요청할 때 굳이 소비자/음식점 서비스와 상호작용할 필요가 없다.

물론 대용량 데이터의 레플리카를 만드는 것은 비효율적이다.

응답 반환 후 마무리

요청 처리 도중 동기 통신을 제거하려면 요청을 다음과 같이 처리하면 된다.

  1. 로컬에서 가용한 데이터만 갖고 요청을 검증한다.
  2. 메시지를 outbox 테이블에 삽입하는 식으로 db를 업데이트 한다.
  3. 클라이언트에 응답을 반환한다.

서비스는 요청 처리 중에 다른 서비스와 동기적으로 상호 작용을 하지 않는다. 그 대신 다른 서비스에 메시지를 비동기 전송한다. 이렇게 하면 서비스를 느슨하게 결합시킬 수 있다.

주문 서비스는 주문 검증을 마친 후 나머지 주문 생성 프로세를 완료한다. 이렇게 처리하면 혹여 소비자 서비스가 내려가는 사고가 발생하더라도 주문 서비스는 계쏙 주문을 생성하고 클라이언트에 응답할 수 있다. 나중에 소비자 서비스가 재가동하면 큐에 쌓인 메시지를 처리해서 밀린 주문을 다시 검증하면 된다.

이처럼 요청을 완전히 처리하기 전에 클라이언트에 응답하는 서비스는 클라이언트 코드가 조금 복잡한 편이다. 가령 주문 서비스는 응답 반환 시 새로 생성된 주문 상태에 관한 최소한의 정보만 보장하낟. 주문 생성 직후 반환되므로 주문 검증이나 소비자 신용카드 승인은 아직 완료 전이다. 따라서 클라이언트 입장에서 주문 생성 성공 여부를 알아내려면 주기적으로 폴링하거나 주문 서비스가 알림 메시지를 보내주어야 한다. 복잡하게 들리지만 이게 더 나은 방법이다.

4장

트랜잭션 관리: 사가

마이크로서비스 아케텍처에서도 단일 서비스 내부의 트랜잭션은 ACID가 보장하지만, 여러 서비스의 데이터를 업데이트하는 트랜잭션은 구현하기가 까다롭습니다.

여러 서비스에 걸친 작업의 데이터 일관성을 유지하려면 ACID 트랜잭션 대신 사가(saga)라는 메시지 주도 방식의 로컬 트랜잭션을 사용해야 한다. 그런데 사가는 ACID에서 I가 빠진 ACD만 지원하고 격리가 되지 않기 때문에 동시 비정상의 영향을 방지하거나 줄일 수 있는 설계 기법을 적용해야 한다.

마이크로서비스 아키텍처에서의 트랜잭션 관리

서비스마다 db가 따로 있기 때문에 데이터 일관성을 유지할 수 있는 수단을 강구해야 한다.

분산 트랜잭션의 문제점

예전에는 분산 트랜잭션을 이용해서 여러 서비스, db, 메시지 브로커에 걸쳐 데이터 일관성을 유지했다. 하지만 NoSQL DB와 현대 메시지 브로커(RabbitMQ, 아파치 카프카)는 분산 트랜잭션을 지원하지 않으므로 분산 트랜잭션이 필수라면 최근 기술은 포기할 수 밖에 없다.

동기 IPC형태라서 가용성이 떨어진다. 분산 트랜잭션은 참여한 서비스가 모두 가동중이어야 커밋할 수 있다.

에릭 브루어의 CAP정리에 따르면 시스템은 일관성, 가용성, 분할 허용성 중 두 가지 속성만 가질 수 있다. 요즘 아키텍스트들은 일관성보다 가용성을 더 우선시한다.

데이터 일관성을 유지하려면 비동기 서비스 개념을 토대로 다른 메커니즘이 필요하다. 이것이 바로 사가다.

데이터 일관성 유지: 사가 패턴

사가는 마이크로서비스 아키텍처에서 분산 트랜잭션 없이 데이터 일관성을 유지하는 메커니즘이다. 여러 서비스의 데이터를 업데이트하는 시스템 커맨드마다 사가를 하나씩 정의한다. 사가는 일련의 로컬 트랜잭션이다. 각 로컬 트랜잭션은 앞서 언급한 ACID 트랜잭션 프레임워크/라이브러리를 이용하여 서비스별 데이터를 업데이트 한다.

시스템 작업은 사가의 첫 번째 단계를 시작하낟. 어느 로컬 트랜잭션이 완료되면 이어서 그 다음 로컬 트랜잭션이 실행된다. 비동기 메시징으로 단계를 편성하는 방법은 잠시후 나오지만, 비동기 메시징은 하나 이상의 사가 참여자가 일시 불능일 상태인 경우에도 사가의 전체 단계를 확실히 실행시킬 수 있는 중요한 장점이 있다.

사가와 ACID 트랜잭션은 차이점이 있다. 첫째, ACID 트랜잭션에 있는 격리성이 사가에 없다. 둘째, 사가는 로컬 트랜잭션마다 변경분을 커밋하므로 보상 트랜잭션을 걸어 롤백해야 한다.

예제: 주문 생성 사가

서비스는 로컬 트랜잭션이 완료되면 메시지를 발행하여 다음 사가 단계를 트리거한다. 메시지를 통해 사가 참여자를 느슨하게 결합하고 사가가 반드시 완료되도록 보장하는 것이다. 메시지 수신자가 일시 불능 상태라면 메시지 브로커는 다시 메시지를 전달할 수 있을 때까지 메시지를 버퍼링한다. 도중에 에러가 발생하면 변경분을 어떻게 롤백시킬까?

사가는 보상 트랜잭션으로 변경분을 롤백한다
사가는 단계마다 로컬 db에 변경분을 커밋하므로 자동 롤백은 불가능하다. 즉, 보상 트랜잭션을 미리 작성한다.

사가 편성

사가 편성 로직은 두 가지 종류가 있다.

  • 코레오그래피: 의사 결정과 순서화를 사가 참여자에게 맡긴다. 사가 참여자는 주로 이벤트 교환 방식으로 통신한다.

  • 오케스트레이션: 사가 편성 로직을 사가 오케스트레이터에 중앙화한다. 사가 오케스트레이터는 사가 참여자에게 커맨드 메시지를 보내 수행할 작업을 지시한다.

코레오그래피 사가

코레오그레피 방식으로 사가를 구현하려면 두 가지 통신 이류를 고려해야 한다.

  1. 사가 참여자가 자신의 db를 업데이트하고, db트랜잭션의 일부로 이벤트를 발행하도록 해야 한다. db를 업데이트 하는 작업과 이벤트를 발행하는 작업은 워낮적으로 일어나야 한다.

  2. 사가 참여자는 자신이 수신한 이벤트와 자신이 가진 데이터를 연관 지을 수 있어야 한다.

코레오그래피 사가의 장단점
장점:

  • 단순함: 비즈니스 객체를 생성, 수정, 삭제할 때 서비스가 이벤트를 발행
  • 느슨한 결합: 참여자는 이벤트를 구독할 뿐 서로를 직접 알지 못한다.

단점:

  • 이해하기 어렵다: 여러 서비스에 구현 로직이 흩어져 있다.
  • 서비스간 순환 의존성:
  • 단단히 결합될 위험성: 예를 들어 회계 서비스는 소비자 신용 카드를 과금하게 만드는 모든 이벤트를 구독해야 한다.

오케스트레이션 사가

오케스트레이션 사가에서는 사가 참여자가 할 일을 알려 주는 오케스트레이터 클래스를 정의한다. 사가 오케스트레이터는 커맨드/비동기 응답 상호 작용을 하며 참여자와 통신한다.

오케스트레이션 사가의 장단점
장점:

  • 의존 관계 단순화: 오케스트레이터는 참여자에 의존하지만 반대는 성립하지 않으니 순환 의존성 발생 x
  • 낮은 결합도
  • 관심사를 더 분리하고 비즈니스 로직을 단순화

단점:

  • 비즈니스 로직을 너무 중앙화하면 오케스트레이터 하나가 부담. 오케스트레이터는 순서화만 담당하고 비즈니르 로직은 갖고 있지 않게 설계할 것.

비격리 문제 처리

사가는 격리성이 빠져있다. 실제로 사가의 한 트랜잭션이 커밋한 변경분을 다른 사가가 즉 볼 수있다. 이렇게 되면 한 사가가 실행중에 접근하는 데이터를 다른 사가가 변경 가능. 또한 사가가 데이터를 업데이트하기 전에 다른 사가가 그 데이터를 읽어 일관성이 깨질 수 있다.

  • 원자성: 사가는 트랜잭션을 모두 완료하거나 모든 변경분을 언두해야한다.
  • 일관성: 서비스 내부의 참조 무결성은 로컬 db가, 여러 서비스에 걸친 참조 무결성은 서비스가 처리한다.
  • 지속성: 로컬 db로 처리한다.

비정상 개요

  • 소실된 업데이트: 한 사가의 변경분을 다른 사가가 미쳐 못 읽고 덮어 쓴다.

  • 더티 읽기: 사가 업데이트를 하지 않은 변경분을 다른 트랜잭션이나 사가가 읽는다

  • 퍼지/반복 불가능한 읽기: 한 사가의 상이한 두 단계가 다른 데이터를 읽어도 결과가 달라지는 현상. 다른 사가가 그 사이 업데이트를 했기 때문에 생기는 문제.

비격리 대책을 살펴보기 전에, 사가의 구조를 나타내는 용어를 보자.

사가의 구조

보상 기능 트랜잭션: 보상 트랜잭션으로 롤백 가능한 트랜잭션

피봇 트랜잭션: 사가의 진행/중단 지점. 피봇 트랜잭션이 커밋되면 사가는 완료될 때까지 실행된다. 피봇 트랜잭션은 보상 가능 트랜잭션, 재시도 가능한 트랜잭션 그 어느 쪽도 아니지만, 최종 보상 가능 트랜잭션 또는 최초 재시도 가능 트랜잭션이 될 수는 있다.

재시도 가능 트랜잭션: 피봇 트랜잭션 직후의 트랜잭션. 반드시 성공한다.

비격리 대책

대책: 시멘틱 락
대책: 교환적 업데이트
대책: 비관적 관점
대책: 값 다시 읽기
대책: 버전 파일
대책: 값에 의한

주문 서비스 및 주문 생성 사가 설계

이하 생략. 책 참조할 것

5장. 비즈니스 로직 설계

비즈니스 로직이 여러 서비스에 흩어져 있는 마이크로서비스 아키텍처는 복잡한 비즈니스 로직을 개발하기가 까달보다. 골치아픈 문제는 크게 두 가지다.

  1. 도메인 모델은 대부분 상호 연관된 클래스가 거미줄처럼 뒤얽혀 있다.
  2. 마이크로서비스 아키텍처 특유의 트랜잭션 관리 제약 조건하에서도 작동되는 비즈니스 로직을 설계해야 한다.

다행히 이 두 문제는 서비스 비즈니스 로직을 여러 애그리거트로 구성하는 DDD 애그리거트 패턴으로 해결할 수 있다. 애그리거트는 한 단위로 취급 가능한 객체를 모아 놓은 것이다.

  • 애그리거트를 사용하면 객체 레퍼런스가 서비스 경계를 넘나들일이 없다. 객체 참조 대신 기본키를 이용하여 애그리거트가 서로 참조하기 때문이다.

  • 한 트랜잭션으로 하나의 애그리거트만 생성/수정할 수 있다. 따라서 애그리거트는 마이크로서비스 트랜잭션의 모델의 제약 조건에 잘 맞다.

비즈니스 로직 구성 패턴

생략

도메인 모델 설계: DDD 애그리거트 패턴

애그리거트는 경계가 분명하다

애그리거트는 한 단위로 취급 가능한 경계 내부의 도메인 객체들이다. 하나의 루트 엔티티와 하나 이상의 기타 엔티티 + 밸류 객체로 구성된다. 비즈니스 객체는 대부분 애그리거트로 모델링한다.

애그리거트는 도메인 모델을 개별적으로 이해하기 쉬운 덩어로리 분해한다. 또 로드, 수정, 삭제 같은 작업 범위를 분명하게 설정한다. 작업은 애그리거트 일부가 아닌 전체 애그리거트에 작용한다.

애그리거트(Aggregate)는 한마디로 서로 관련이 있는 도메인 모델들의 집합이다.
많은 수의 도메인 모델 간의 복잡한 관계를 파악하기란 쉬운 일이 아니다.

그렇기 때문에 서로 관련이 있는 도메인 모델들 끼리 묶어 각 도메인 모델의 상세 구현보다는 더 큰 그림으로 도메인 모델간의 관계를 파악하는것이 좋다.

자세한 사항이 궁금하면 그 때 애그리거트 내부를 살펴보면 된다.

대부분의 경우 하나의 애그리거트는 하나의 엔티티와 여러개의 밸류로 구성된다. 드물게 하나의 애그리거트에 두개의 엔티티가 존재하기도 한다.

각 애그리거트에는 애그리거트 루트라는 도메인 엔티티가 하나씩 있다.

애그리거트 루트는 애그리거트 내에 속한 객체의 변경을 책임지며, 도메인 규칙에 따라 언제나 애그리거트 내 모든 도메인 모델들의 일관성을 유지할 책임이 있다.

애그리거트는 DB 에 도메인을 저장하거나 읽어들이는 단위이며, 애그리거트를 읽을 때는 애그리거트 루트의 id 를 이용한다.

일반적으로 하나의 애그리거트에는 하나의 도메인 엔티티(애그리거트 루트)가 존재하며, 0개 이상의 밸류 타입이 존재한다.

드물게 한 애그리거트에 2개 이상의 엔티티가 존재하지만, 사실은 애그리거트 루트를 제외한 도메인 모델이 엔티티가 아니라 밸류 타입이거나 다른 애그리거트에 속해야 하는 경우가 많으므로 잘 확인해야한다.

서로 다른 도메인 모델이 변경의 주체와 생성 및 변경의 시점이 같다면 같은 애그리거트에 속할 가능성이 높다.

애그리어크는 일관된 경계

일부가 아니라 전체 애그리거트를 업데이트 하므로 좀 전에 설명한 일관성 문제가 해소된다.

DDD 도메인 모델 설계의 핵심은 애그리거트와 그 경계, 그리고 루트를 식별하는 것이다.

애그리거트 규칙

규칙1: 애그리거트 루트만 참조하라
규칙2: 애그리거트 간 참조는 반드시 기본키를 사용하라
규칙3: 하나의 트랜잭션으로 하나의 애그리거트를 생성/수정하라

애그리거트 입도

애그리거트는 작으면 작을 수록 좋다. 각 애그리거트의 업데이트는 직렬화되므로 잘게 나뉘어져 있으면 그만큼 애플리케이션이 동시 처리 가능한 요청 개수가 늘고 확장성이 좋아진다. 두 사용자가 동시에 같은 애그리거트를 업데이트하다가 충돌할 가능성도 줄어든다. 한편, 애그리거트가 곧 트랜잭션의 범위라서 어떤 업데이틀르 원자적으로 처리하려면 애그리거트를 크게 잡아야할 수도 있다.

비즈니스 로직 설계: 애그리거트

마이크로서비스 비즈니스 로직은 대부분 애그리거트로 구성되낟. 나머지는 도메인 서비스와 사가에 위치한다.

코드를 보기전에 애그리거트와 일접하게 연관된 도메인 이벤트 개념을 보자.

도메인 이벤트 발행

애그리거트는 상태가 전이될 때마다 이에 관련된 컨슈머를 위해 이벤트를 발행한다.

이하 생략.

6장. 비즈니스 로직 개발: 이벤트 소싱

이벤트 소싱을 잘 활용하면 애그리거트가 생성/수정될 때마다 무조건 이벤트를 발행해서 프로그래밍 오류를 제거할 수 있다.

이벤트 소싱 응용 비즈니스 로직 개발

이벤트 소싱은 비즈니스 로직을 구성하고 애그리거트를 저장하는 또 다른 방법이다. 애그리거트를 일련의 이벤트 형태로 저장한다. 이벤트는 각 애그리거트의 상태 변화를 나타낸다. 애플리케이션은 이벤트를 재연하여 애그리거트의 현재 상태를 재생성한다.

이벤트는 여러모로 좋은 점이 많다. 애그리거트 이력이 보존되므로 감사/통제 용도로도 가치가 있고, 도메인 이벤트를 확실하게 발행할 수 있어서 마이크로서비스 아키텍처에서 특히 유용하다.

기존 영속화의 문제점

클래스는 db 테이블에, 클래스 필드는 테이블 칼럼에, 클래스 인스턴스는 테이블 각 로우에 매핑하는 것이 기존 영속화 방식이다. 일반적으로 JPA 같은 ORM 프레임워크나 마이바티스 드으이 저수준 프레임워크를 사용하여 주문 인스턴스를 테이블의 로우 단위로 저장한다.

엔터프라이즈 애플리케이션은 대부분 이런 식으로 데이터를 저장한다. 하지만 단점과 한계가 있다.

  • 객체-관계 임피던스 부정합
  • 애그러거트 이력이 없다.
  • 감사 로깅을 구현하기가 번거롭고 에러가 잘 난다.
  • 이벤트 발행 로직이 비즈니스 로직에 추가된다.

객체 - 관계 임피던스 부정합

테이블 형태의 관계형 스키마와 관계가 복잡한 리치 도메인 모델의 그래프 구조는 근본적인 개념부터 다르다.

애그리거트 이력이 없다.

기존 영속화 메커니즘은 현재 애그리거트의 상태만 저장한다. 즉, 애그리거트가 업데이트되면 이전 상태는 사라지고 없다.

감사 로깅은 구현하기 힘들고 오류도 자주 발생한다.

이벤트 발행 로직이 비즈니스 로직에 추가된다.

기존 영속화의 또 다른 한계는 도메인 이벤트 발행을 지원하지 않는 점이다. 도메인 이벤트는 애그리거트가 자신의 상태를 변경한 후 발행하는 이벤트다. 마이크로서비스 아키텍처에서는 데이터를 동기화하고 알림을 전송하는 용도로 유용하게 쓰인다.

이벤트 소싱 개요

이벤트 소싱은 이벤트를 위주로 비즈니스 로직을 구현하고, 애그리거트를 DB에 일련의 이벤트로 저장하는 기법이다.

이벤트를 이용하여 애그리거트를 저장

이벤트 소싱은 도메인 이벤트 개념에 기반한 전혀 새로운 방식, 즉 애그리거트를 db에 있는 이벤트 저장소에 일련의 이벤트로 저장한다.

예를 들어 Order 애그리거트를 이벤트 소싱으로 저장한다면 Order를 ORDER 테이블에 로우 단위로 저장하는 것이 아니라, Order 애그리거트를 Events 테이블의 여러 로우로 저장한다. 각 로우가 바로 주문 생성됨, 주문 승인됨, 주문 배달됨 등의 도메인 이벤트이다.

애그리거트 생성/수정 시 애플리케이션은 애그리거트가 발생시킨 이벤트를 Events 테이블에 삽입한다. 그리고 애그리거트를 로드할 때 이벤트 저장소에서 이벤트를 가져와 재연을 하는데, 구체적으로 이 작업은 다음 3단계로 구성된다.

  1. 애그리거트의 이벤트를 로드한다.
  2. 기본 생성자를 호출하여 애그리거트 인스턴스를 생성한다.
  3. 이벤트를 하나씩 순회하며 apply()를 호출한다.

이벤트는 곧 상태 변화

이벤트 소싱에서는 이벤트가 필수다. 생성을 비롯한 모든 애그리거트의 상태 변화를 도메인 이벤트로 나타내며, 애그리거트는 상태가 바뀔 때마다 반드시 이벤트를 발생시킨다.

애그리거트 메서드의 관심사는 오직 이벤트

이벤트 소싱을 사용하면 커맨드 메서드가 반드시 이벤트를 발생시킨다. 이벤트 소싱은 커맨드 메서드 하나를 둘 이상의 메서드로 리팩터링한다.

첫 번째 메서드는 요청을 나타낸 커맨드 객체를 매개변수로 받아 상태를 어떻게 변경할지 결정한다. 이 메서드는 매개변수 확인 후 애그리거트 상태는 바꾸지 않고 상태 변경을 나타낸 이벤트 목록을 반환한다. 물론 수행할 수 없는 커맨드라면 예외를 던진다.

다른 메서드는 각자 정해진 이벤트 타입을 매개변수로 받아 애그리거트를 업데이트 한다. 이벤트마다 이런 메서드가 하나씩 있다. 이벤트는 이미 발생한 상태 변경을 나타내므로 이런 메서드는 실패할 수 없다.

동시 업데이트: 낙관적 잠금

기존 영속화 매커니즘은 대개 한 트랜잭션이 다른 트랜잭션의 변경을 덮어 쓰지 못하게 낙관적 잠금(버전 컬럼을 이용하여 마지막으로 애그리거트를 읽은 후 변경되었는지 감지)을 하여 처리한다. 즉, 애그리거트 루트를 VERSION 칼럼이 있는 테이블에 매핑하고 애그리거트가 업데이트 될때마다 UPDATE 문으로 값을 하나씩 증가시킨다.

이벤트 소싱과 이벤트 발행

이벤트 소싱은 애그리거트를 여러 이벤트로 저장하며, 이 이벤트를 가져와 현재 애그리거의 상태를 다시 구성한다. 이벤트 소싱은 일종의 확실한 이벤트 발행 장치로도 활용할 수 있다.

이벤트 발행: 폴링

잘 이해 안간다. 책 참고. 377p

이벤트 소싱의 장점

  • 도메인 이벤트를 확실하게 발행한다.
  • 애그리거트 이력이 보존된다.
  • O/R 임피던스 불일치 문제를 대부분 방지할 수 있다.
  • 개발자에게 타임 머신을 제공한다.

이벤트 소싱의 단점

  • 새로운 프로그래밍 모델을 배우는 데 시간이 걸린다.
  • 메시징 기반 애플리케이션은 복잡하다.
  • 이벤트를 개량하기가 까다롭다.
  • 데이터를 삭제하기 어렵다.
  • 이벤트 저장소를 쿼리하기가 많많찮다.

이벤트 저장소 구현

이벤트 소싱 애플리케이션은 이벤트 저장소에 이벤트를 저장한다. 이벤트 저장소는 DB와 메시지 브로커를 합한 것이다. 애그리거트의 이벤트를 기본키로 삽입/조회하는 API가 있어 마치 DB처럼 움직이서, 이벤트를 구독하는 API도 있어서 메시지 브로커처럼 동작하기도 한다.

성능/확장성이 우수한 다기능의 전용 이벤트 저장소를 두는 방법이 있다.

  • 이벤트 스토어
  • 라곰
  • 액손
  • 이벤추에이트

이벤추에이트 로컬 이벤트 저장소의 작동 원리

이벤추에이트 로컬은 오픈 소스 이벤트 저장소다. 아키텍처는 그림 6-9와 같다. 이벤트는 MySQL 등의 DB에 저장된다. 애플리케이션은 애그리거트 이벤트를 기본키로 조회/삽입하고, 아파치 카프카 등의 메시지 브로커에서 이벤트를 가져와 소비한다. 트랜잭션 로그 테일링 장치는 끊임없이 DB에서 메시지 브로커로 이벤트를 퍼 나른다.

책 참조

이벤추에이트 로컬의 이벤트 브로커를 구독하여 이벤트를 소비

이벤추에이트 로컬 이벤트 릴레이가 이벤트를 Db에서 메시지 브로커로 전파

사가와 이벤트 소싱을 접목

여러 서비스에 걸쳐 데이터 일관성을 유지하려면 서비스가 사가를 시작하거나 사가에 참여해야 할 경우가 많다. 이벤트 소싱에서는 코레오그래프 사가를 쉽게 이용할 수 있다.

이벤트 소싱에서는 코레오그래피 사가를 쉽게 이용할 수 있다. 참여자는 자신의 애그리거트가 발생시킨 도메인 이벤트를 교환하고, 각 참여자의 애그리거트는 커맨드를 처리하고 새로운 이벤트를 발생시키는 식으로 이벤트를 처리한다.

이벤트 저장소의 RDBMS/NoSQL 사용 여부는 이벤트 소싱과 오케스트레이션 사가의 연계 가능성을 가늠하는 핵심 기준이다. 이벤추에이트 트램 사가 프레임워크와 그 하부를 지지하는 트램 메시징 프레임워크는 RDBMS에서 지원되는 유연한 ACID 트랜잭션에 의존한다. 사가 오케스트레이터와 참여자는 ACID 트랜잭션을 걸고 DB를 원자적으로 업데이트 한 후, 메시지를 교환한다. 이벤추에이트 로컬 등 RDBMS 기반의 이벤트 저장소를 사용하는 애플리케이션은 융통성 있게 이벤추이에이트 트램 사가 프레임워크를 호출해서 이벤트 저장소를 ACID 트랜잭션으로 업데이트 할 수 있다. 그러나 NoSQL DB를 쓰는 이벤트 저장소는 이벤추에이트 트램 사가 프레임워크와 동일한 트랜잭션에 참여할 수 없기 때문에 다른 방법을 궁리해야 한다.

코레오그래피 사가 구현: 이벤트 소싱

이벤트 소싱은 속성상 이벤트가 모든 것을 주도하므로 코레오그래피 사가를 아주 쉽게 구현할 수 있다. 애그리거트가 업데이트되면 사가가 이벤트를 발생시키고, 제각기 배정된 이벤트 핸들러는 해당 이벤트를 소비한 후 애그리거트를 업데이트 한다. 이벤트 소싱 프레임워크는 각 이벤트 핸들러를 알아서 멱등하게 만든다.

이벤트 소싱과 코레오그래피 사가는 찰떡궁합이다. 이벤트 소싱은 메시지 기반의 IPC, 메시지 중복 제거, 원자적 상태 업데이트와 메시지 전송 등 사가가 필요로하는 여러 가지 메커니즘을 제공한다. 물론 코레오그래피 사가는 단순해서 좋지만 단점도 많다. 특히 이벤트 소싱에만 해당되는 단점이 있다.

사가 코레오그래피에 이벤트를 사용하면 이벤트의 목적이 이원화되는 문제다. 이벤트 소싱은 상태 변화를 나타내기 위해 이벤트를 이용하는데, 이벤트를 사가 코레오그래피에 갖다 쓰면 애그리거트는 상태 변화가 없어도 무조건 이벤트를 발생시켜야 한다.

이런 문제가 있어 조금 더 복잡하지만 오케스트레이션 사가를 구현하는 것이 최선이다.

오케스트레이션 사가 생성

사가 오케스트레이터 작성: RDBMS 이벤트 저장소 사용 서비스

RDBMS 이벤트 저장소를 사용하는 서비스에서는 이벤트 저장소를 업데이트하고 사가 오케스트레이터를 생성하는 작업을 한 트랜잭션으로 묶을 수 있다.

사가 오케스트레이터 작성: NoSQL 이벤트 저장소 사용 서비스

서비스는 애그리거트가 발생시킨 도메인 이벤트에 반응하여 사가 오케스트레이터를 생성하는 이벤트 핸들러를 갖고 있어야 한다. 사가 오케스트레이터를 생성하는 이벤트 핸들러를 작성할 때 주의할 점은 중복 이벤트를 처리해야 한다는 사실이다.

이벤트 소싱 기반의 사가 참여자 구현

커맨드 메시지를 멱등하게 처리

커맨드 메시지를 멱등하게 처리하려면 우선 이벤트 소싱 기반의 사가 참여자가 중복 메시지를 솎아 낼 수 있는 수단이 필요하다. 메시지를 처리할 때 생성되는 이벤트에 메시지 ID를 기록하면 사가 참여자는 다음에 애그리거트를 업데이트 하기 전에 메시지 ID를 이벤트에서 꺼내 보고 자신이 이전에 이메시지를 처리한 적이 있는지 확인한다.

응답 메시지를 원자적으로 전송

이하 생략. 어렵다.

7장. 마이크로서비스 쿼리 구현

마이크로서비스로 전환시 고민해야 할 분산 데이터 문제가 트랜잭션만 있는 것은 아니다. 쿼리를 구현하는 방법도 찾아내야 한다.

마이크로서비스 아키텍처에서는 다음 두 가지 패턴으로 쿼리를 구현한다.

  • API 조합 패턴: 서비스 클라이언트가 데이터를 가진 여러 서비스를 직접 호추랗여 그 결과를 직접 호출하여 그 결과를 조합하는 패턴. 가장 단순해서 가급적 이 방법을 쓰는 것이 좋다.

  • CQRS(커맨드 쿼리 책임 분산) 패턴: 쿼리만 지원하는 하나 이상의 뷰 전용 DB를 유지하는 패턴이다. API 조합 패턴보다 강력한 만큼 구현하기가 더 복잡하다.

API 조합 패턴 응용 쿼리

모놀리식 애플리케이션은 전체 데이터가 하나의 db에 있기 때문에 알기 쉽게 SELECT 문으로 여러 테이블을 조인해서 주문 내역을 조회하면 된다 반면 마이크로서비스 아키텍처로 전환하면 데이터가 여러 서비스에 뿔뿔이 흩어지게 된다.

API 조합 패턴 개요

API 조합 패턴은 데이터를 가진 서비스를 호출한 후 그 반환 결과를 조합해서 가져온다. 이 과정에는 다음 두 종류의 참여자가 개입한다.

  • API 조합기: 프로바이더 서비스를 쿼리하여 데이터를 조회한다.
  • 프로바이더 서비스: 최종 결과로 반환할 데이터의 일부를 갖고 있는 서비스

API 조합기는 A, B, C 세 프로바이더 서비스에서 데이터를 조회한 후 그 결과를 조합한다. API 조합기는 웹 애플리케이션처럼 웹 페이지에 데이터를 렌더링하는 클라이언트일 수도 있고, 쿼리 작업을 API 끝점으로 표출한 API 게이트웨이나 프론트엔드를 위한 백엔드 패턴의 변형일 수도 있다.

이 패턴으로 특정 쿼리 작업을 구현할 수 있을지 여부는 데이터가 어떻게 분할되었는지, 데이터를 가진 서비스가 어떤 API 기능을 표출하는지, 사용 중인 DB는 어떤 기능을 제공하는지 등 다양한 요건에 따라 가변적이다. 예를 들어 프로바이더 서비스가 필요한 데이터를 조회할 수 있는 API를 제공하더라도 애그리거트가 거대한 데이터 뭉치를 비효율적으로 인-메모리 조인을 해야할 수도 있다. 따라서 이 패턴으로 구현할 수 없는 쿼리 작업도 있지만, 다행히 대부분의 경우 이 패턴을 적용해서 쿼리 작업을 구현할 수 있다.

API 조합 설계 이슈

누가 API 조합기 역할을 맡을 것인가

  1. 서비스 클라이언트
  • 클라이언트가 방화벽 외부에 있고 서비스가 위치한 네트워크가 느리다면 실용적이지 않다.
  1. 애플리케이션의 외부 API가 구현된 API 게이트웨이
  2. API 조합기를 스탠드 얼론 서비스로 구현

API 조합 패턴의 장단점

단점:

  • 오버헤드 증가
  • 가용성 저하 우려
  • 데이터 일관성 결여

CQRS 패턴

엔터프라이즈 애플리케이션은 대부분 RDBMS에 트랜잭션을 걸어 레코드를 관리하고, 텍스트 검색 쿼리는 일래스틱서치나 솔라 등의 텍스트 검색 db를 이용해서 구현한다. 애플리케이션에 따라서 RDBMS와 텍스트 검색 db를모두 출력하여 동기화하기도 하고, 주기적으로 RDBMS에서 텍스트 검색 db로 데이터를 복사하는 경우도 있다. RDBMS 특유의 트랜잭션 기능과 텍스트 검색 db의 탁월한 쿼리 능력을 융합해서 사용하는 것이다.

커맨드 쿼리 책임 분리
여러 서비스에 있는 데이터를 가져오는 쿼리는 이벤트를 이용하여 해당 서비스의 데이터를 복제한 읽기 전용 뷰를 유지한다.

CQRS는 이런 종류의 아키텍처를 일반화한 것이다. 하나 이상의 쿼리가 구현된 하나 이상의 뷰 db를 유지하는 기법이다. cqrs는 api 조합 패턴으로는 효율적으로 구현하기 어려운 쿼리 때문에 각광받기 시작했다.

CQRS 개요

마이크로서비스 아키텍처에서는 쿼리를 구현할 때 세 가지 난관에 봉착한다.

  • API를 조합하여 여러 서비스에 흩어진 데이터를 조회하려면 값 비싸고 비효율적인 인-메모리 조인을 해야 한다.

  • 데이터를 가진 서비스는 필요한 쿼리를 효율적으로 지원하지 않는 db에 또는 그런 형태로 데이터를 저장한다.

  • 관심사를 분리할 필요가 있다는 것은 데이터를 가진 서비스가 쿼리 작업을 구현할 장소로 적합하지 않다는 뜻이다.

이 세가지 문제를 CQRS가 해결한다.

CQRS는 커맨드와 커리를 서로 분리한다.

CQRS(커맨드 쿼리 책임 분리)는 이름처럼 관심사의 분리/구분에 관한 패턴이다. 이 패턴에 따르면 영속적 데이터 모델과 그것을 사용하는 모듈은 커맨드롸 쿼리, 두 편으로 가른다. 조회(R) 기능(HTTP GET)은 쿼리 쪽 모듈 및 데이터 모델에, 생성/수정/삭제(CUD) 기능은 커맨드 쪽 데이터 모델에 구현하는 것이다. 양쪽 데이터 모델 사이의 동기화는 커맨드 쪽에서 발행한 이벤트를 쿼리쪽에서 구독하는 식으로 이루어진다.

449 그림 참조

CQRS 장점

  • 마이크로서비스 아키텍처에서 쿼리를 효율적으로 구현할 수 있다.
    • 여러 서비스에서 데이터를 미리 조인해 놓는 CQRS 뷰를 이용하는 것이 더 효율적이다.
  • 다양한 쿼리를 효율적으로 구현할 수 있다.
  • 이벤트 소싱 애플리케이션에서 쿼리가 가능하다.
  • 관심사가 더 분리된다.

단점

  • 아키텍처가 더 복잡하다
  • 복제 시차를 처리해야 한다.
    • 커맨드 쿼리 양쪽 뷰 사이의 시차를 처리해야 한다. 당연히 커맨드 쪽이 이벤트를 발행하는 시점과 쿼리 쪽이 이벤트를 받아 뷰를 업데이트하는 시점 사이에 지연이 발생할 것이다.

CQRS 뷰 설계

CQRS 뷰 모듈에는 하나 이상의 쿼리 작업으로 구성된 API가 있다. 하나 이상의 서비스가 발행한 이벤트를 구독해서 최신 상태로 유지된 DB를 조회하는 쿼리 API이다.

뷰 모듈을 개발할 때에는 몇 가지 중요한 설계 결정을 해야 한다.

  • DB를 선정하고 스키마를 설계해야 한다.

  • 데이터 접근 모듈을 설계할 때 멱등한/동시 업데이트 등 다양한 문제를 고려해야 한다.

  • 기존 애플리케이션에 새 뷰를 구현하거나 기존 스키마를 바꿀 경우, 뷰를 효율적으로 (재)빌드할 수 있는 수단을 강구해야 한다.

  • 뷰 클라이언트에서 복제 시차를 어떻게 처리해야할지 결정해야 한다.

뷰 DB 선택

SQL 대 NoSQL

NoSQL은 트랜잭션 기능이 제한적이고 범용적인 쿼리 능력이 없지만, 어떤 유스케이스는 유연한 데이터 모델, 우수한 성능/ 확장성 등 SQL 기반 DB보다 더 낫다. NoSQL은 CQRS와 잘 맞는다. 또 CQRS는 단순 트랜잭션만 사용하고 고정된 쿼리만 실행하므로 NoSQL 제약 사항에도 영향을 받지 않는다.

물론 SQL DB를 사용하는게 더 나을 때도 있다. 비관계형 기능 (예: 지리 공간 데이터형 및 쿼리)을 추가할 수도 있고, 리포팅 엔진 때문에 써야할 수도 있다.

데이터 접근 모듈 설계

이벤트 핸들러와 쿼리 API 모듈은 DB에 직접 접근하지 않는다. 그대신 데이터 접근 객체 및 헬퍼 클래스로 구성된 데이터 접근 모듈을 사용한다. DAO는 이벤트 핸들러가 호출한 업데이트 작업과 쿼리 모듈이 호출한 쿼리 작업을 실질적으로 수행한다. 또 고수준 코드에 쓰이는 자료형과 DB API간 매핑, 동시 업데이트 처리 및 업데이트 멱등성 보장 등 DAO는 하는 일이 많다.

동시성 처리
DAO는 동시 업데이트로 서로가 서로의 데이터를 덮어 쓰지 않도록 작성되어야 한다.

멱등한 이벤트 핸들러
이벤트 핸들러는 같은 이벤트를 한 번 이상 넘겨 받고 호출될 수도 있다. 중복 이벤트 때문에 부정확한 결과가 나온다면 멱등한 이벤트 핸들러가 아니다. 비멱등적 이벤트 핸드럴는 자신이 뷰 데이터 저장소에서 처리한 이벤트 ID를 기록해 두었다가 중복 이벤트가 들어오면 솎아 내야 한다.

이벤트 핸드러는 반드시 이벤트 ID를 기록하고 데이터 저장소를 원자적으로 업데이트 해야 한다. 그 방법은 DB 종류마다 다르다.

CQRS 뷰 추가 및 업데이트

아카이빙된 이벤트를 이용하여 CQRS 뷰 구축

메시지 브로커는 메시지를 무기한 보관할 수 없다. 기존 메시지 브로커 (예: rabbitMQ)는컨슈머가 메시지를 처리한 직후 메시지를 삭제하며, 미리 설정된 시간 동안 메시지를 보관 가능한 최신 브로커 (예: 아파치 카프카) 역시 이벤트를 영구 보관하지는 않느낟. 그래서 이벤트를 메시지 브로커에서 전부 읽기만 해서는 뷰를 구축할 수 없다. 따라서 AWS S3 같은 곳에서 아카이빙된, 더 오래된 이벤트도 같이 가져와야 한다.

CQRS 뷰를 단계적으로 구축

전체 이벤트를 처리하는 시간/리소스가 점점 증가하는 것도 뷰 생성의 또 다른 문제점이다. 그래서 주기적으로 애그리거트 인스턴스의 스냅샷을 찍고, 그 이후 발생한 이벤트를 이용하여 뷰를 생성한다.

8장. 외부 API 패턴

모놀리식 애플리케이션은 배포하면 서비스마다 API를 갖고 있기 때문에 어떤 종류의 API를 클라이언트에 표출해야 할지 결정해야 한다.

외부 API 설계 이슈

웹 애플리케이션은 방화벽 내부에서 실행되기 때문에 대역폭이 높고 지연 시간이 짧은 LAN을 통해 서비스에 접속하지만, 다른 클라이언트는 방화벽 내부에 있으므로 상대적으로 대역폭이 낮고 지연이 높은 인터넷 또는 모바일 네트워크 환경에서 서비스에 접근한다.

클라이언트가 서비스를 직접 호출하도록 API를 설계할 수도 있다. 모놀리식은 이렇게 하지만 마이크로 서비스 아키텍처에서는 거의 쓰지 않는다.

  • 서비스 API가 잘게 나뉘어져 있어서 클라이언트가 여러번 요청을해야하고, UX가 나빠진다.

  • 클라이언트가 서비스 및 API를 알아야 하는 구조라서 캡슐화가 되지 않고, 나중에 아키텍처와 API를 바꾸기도 어렵다.

    • 캡슐화가 되지 않아 프론트엔드 개발자가 백엔드와 맞물려 코드를 변경해야 한다.

API 게이트웨이 패턴

서비스에 직접 접근하면 이렇게 여러모로 문제가 많다. 클라이언트가 인터넷을 통해서 API를 조합한다는 것 자체가 실용적인 방식은 아니다. 캡슐화가 안되므로 개발자가 서비스를 분해하고 API를 변경하기도 어렵다. 방화벽 외부에서 부적절한 프로토콜로 통신하는 서비스도 있기 때문에 API 게이트웨이를 사용하는 것이 훨씬 나은 방법이다.

API 게이트웨이 패턴 개요

API 게이트웨이는 방화벽 외부의 클라이언트가 애플리케이션에 API 요청을 하는 단일 창구 역할을 하는 서비스다. 객체지향설계에서 퍼사드 패턴이다. 내부 애플리케이션 아키텍처를 캡슐화하고 자신의 클라이언트에는 API를 제공한다. 요청 라우팅, API 조합, 프로토콜 변환을 관장한다.

요청 라우팅

API 게이트웨이는 라우팅 맵을보고 어느 서비스로 요청을 보낼지 결정한다. 라우팅 맵은 이를테면 HTTP 메서드와 서비스의 HTTP URL을 매핑한 것이다. 엔진엑스 같은 웹 서버의 리버스 프록시와 똑같다.

API 조합

모바일 클라이언트가 한번으로 필요한 데이터를 조회할 수 있도록 대단위 API를 제공한다.

프로토콜 변환

내부에서 REST와 gRPC를 혼용해도 외부에는 REST API를 제공할 수 있다.

API 게이트웨이는 클라이언트마다 적합한 API를 제공한다.

엣지 기능 구현

인증, 인가, 사용량 제한, 캐싱, 지표 수집, 요청 로깅 등

API 게이트웨이 아키텍처

API 게이트웨이는 API 계층과 공통 계층으로 구성된 모듈 아키텍처 구조다. API 계층에는 독립적인 하나 이상의 API 모듈이 있고, 각 API 모듈에는 특정 클라이언트용 API가 구현되어 있다. 공통 계층에는 엣지 기능 등 공통 기능이 구현되어 있다.

API 게이트웨이 소유권 모델

API 게이트웨이 개발/운영은 누가 담당할까? 따로 팀을 신설할 수도 있지만, 넷플릭스는 해당 클라인트 팀(모바일, 웹, 퍼블릭 api팀) 이 소유하는 구조가 바람직하다고 한다.

프론트엔드 패턴을 위한 백엔드

각 클라이언트마다 API 게이트웨이를 따로 두는 BFF (Backends For Frontends) 패턴.

각 API 모듈이 하나의 클라이언트 팀이 개발/운영하는 스탠드 얼론 API 게이트웨이가 되는 구조다. 퍼블릭 api 팀은 자기네 api 게이트웨이를 소유/운영하고, 모바일 팀도 자기네 api 게이트웨이를 소유/운영하는 식이다.

책임을 명확히 하는 것 외에도 BFF 패턴은 장점이 많다. 일단 API 모듈이 격리되어 신뢰성이 향상된다. 자체 프로세스로 작동되므로 관측성도 좋고, 독립적 확장도 가능하다.

장단점

장점

애플리케이션 내부 구조 캡슐화. 클라이언트가 특정 서비스를 호출할 필요 없이 무조건 게이트웨이에 이야기를 하면 된다.

단점

개발, 배포, 관리를 해야하는 고가용 컴포넌트가 하나 더 늘어난다. 개발 병목지점이 될 수도 있다.

API 게이트웨이 설계 이슈

  • 성능과 확장성

    • API 게이트웨이에 동기 IO를 사용할 것인가 비동기를 사용할 것인가는 중요한 이슈다. 동기 모델은 각 네트워크 접속마다 스레드를 하나씩 배정한다. 따라서 프로그래밍 모델이 간단하고 잘 작동된다. 그러나 동기 IO는 다소 무거운 OS 스레드를 사용하기 때문에 스레드 개수에 제약을 받고 API 게이트웨이의 동시 접속 가능 개수도 제한적이다.

    반면 비동기(논블로킹) IO 모델은 단일 이벤트 루프 스레드가 IO 요청을 각 이벤트 핸들러로 디스패치 한다. 비동기 기술은 JVM에서는 네티, 버택스 등 NIO 기반의 프레임워크가 있고, 비 JVM 환경에서는 자바스크립트 엔진이 탑재된 Node.js 플랫폼이 있다.

    논블로킹 IO는 다중 스레드를 사용하는 오버헤드가 없기 때문에 확장성이 더 좋다. 비동기/콜백 기반의 프로그래밍과 모델은 훨씬 복잡한 편이라서 코드를 작성하고,이해하고 디버깅하기가 어려운 단점은 있다. 이벤트 핸들러는 이벤트 루프 스레드가 블로킹되지 않도록 제어권을 신속하게 반환해야 한다.

  • 리액티브 프로그래밍 추상체를 이용하여 관리 가능한 코드 작성

  • 부분 실패 처리

API 게이트웨이 구현

두 가지 방법이 있다.

  • 기성 API 게이트웨이 제품/서비스 활용

  • 직접 개발

기성 API 게이트웨이 제품/서비스 활용

AWS API 게이트웨이

각각의 (메서드, 리소스)를 백엔드 서비스(AWS 람다 함수, 애플리케이션에 정의된 HTTP 서비스, AWS 서비스 등)로 라우팅할 수 있게 구성한다.

API 조합 기능을 제공하지 않기에 직접 백엔드 서비스에 조합 로직을 구현해야 한다.

AWS 애플리케이션 부하 분산기

AWS ALB(Application Load Balancer)는 HTTP, HTTPS, 웹 소켓, HTTP/2용 부하 분산기다.

API 게이트웨이 자체 개발

주울 (넷플릭스 오픈 소스 프로젝트)과 스프링 클라우드 게이트웨이(피보탈의 오픈 소스 프로젝트)를 사용해서 할 수 있다.

API 게이트웨이 구현: GraphQL

API를 호출하는 것은 간단해보이지만, 여러 서비스에서 데이터를 가져오는 끝점이기 때문에 서비스 호출 후 결과를 조합하는 api 조합 코드를 작성해야 하낟. 또한 클라이언트마다 필요한 데이터가 다르기도 하다.

api 게이트웨이에 별의별 클라이언트를 지원하는 REST API를 구현하는 것은 시간 낭비다. 결국 GraphQL처럼 데이터를 효율적으로 가져오도록 설계된 그래프 기반의 API 프레임워크를 찾게된다. 그래프 기반 API 프레임워크는 그래프 기반의 스키마로 서버 api를 구성하는 것이 핵심이다. 그래프 기반 스키마는 프로퍼티 및 다른 노드와 연고나된 노드를 정의하낟. 클라이언트는 그래프 노드와 이들의 프로퍼티/관계 단위로 필요한 데이터를 지정해서 조회하기 때문에 API 게이트웨이로 원하는 데이터를 한 번에 모두 가져올 수 있다.

9장. 마이크로서비스 테스트 1부

모놀리식은 덩치가 커서 테스트가 어렵다. 마이크로서비스 아키텍처를 도입하는 중요한 계기 중 하나는 테스트성을 개선하는 것이다. 마이크로서비스 아키텍처 특유의 복잡성 때문에라도 테스트는 반드시 자동화해야 한다.

마이크로서비스 아키텍처 테스트 전략

테스트의 목적은 SUT(System Under Test)의 동작을 확인하는 것이다.

자동화 테스트 4단계

  1. 설정: SUT와 그 디펜던스로 구성된 테스트 픽스처 초기화
  2. 실행: SUT 호출
  3. 확인: 호출 결과 및 SUT 상태 단언
  4. 정리: 테스트 픽스처 정리

목/스텁을 이용한 테스트
SUT만 따로 테스트하기 위해 테스트 더블(test double)로 대체한다.

테스트 더블은 스텁(stub)과 목(mock) 두 종류다. 스텁은 SUT에 값을 반환하는 테스트 더블, 목은 SUT가 정확하게 디펜던시를 호출했는지 확인하는 테스트 더블이다. 그리고 목은 스텁의 일종이다.

컨슈머 주도 계약 테스트
API 게이트웨이의 OrderServiceProxy는 GET /orders/{orderID}와 같은 REST 끝점을 여럿 호출한다. 따라서 API 게이트웨이와 주문 서비스, 양쪽 API가 서로 맞는지 테스트를 작성해서 확인해야 한다. 컨슈머 계약 테스트 용어로는 두 서비스가 컨슈머-프로바이더 관게를 맺는다. 컨슈머는 API 게이트웨이, 프로바이더는 주문 서비스가 된다.

컨슈머 계약 테스트의 초점은 프로바이더 API의 형상이 컨슈머가 기대하는 것과 부합하는지 확인하는 것이다.

  • 컨슈머가 기대한 HTTP 메서드와 경로인가
  • 컨슈머가 기대한 헤더를 받는가.
  • 요청 본문을 받는가
  • 컨슈머가 기대한 상태코드, 헤더, 본문이 포함된 응답을 받는가.

컨슈머 계약 테스트는 프로바이더의 비즈니스 로직을 다 체크하는 테스트가 아니다.

배포 파이프라인

배포 파이프라인은 개발자가 데스크톱에서 작성한 코드를 프로덕션에 반영하는 자동화 프로세스다. 보통은 젠킨스 같은 CI 서버로 배포 파이프라인을 구축한다.

  1. 사전 - 커밋 테스트: 단위 테스트 실행
  2. 커밋 테스트 단계: 서비스 컴파일 후 단위 테스트를 실행하고 정적 코드 분석
  3. 통합 테스트 단계:
  4. 컴포넌트 테스트 단계
  5. 배포 단계
  6. 프로덕션 환경

2~5까지가 배포 파이프라인에 있다.

서비스 단위 테스트 작성

일반적으로 단위 테스트는 해당 클래스가 예상대로 잘 동작하는지 확인하는 것이 목표다.
단위 테스트는 두 가지 종류가 있다.

  • 독립 단위 테스트: 클래스 디펜던시를 목 객체로 나타내고 클래스를 따로 테스트한다.
  • 공동 단위 테스트: 클래스와 디펜던시를 테스트한다.

일반적으로 컨트롤러와 서비스는 독립 단위 테스트, 엔터티와 밸류 객체 같은 도메인 깨체는 공동 단위 테스트를 사용한다.

단위 테스트 작성: 엔티티

단위 테스트 작성: 밸류 객체

밸류 객체는 불변이고 부수 효과를 걱정할 필요가 없기 때문에 테스트하기 쉬운편이다. 주어진 상태로 밸류 객체 생성 후, 메서드 하나를 호출해 수신한 값을 단언한다.

단위 테스트 작성: 사가

사가는 사가 참여자에게 커맨드 메시지를 보내고 이들의 응답을 처리하는 영속적 객체다. 이 클래스의 테스트는 사가를 생성하고 사가가 참여자에게 기대한 순서대로 메시지를 전송하는지 확인한다. 우선 정상적인 과정을 테스트하고, 사가 참여자가 실패 메시지를 반환해서 사가가 롤백되는 다양한 시나리오에서도 테스트를 작성해야 한다.

단위 테스트 작성: 이벤트/메시지 핸들러

메시지 어댑터는 여느 컨트롤러처럼 도메인 서비스를 호출하는 단순 클래스이다. 메시지 어댑터의 각 메서드는 메시지/이벤트에서 꺼낸 데이터를 서비스 메서드에 넘겨 호출한다.

메시지 어댑터는 컨트롤러와 비슷한 방법으로 단위 테스트할 수 있다. 테스트별로 메시지 어댑터 인스턴스를 생성하고 메시지를 채널에 전송한 후, 서비스 목이 정확히 호출되었는지 확인하는 흐름이다. 물론 하부의 메시징 인프라는 스터빙했기 때문에 어떤 메시지 브로커도 관여하지 않는다.

10장. 마이크로서비스 테스트 2부

통합 테스트는 인프라 서비스, 타 애플리케이션 서비스와 적절히 연동되는지 확인하는 테스트다.

통합 테스트는 종단 간 테스트처럼 전체 서비스를 실행시키지 않는다.

통합 테스트: 영속화

  1. 설정: DB 스키마를 생성하고 기지의 상태로 초기화
  2. 실행
  3. 확인
  4. 정리

통합 테스트: REST 요청/응답형 상호 작용

통합 테스트는 컨슈머 주도 계약 테스트를 활용하는 것이 좋다.

11장. 프로덕션 레디 서비스 개발

서비스를 프로덕션에 배포할 수 있으려면 보안, 구성성, 관측성을 보장해야 한다.

보안 서비스 개발

애플리케이션 개발자는 주로 다음 네 가지 보안 요소를 구현한다.

인증(authentication): 애플리케이션에 접근하는 주체 시누언 확인.

인가(authorization): 주체가 어떤 권한이 있는지 확인. 보통 역할 기반(role-based) 보안ACL(Access Control List)을 함께 사용한다. 역할 기반 보안은 사용자마다 하나 이상의 역할을 배정해서 특정 작업을 호출 권한을 부여하고, ACL은 사용자 또는 역할을 대상으로 특정 비즈니스 객체나 애그리거트에 작업할 권한을 부여한다.

감사(auditing): 보안 이슈 탐지, 컴플라이언스 시행, 고객 지원을 위해 주체가 수행하는 작업을 추적한다.

보안 IPC: 모든 서비스를 드나드는 통신이 TLS(Transport Layer Security)를 경유하는 것이 가장 이상적이다. 서비스간 통신은 인증이 필요한 경우도 있다.

기존 모놀리식 애플리케이션의 보안

마이크로서비스 아키텍처에서의 보안 구현

모노실리식에서 통했던 다음 두 방식은 마이크로서비스 아키텍처에서 사용할 수 없다.

인-메모리 보안 컨텍스트(in-momory security context): 스레드 로컬 등 인-메모리 보안 컨텍스트를 이용해서 사용자 신원을 전달하는 방법이지만, 서비스는 메모리를 공유할 수 없으므로 인-메모리 보안 컨텍스트로 사용자 신원을 전달할 수 없다.

중앙화 세션(centralized session): 인-메모리 보안 컨텍스트가 의미가 없으니 인-메모리 세션도 마찬가지다. 느슨한 결합 원칙에 위배되기는 하지만 이론상 여러 서비스가 db 기반의 세션에 접근하는 것은 가능하다.

API 게이트웨이에서의 인증 처리

요청을 서비스에 보내기 전에 API 게이트웨이가 요청을 인증하는 것이 좋다.

API 게이트웨이로부터 호출받은 서비스는 요청 주체가 누구인지 알아야 하고, 인증을 마친 요청인지 아닌지 반드시 확인해야 한다. 해결 방법은 API 게이트웨이가 매번 서비스에 요청을 할 때마다 토큰을 함께 넣어 보내는 것이다. 서비스는 이 토큰을 이용하여 요청을 검증하거나 주체 정보를 획득할 수 있다.

로그인 기반 클라이언트의 이벤트 순서

  1. 클라이언트는 자격증명이 포함된 로그인 요청을 한다.

  2. API 게이트웨이는 보안 토큰을 반환한다.

  3. 클라이언트는 작업을 호출하는 요청에 보안 토큰을 넣어 보낸다.

  4. API 게이트웨이는 보안 토큰을 검증하고 해당 서비스로 포워딩한다.

인가 처리

인증처럼 인가 로직도 API 게이트웨이 내부에 중앙화하면 보안을 강화할 수 있다. 스프링 시큐리티 같은 보안 프레임워크를 이용하면 비교적 간단하게 구현할 수 있다.

하지만 이렇게 API 게이트웨이에 인가 로직을 두면, API 게이트웨이와 서비스가 단단히 결합하게 되어서 나중에 변경할 일이 생기면 서로 맞물리게될 수도 있다. 또 API 게이트웨이는 역할 기반의 URL 경로 접근만 구현할 수 있으며, 개별 도메인 객체의 접근 권한을 제어하는 ACL까지 구현하기는 무리다. API 게이트웨이가 서비스 도메인 로직의 세부 내용까지 알고 있어야하는 것은 말이 안된다.

따라서 인가 로직은 서비스에 구현하는 것이 좋다. 서비스가 직접 역할 기반으로 URL과 메서드를 인가하고, ACL로 애그리거트 접근을 따로 관리하는 것이다.

JWT로 사용자 신원/역할 전달

API 게이트웨이가 어떤 종류의 토큰에 사용자 정보를 담아 서비스에 전달할지 결정해야 한다. 두 가지가 있다.

  1. 난독화 토큰. 보통 UUID를 많이 쓴다. 성능 및 가용성이 떨어지고 지연 시간이 길다. 토큰 수신자가 토큰의 유효성을 검증하고 보안 서비스를 동기 RPC 호출하여 사용자 정보를 조회해야 하기 때문이다.

  2. 투명 토큰. JWT는 사실상 투명 토큰의 표준 규격이다.

OAuth 2.0 응용

OAuth 2.0은 깃허브나 구글 등 퍼블릭 클라우드 서비스 사용자가 자기 정보에 접근하려는 서드파티 애플리케이션을 패스워드 노출 없이 허가할 수 있는 방안을 찾다가 정착된 인증 프로토콜이다.처음에는 퍼블릭 클라우드 서비스의 접근 인가 수단으로 쓰였지만, 여느 애플리케이션의 인증/인가 용도로도 사용할 수 있다.

다음은 OAuth2.0의 핵심 개념이다.

  • 인증 서버: 사용자 인증 및 엑세스/리프레시 토큰 획득 API를 제공한다. 스프링 OAuth는 OAuth 2.0 인증 서버를 구축하는 대표적인 프레임워크다.

  • 액세스 토큰: 리소스 서버 접근을 허가하는 토큰

  • 리프레시 토큰: 클라이언트가 새 엑세스 토큰을 얻기 위해 필요한 토큰

  • 리소스 서버: 액세스 토큰으로 접근을 허가하는 서비스

  • 클라이언트: 리소스 서버에 접근하려는 클라이언트

  1. 클라이언트는 기본 인증을 이용하여 자격증명과 함께 요청한다.
  2. API 게이트웨이는 OAuth 2.0 인증 서버에 패스워드 승인을 요청한다.
  3. 인증 서버는 API 클라이언트의 자격 증명을 검증하고 액세스/리프레시 토큰을 반환한다.
  4. API 게이트웨이는 서비스에 요청을 할 때마다 발급 받은 액세스 토큰을 넣어 보내고, 서비스는 액세스 토큰을 이용하여 요청을 인증한다.

액세스 토큰이 만료되면 리프레시 토큰을 이용하여 액세스 토큰을 다시 발급받는다.

구성 가능한 서비스 설계

런타임에 구성 프로퍼티 값을 서비스에 제공하는 외부화 구성 메커니즘은 구현 방식에 따라 푸시/풀 두 가지 모델이 있다.

  • 푸시 모델: OS 환경 변수, 구성 파일 등을 통해 배포 인프라에서 서비스로 프로퍼티 값을 전달한다.

  • 풀 모델: 서비스 인스턴스가 구성 서버에 접속해서 프로퍼티 값을 읽어 온다.

푸시 기반의 외부화 구성

푸시 모델은 지금도 널리 사용되는 서비스 구성 메커니즘이지만, 이미 실행 중인 서비스를 재구성하기는 어려운 한계가 있다. 배포 인프라 구조상, 실행 중인 서비스의 외부화 구조성을 서비스를 재시동 않고서는 바꿀 수 없는 경우가 있다 (실행 중인 프로세스의 환경 변수 값은 기술적으로 변경 불가). 구성 프로퍼티 값이 여러 서비스에 흩어지는 것도 문제다.

풀 기반의 외부화 구성

풀 모델은 서비스 인스턴스가 시동 시 자신이 필요한 값을 구성 전용 서버에 접속하여 읽는 방식이다.

구성 서버는 여러 가지 방법으로 구현할 수 있다.

  • 버전 관리 시스템 (깃, SVN)
  • SQL/ NoSQL DB
  • 전용 구성 서버 (예: 스프링 클라우드 컨피그 서버), AWS 파리미터 스토어

구성 서버가 있으면 여러모로 장점이 많다.

  • 중앙화 구성: 모든 구성 프로퍼티를 한 곳에서 관리하면 간편하고, 전역 기본값을 정의해서 서비스 단위로 재정의하는 식으로 중복 구성 프로퍼티 제거

  • 민감한 데이터의 투명한 복호화

  • 동적 재구성: 수정된 프로퍼티 값을 폴링 등으로 감지해서 자동 재구성한다.

관측 가능한 서비스 설계

다음은 관측 가능한 서비스를 설계하는 패턴이다.

  • 헬스 체크 API: 서비스 헬스를 반환하는 끝점을 표출한다.

  • 로그 수집: 서비스 활동을 로깅하면서 검색/경고 기능이 구현된 중앙 로그 서버에 로그를 출력한다.

  • 분산 추적: 각 외부 요청에 ID를 하나씩 붙여 서비스 사이를 드나드는 과정을 추적한다.

  • 예외 추적: 예외 중복 제거, 개발자 알림, 예외별 해결 상황 추적 등을 수행하는 예외 서비스에 예외를 보고한다.

  • 애플리케이션 지표: 서비스는 카운터, 게이지 등 지표를 유지하고, 수집한 데이터를 지표 서버에 표출한다.

  • 감사 로깅: 사용자 액션을 로깅한다.

헬스 체크 API 패턴

서비스 인스턴스는 자신이 요청을 처리할 수 있는 상태인지 여부를 배포 인프라에 알려야 한다. 배포 인프라가 호출 가능한 헬스 체크 끝점을 서비스에 구현하는 것이 좋은 방법이다. 자바 진영 스프링 부트 액추에이터는 끝점 호출 시 서비스 상태가 정상이면 200, 그 외에는 503이라는 상태 코드를 반환하는 라이브러리다.

헬스 체크 요청 핸들러는 서비스 인스턴스 및 외부 서비스의 접속 상태를 테스트한다. db에도 주기적으로 테스트 쿼리를 전송한다.

헬스 체크 끝점 구현

스프링 부트 액추에이터는 JDBC 데이터 소스를 사용하는 서비스에는 테스트 쿼리를 실행하고, RabbitMQ 메시지 브로커를 쓰는 서비스에는 RabbitMQ 서버가 가동 중인지 확인하는 헬스 체크 로직을 자동으로 구성한다.

로그 수집 패턴

로그 파일이 API 게이트웨이와 여러 서비스에 흩어져 있는 상황에서 필요한 로그 항목을 어떻게 끌어모을 수 있을까? 정답은 로그 수집이다. 모든 서비스 인스턴스가 남긴 로그를 로그 수집 파이프라인을 통해 중앙 로깅 서버로 보낸다.

서비스 로그 생성

로그 수집 인프라

로깅 인프라는 로그를 수집, 저장한다. 사용자는 이렇게 저장된 로그를 검색할 수 있다.

  • 일래스틱서치: 로깅 서버로 쓰이는 텍스트 검색 지향 NoSQL DB
  • 로그스태시: 서비스 로그를 수집하여 일래스틱서치에 출력하는 로그 파이프라인
  • 키바나: 일래스틱서치 전용 시각화 툴

분산 추적 패턴

분산 추적은 요청을 처리할때마다 서비스 호출 트리 정보를 기록한다. 서비스가 외부 요청을 처리하며 어떤 상호 작용을 했는지, 어느 지점에서 얼만큼 시간을 썼는지 파악할 수 있다.

지표 서비스에 지표 전달

서비스는 수집한 지표를 푸시 또는 풀 방식으로 메트릭스 서비스에 전달한다. 푸시 모델은 서비스 인스턴스가 API를 호출하여 메트릭스 서비스에 지표를 밀어 넣는 방법이다. (예: AWS 클라우드워치)

풀 모델은 메트릭스 서비스가 서비스 API를 호출하여 서비스 인스턴스에서 지표를 당겨 오는 방법이다. (예: 오픈 소스 모니터링)

서비스 개발: 마이크로서비스 섀시 패턴

예외 추적, 로깅, 헬스 체크, 외부화 구성, 분산 추적 등의 횡단 관심사를 처리하는 프레임워크를 기반으로 서비스를 구축한다.

마이크로서비스 섀시

12장. 마이크로서비스 배포

요즘 애플리케이션은 보통 다양한 프로그래밍 언어와 다양한 프레임워크로 작성된 서비스가 수십 개, 수백 개에 이른다. 서비스 하나가 곧 작은 애플리케이션이니 사실상 애플리케이션을 수십 개, 수백 개 운영하는 셈이다. 따라서 고도로 자동화된 배포 프로세스/ 아키텍처가 있어야 한다.

서비스 배포: 언어에 특정한 패키징 포맷 패턴

언어에 특정한 패키지로 배포하는 방법이 있다. 프로덕션에 배포할 코드와 필요한 런타임을 모두 언어에 특정한 패키지 (JAR/WAR 파일)에 넣고 배포하는 것이다. Node.js로 개발한 서비스라면 소스 코드와 모듈 디렉토리를 배포할 것이다.

음식점 서비스를 배포하려면 우선 필요한 런타임(JDK)을 설치해야 한다. WAR 파일로 배포하려면 웹 컨테이너도 설치해야 한다. 패키지를 머신에 복사하고 서비스를 시동하면, 서비스 인스턴스는 개별 JVM 프로세스로 실행된다.

서비스를 프로덕션에 자동 배포하는 배포 파이프라인을 구축하는 것이 가장 이상적이다.

언어에 특정한 패키징 포맷의 장단점

장점:

  • 배포가 빠르다.
  • 리소스를 효율적으로 활용할 수 있다.
    단점:
  • 기술 스택을 캡슐화할 수 없다.
  • 서비스 인스턴스가 소비하는 리소스를 제한할 방법이 없다.
  • 여러 서비스 인스턴스가 동일 머신에서 실행될 경우 서로 격리 불가
  • 서비스 인스턴스를 어디에 둘지 자동으로 결정하기 어렵다.

서비스 배포: 가상 머신 패턴

서비스를 AMI(Amazon Machine Image)로 묶어 배포하는 요즘 방식이 낫다. 각 서비스 인스턴스는 AMI로부터 생성된 EC2 인스턴스다. EC2 인스턴스는 정상 인스턴스를 적당한 개수만큼 작동시키는 AWS 오토스케일링 그룹으로 관리한다.

AWS의 일래스틱 빈스토크를 사용하면 서비스를 쉽게 VM으로 배포할 수 있다. 일래스틱 빈스토크는 실행 코드를 WAR 파일로 묶어 업로드하면, 서비스를 부하 분산된 하나 이상의 매니지드 EC2 인스턴스로 배포한다.

가상 머신 패턴의 장단점

장점:

  • VM 이미지로 기술 스택 캡슐화
  • 서비스 인스턴스가 격리
  • 클라우드 인프라 활용

단점:

  • 리소스를 효율적으로 활용 불가능
  • 배포가 비교적 느리다.
  • 시스템 관리 오버헤드 발생

서비스 배포: 컨테이너 패턴

OS 수준에서 가상화한 메커니즘이다. 컨테이너는 다른 컨테이너들과 격리된 샌드박스에서 하나의 프로세스로 실행된다.

프로세스 입장에서 컨테이너는 마치 자체 머신에서 실행되는 것처럼 실행된다. 또 교유한 IP 주소를 갖고 있으므로 포트 충돌 가능성도 없고, 컨테이너마다 자체 루트 파일 시스템을 갖고 있다. 가장 유명한 제품은 도커다.

배포 파이프라인은 빌드 타임에 이미지 빌드 툴로 서비스 코드 및 이미지 디스크립션을 일고 컨테이너 이미지를 생성한 후, 레지스트리에 보관한다. 런타임에는 레지스트리에서 컨테이너 이미지를 당겨 와 컨테이너를 생성한다.

서비스를 도커로 배포

서비스를 컨테이너로 배포하면 반드이 컨테이너 이미지로 묶어야 하낟. 컨테이너 이미지는 애플리케이션과 서비스 구동에 필요한 모든 소프트웨어로 구성된 파일 시스템 이미지다. 더 가벼운 이미지도 잇지만, 이미지는 대부분 온전한 리눅스 루트 파일 시스템이다.

도커 이미지 빌드

이미지를 빌드하는 첫 단께는 도커 컨테이너 이미지를 빌드하는 방법이 기술된 도커파일을 생성하는 것이다. 기초 컨테이너 이미지를 지정하고 소프트웨어 설치 및 컨테이너 구성에 관한 커맨드를 죽 나열한 후, 컨테이너 생성 시 실행할 셸 커맨드를 기재한다.

컨텍스트는 도커파일 및 이미지를 빌드하기 위해 사용되는 파일들로 구성된다. build 커맨드로 도커 데몬에 컨텍스트를 업로드하면 도커 데몬이 이미지를 빌드한다.

도커 이미지를 레지스트리에 푸시

빌드 프로세스의 최종 단계는 새로 빌드된 도커 이미지를 레지스트리에 푸시하는 것이다. 도커 레지스트리는 자바 라이브러리가 집합된 메이븐 저장소나 Node.js 패키지가 모여 있는 npm 레지스트리 같은 것이다. 도커허브는 대표적인 퍼블릭 레지스트리다.

도커 컨테이너 실행

서비스를 컨테이너 이미지로 패키징한 후에는 하나 이상의 컨테이너를 생성할 수 있다. 컨테이너 인프라는 이미지를 레지스트리에서 프로덕션 서버로 당겨 오고, 이 이미지로부터 컨테이너를 하나 이상 만든다. 각 컨테이너가 바로 하나의 서비스 인스턴스인 셈이다.

도커 컴퐂를 사용하면 여러 컨테이너를 하나의 그룹으로 묶어 시동/중지 할 수 있다. 하지만 도커 컴포즈 역시 단일 머신에 국한되는 것이 문제다. 쿠버네티스처럼 여러 머신을 하나의 리소스 풀로 전환해주는 도커 오케스트레이션 프레임워크가 필요하다.

컨테이너 패턴의 장점

장점:

  • 기술 스택의 캡슐화
  • 서비스 인스턴스 격리
  • 서비스 인스턴스의 리소스 제한
    단점:
  • 직접 관리해야 하는 부담
  • OS와 런타임 패치 정기적

FTGO 애플리케이션 배포: 쿠버네티스

쿠버네티스느느 도커 오케스트레이션 프레임워크로서, 도커를 기반으로 여러 머신을 하나의 서비스 실행 리소스 풀로 전환하는 소프트웨어 계층이다. 또 서비스 인스턴스나 머신이 깨지더라도 항상 서비스 인스턴스별 개수가 원하는 만큼 실행되도록 유지한다.

쿠버네티스 개요

쿠버네티스 같은 도커 오케스트레이션 프레임워크 주요 기능

  • 리소스 관리: 여러 머신을 CPU, 메모리, 스토리지 볼륨을 묶어 놓은 하나의 리소스 풀로 취급한다.

  • 스케줄링: 컨테이너를 실행할 머신을 선택한다. 스케줄링은 기본적으로 컨테이너의 리소스 요건 및 노드별 리소스 상황에 따라 결정된다.

  • 서비스 관리: 마이크로서비스에 직접 매핑되는 서비스를 명명하고 버저니한다. 정상 인스턴스를 항상 적정 개수만큼 가동시키고 요청 부하를 인스턴스에 고루 분산한다.

쿠버네티스 아키텍처

쿠버네티스는 머신 클러스터에서 실행된다. 쿠버네티스 클러스터의 머신은 마스터, 노드 둘 중의 하나이다. 클러스터는 대부분 소수의 마스터와 하나 이상의 노드로 구성된다. 마스터는 클러스터를 관장하며, 노드는 하나 이상의 파드를 실행하는 워커다. 파드는 여러 컨테이너로 구성된 쿠버네티스의 배포 단위이다.

마스터는 다음 컴포넌트를 실행한다.

  • API 서버: kubectl CLI에서 사용하는 서비스 배포/관리용 REST API
  • etcd: 클러스터 데이터를 저장하는 키-값 NoSQL DB
  • 스케줄러: 파드를 실행할 노드를 선택
  • 컨트롤러 관리자: 컨트롤러를 실행하낟. 컨트롤러는 클러스터가 원하는 상태가 되도록 제어한다.

노드는 다음 컴포넌트를 실행한다.

  • 큐블릿: 노드에서 실행되는 파드를 생성/관리한다.
  • 큐브 프록시: 여러 파드에 부하를 분산하는 등 네트워킹 관리를 한다.
  • 파드: 애플리케이션 서비스

쿠버네티스 핵심 개념:

  • 파드: 쿠버네티스의 기본 배포 단위. IP 주소, 스토리지 볼륨을 공유하는 하나 이상의 컨테이너로 구성된다.

  • 디플로이먼트: 파드의 선언형 명세이다. 항상 파드 인스턴스를 원하는 개수만큼 실행시키는 컨트롤러이다.

  • 서비스: 클라이언트에 안정된 정적 네트워크 위치를 제공한다. TCP/UDP 트래픽을 하나 이상의 파드에 고루 분산한다. IP주소, DNS 명은 오직 쿠버네티스 내부에서만 접근할 수 있다.

  • 컨피그맵: 하나 이상의 애플리케이션 서비스에 대한 외부화 구성이 정의된 이름-값 쌍의 컬렉션이다.

무중단 배포

쿠버네티스는 파드를 롤링 업데이트한다. 즉 단계적으로 1.1.0.RELEASE 버전을 실행하는 파드를 생성하고 1.0.0.RELEASE 버전을 실행하는 파드를 중지한다. 그런데 쿠버네티스는 영리하게 신 버전이 요청 처리 준비가 완료되기 전에는 구 버전을 중지하지 않는다. 헬스 체크를 해서 파드의 준비 상태를 확인한다.

만약 문제가 생겨 1.1.1.RELEASE 파드가 시동하지 않는다면, 진퇴양난이다. 해결 방법은 두 가지다. YAML 파일을 정정해서 재실행 후 디플로이먼트를 업데이트하거나, 아예 디플로이먼트를 롤백한다.

배포와 릴리스 분리: 서비스 메시

새 버전의 서비스를 시작하기 전에 스테이징 환경에서 테스트하고, 문제가 없으면 구 서비스 인스턴스를 새 서비스 인스턴르소 롤링 업데이트를 하고.. 기존에는 이런식으로 프로덕션 배포를 했다. 스테이징 환경에서 테스트를 통과한 서비스라면 프로덕션에서도 잘 동작하리라 예상한 것이다. 그러나 현실은 그렇지 않다.

운영 환경은 스테이징 환경보다 훨씬 더 용량이 큰 트래픽을 처리하므로 스테이징을 정확히 운영과 동일한 레플리카로 맞추기는 어렵다.

그래서 배포와 릴리스를 따로 분리하는 것이 상책이다.

  • 배포: 운영 환경에서 실행
  • 서비스 릴리스: 최종 사용자에게 서비스를 제공

다음 5단계를 거쳐 서비스를 프로덕션에 배포하자.

  1. 최종 사용자 요청을 서비스에 라우팅하지 않고 새 버전의 서비스를 프로덕션에 배포한다.
  2. 프로덕션에서 새 버전을 테스트한다.
  3. 소수의 최종 사용자에게 새 버전을 릴리즈하낟.
  4. 모든 운영 트래픽을 소화할 때까지 점점 더 많은 사용자에게 새 버전을 릴리즈한다.
  5. 어딘가 문제가 생기면 곧장 구 버전으로 되돌린다. 새 버전이 정확히 잘 돌아간다는 확신이 들면 구 버전을 삭제한다.

기존에는 이런 식으로 배포와 릴리스를 분리하는 일 자체가 너무 방대해 엄두가 나지 않았다. 지금은 서비스 메시 덕분에 훨씬 수월해졌다. 서비스 메시는 한 서비스와 다른 서비스, 외부 애플리케이션이 모든 통신을 중재하는 네트워킹 인프라이다.

12.5 서비스 배포: 서버리스 패턴

지금까지의 배포 패턴은 공통점들이 있다. 첫째, 어떤 컴퓨팅 리소스를 사전에 프로비저닝 해야한다. 부하 상태에 따라 VM/컨테이너 개수를 동적 조정하는 자동 확장 기능을 갖춘 배포 플랫폼도 있지만, 어떤 VM 이든, 컨테이너든 준비하는 비용은 지불해야한다.

둘째, 사람이 직접 시스템을 관리해야한다. 어떤 머신에서 가동하든 그 OS는 반드시 패치해야하고, 물리 머신일 경우 랙킹, 스택킹하는 작업도 병행해야 한다.

AWS 람다를 이요한 서버리스 배포

AWS 람다는 자바, Node.js, C#, Go, 파이썬을 지원한다. 람다 함수는 대부분 AWS 서비스를 호출하여 요청을 처리하는 무상태 서비스다. 예를 들어 어떤 이미지가 S3 버킷에 업로드될때마다 DynamoDB의 IMAGES 테이블에 데이터를 삽입하고 키네시스에 메시지를 발행해서 이미 처리를 트리거하는 람다 함수를 호출하는 식이다.

서비스를 배포하려면 우선 애플리케이션을 ZIP 또는 ZAR 파일로 묶고 AWS 람다에 업로드 한다. 그런 다음 요청을 처리할 함수명을 지정한다. AWS 람다는 들어온 요청을 철히하기에 충분한 개수만큼 마이크로서비스 인스턴스를 자동 실행한다. AWS 사용자는 요청별 소요 시간 및 메모리 사용량에 해당하는 비용만 지불하면 된다. AWS 람다도 물론 만능은 아니고 나름대로 한계가 있지만, 개발자와 개발 조직 누구도 서버, 가상 머신, 컨테이너 관련 부분을 신경 쓸 필요가 없다는 점에서 매우 강력하다.

서버리스 배포
퍼블릭 클라우드에서 제공하는 서버리스 배포 메커니즘을 이용하여 서비스를 배포한다.

람다 함수 호출

람다 함수 호출 방법 네 가지

  • HTTP 요청
    API 게이트웨이는 람다 함수를 HTTPS 끝점으로 표출하고, HTTP 요청 객체가 들어오면 이를 람다 함수로 전달하여 HTTP 응답 객체를 반환하는 HTTP 프록시 역할을 한다.
  • AWS 서비스에서 생성된 이벤트
    AWS에서 생성된 이벤트를 람다 함수가 처리하도록 트리거한다.

  • S3 버킷에 객체가 생성된다.

  • DynamoDB 테이블의 데이터 항목이 생성, 수정, 삭제된다.

  • 키네시스 스트림에서 메시지를 읽을 준비가 된다.

  • SES를 통해 이메일을 수신한다.

AWS 람다는 다른 AWS 서비스와 완벽하게 연계되므로, 광범위한 태스크 수행에 유리하다.

  • 람다 함수 스케줄링
    스케줄러로 람다 함수가 주기적으로 호출되도록 설정

  • API를 직접 호출
    웹 서비스를 요청할 때 람다 함수명과 입력 이벤트 데이터를 지정하고, 람다 함수를 동기/비동기 호출

람다 함수의 장점

  • 다양한 AWS 서비스와의 연계
  • 시스템 관리 업무가 많이 경감됨
  • 탄력성
  • 사용량만큼 과금

람다 함수의 단점

  • 긴-꼬리 지연: 동적 실행하므로 AWS가 애플리케이션 인스턴스를 프로비저닝하고 시동하기까지 시간이 걸린다.

  • 제한된 이벤트/요청 기반 프로그래밍 모델: 처음부터 실행 시간이 긴 서비스를 배포할 용도는 아니다.

REST 서비스 배포: AWS 람다 및 AWS 게이트웨이

AWS 람다 함수는 웹 애플리케이션도 아니고 main() 메서드가 있는 자바 애플리케잇녀도 아니다. 요청 핸들러에 필요한 디펜던시를 주입하는 클래스에서, SpringApplication.run()으로 ApplicationContext 생성 후, 최초로 들어온 요청을 처리하기 전에 디펜던시를 자동 와이어링한다.

13장. 마이크로서비스 리팩터링

절대 한번에 모놀리스를 뜯어 내려고 하지마라. 아마존은 모놀리스를 마이크로서비스 패턴으로 전환하는데 2년이 걸렸다.

모놀리스 -> 마이크로서비스 리팩터링 전략

  1. 새 기능을 서비스로 구현한다.
  2. 표현 계층과 백엔드를 분리한다.
  3. 기능을 여러 서비스로 추출해서 모놀리스를 분해한다.

표현 계층과 백엔드를 분리한다.

엔터프라이즈 애플리케이션은 일반적으로 다음 세 계층으로 구성된다.

  • 표현 계층: HTTP 요청을 처리해서 웹 UI에 전달한 HTML 페이지를 생성하는 모듈로 구성된다. 사용자 인터페이스가 정교한 애플리케이션은 표현 계층이 코드 대부분을 차지한다.

  • 비즈니스 로직: 엔터프라이즈 애플리케이션 특성상 복잡한 비즈니스 규칙이 구성된 모듈로 구성된다.

  • 데이터 접근 로직: DB, 메시지 브로커 등 인프라 서비스에 접근하는 모듈로 구성된다.

비즈니스 계층에는 비즈니스 로직을 캡슐화한, 하나 이상의 퍼사드로 구성된 대단위 API가 있다. 이 API가 바로 모놀리스를 더 작은 두 애플리케이션으로 쪼갤 수 있는 틈새에 해당한다.

즉, 표현 계층이 포함된 애플리케이션 A와 비즈니스/데이터 접근 로직이 포함된 애플리케이션 B로 나누는 것이다. 분리한 후에는 표현 계층 애플리케이션 A가 비즈니스 계층 애플리케이션 B를 원격 호출한다.

하지만 이 방식은 한계가 있다. 둘 중 하나는 모놀리스가 될 수 있다.

기능을 여러 서비스로 추출한다

모놀리스가 가진 비즈니스 능력을 하나씩 서비스로 옮기는 분해 전략을 구사해야 한다. 비즈니스에 가장 중요하고 계속 발전하는 서비스를 가장 먼저 추출하는 것이 좋다.

도메인 모델 분리

서비스를 추출하려면 먼저 모놀리스 도메인 모델에서 서비스의 도메인 모델을 추출한다.

서비스 경계에 걸쳐 있는 객체 레퍼런스를 제거하는 일은 어려운데, DDD 애그리거트 방식으로 생각하면 편하다. 애그리거트는 객체 레퍼런스 대신 기본키로 서로를 참조하기 때문이다.

인증/인가 처리

마이크로서비스 애플리케이션은 JWT 같은 토큰 형태로 사용자 신원을 전달합니다. 인-메모리 세션 상태를 관리하고 스레드 로컬을 활용하여 사용자 신원을 전달하는 기존 모놀리식 애플리케이션과는 완전히 다르다. 결국 모놀리식 보안 메커니즘과 jWT 기반의 보안 메커니즘을 동시에 지원하는 일이 관건이다.

로그인 핸들러가 ID/역할 등의 사용자 정보가 포함된 추가 쿠키(USERINFO)를 반환하면, 브라우저는 이후 모든 요청에 이 쿠키를 넣어 보낸다. API 게이트웨이는 쿠키에서 이 정보를 추출하여 HTTP 요청에 포함시켜 서비스를 호출하고, 서비스는 자신이 필요한 사용자 정보를 꺼내 쓸 수 있다.

0개의 댓글