contra

<마이크로 서비스 패턴> 독서 정리

2021-04-27

프로세스 간 통신

메시지 브로커

브로커마다 일장일단이 있다. 지연 시간이 매우 짧은 브로커는 메시지 순서가 유지되지 않거나 메시지 전달이 보장되지 않는다. 아니면 메모리에만 메시지를 저장. 반대로 메시지 전달을 보장하고 메시지를 디스크에 확실히 저장하는 브로커는 지연 시간이 긴 편. AWS SQS는 발행-구독 채널이 없고 점대점 채널만 있다.

메시지 큐에서 메시지 순서 유지

메시지 큐에서 여러 수신자에게 메시지를 발행하는 경우, 전달 순서가 꼬일 수 있다. 주문 생성, 취소, 생성 이렇게 보내면 수신자가 여럿이서 처리하다 보면 취소가 먼저 처리될 수도 있다. 이럴때는 샤딩된 채널을 사용한다. 각 샤딩된 채널들의 샤드는 채널처럼 작동한다. 샤드 키를 해싱한 값으로 샤드를 지정하고, 그러면 같은 샤드 키를 가진 메시지는 항상 똑같은 샤드에게만 전달될것이다.(아파치 카프카 용어로 컨슈머 그룹이라 한다) 샤드 키를 orderId 로 지정하고, 샤딩 함수를 샤드키 값을 샤드 갯수로 나눈 나머지를 구하도록 설정하면 주문 별 이벤트는 각각 동일한 샤드에 발행되고, 어느 한 컨슈머 인스턴스만 메시지를 읽기 때문에 메시지 처리 순서가 보장된다.

중복 메시지 처리

메시지를 딱 한 번 만 전달하는 것도 문제다. 어렵기 때문에 대부분은 한 번 이상 전달되겠노라고 약속한다. 클라이언트네트워크브로커의 실패로 메시지가 한 번 이상 전달될 수 있다. 브로커에게서 메시지를 받고 처리가 끝난 후 ack를 보내기 전에 클라이언트가 갑자기 멎을 수 있다. 또는, 주문 생성됨->주문 취소됨 순서로 처리되는데, 주문 생성됨 이벤트 ack를 못 받을 수도 있다. 나중에 브로커가 주문 생성됨 이벤트만 재전송하면 주문 취소가 undo 될 수도 있다.

중복 메시지를 처리하는 방법

  • 멱등한 메시지 핸들러
  • 메시지를 추적하고 중복을 솎아내기

멱등한 메시지 핸들러

메시지를 여러번 전달해도 결과가 바뀌지 않는 경우. 하지만 이런 경우는 별로 없다.

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

신용카드를 승인하는 메시지 핸들러라면, 반드시 주문별로 1회만 승인해야 한다. 메시지 id를 중복 insert 불가한 table에 넣도록 하면 막을 수 있다.

트랜잭셔널 메시징

서비스는 보통 db를 업데이트하는 트랜잭션의 일부로 메시지를 발행한다. db 업데이트와 메시지 전송을 한 트랙잭션으로 묶어야 한다. db와 브로커에 분산 트랜잭션을 적용했던 적도 있지만, 요즘 애플리케이션에 어울리지도 않을 뿐더러 카프카 같은 브로커는 분산 트랜잭션 지원하지 않는다. 어떻게 하면 메시지를 확실하게 발행할 수 있을까?

db큐 (트랜잭셔널 아웃박스 패턴)

주문 서비스 -> order table cud -> outbox table insert <- message relay(중계기) -> 메시지 브로커에 발행

Db큐에서 어떻게 메시지 브로커로 전달해줄까?

  • 테이블 폴링
  • db의 트랜잭션 로그를 계속 읽어서 하나씩 메시지 브로커로 전달

    • 디비지움
    • 링크드인 데이터버스
    • 다이나모db 스트림즈
    • 이벤추에이트 트램

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

rest api는 가장 직관적이지만, 동기 프로토콜이라는 단점이 있다. rest건 비동기 메시징이건 어떤 서비스가 다른 서비스의 응답을 받은 이후에 자신의 클라이언ㄴ트에게 응답하는 구조라면 가용성은 떨어진다. 그러므로 가용성을 최대화하려면 동기 통신을 최소화 해야 한다.

비동기 상호 작용

모든 api를 비동기적으로 만든다. 주문 생성 요청 -> 큐(채널) <- 주문 서비스 이런 식으로, 모든 요청을 큐를 통해 주고받도록 한다. 하지만 동기적인 api를 쓸 수 밖에 없을 수도 있다. 외부 api 같은 케이스. 이럴 때는 데이터 복제를 사용한다.

데이터 복제

서비스 요청 처리에 필요한 복제본으로 처리하는 방법. 변경이 있을 때마다, 변경 이벤트를 받아서 자신의 레플리카를 업데이트 시켜준다. 하지만 대용량의 복제본을 만드는 것은 비효율적. 다른 서비스가 소유한 데이터를 업데이트 하는 문제도 데이터 복제만으로 해결되지 않는다. 한 가지 해결 방법은, 자신의 클라이언트에 응답하기 전까지 다른 서비스와의 상호 작용을 지연시키는 것.

응답 반환 후 마무리

일단 주문을 pending 으로 생성한다. 주문을 검증하고 기타등등 결제를 하고 하는 작업은 모두 비동기로 뒤에서 처리한다. 클라이언트가 주문이 진짜 성공했는지 알려면 주문을 주기적으로 폴링하거나 주문 서비스가 알림 메시지를 보내주어야 한다. 복잡하게 들리지만 이것이 더 나은 방향이다. 다음 장에서 다룰 분산 트랙잭션 관리 이슈도 이 방법으로 해결할 수 있다.

요약

  • IPC 중요하다
  • api 버저닝 중요하다
  • 요청 시 타임아웃을 설정하여 잔존 요청 수를 제한하고, 서킷 브레이커를 이용하여 실패한 서비스가 호출되지 않도록 막아야 한다

    Saga

    마이크로서비스의 데이터 일관성 유지 패턴. 마이크로서비스 아키텍처에서 분산 트랜잭션 없이 데이터 일관성을 유지할 수 있게 해준다. 사가는 비동기 메시징을 이요해 편성한 일련의 로컬 트랜잭션. 로컬 트랜잭션마다 커밋하므로 롤백이 필요할 때에는 보상 트랜잭션을 걸어 롤백해야 한다.

    주문 사가

  • 주문 서비스: 주문을 APPROVAL_PENDING 상태로 생성
  • 소비자 서비스: 주문 가능한 소비자인지 확인
  • 주방 서비스: 주문 내역을 확인하고 티켓을 CREATE_PENDING 상태로 생성
  • 회계 서비스: 소비자 신용카드를 승인
  • 주방 서비스: 티켓 상태를 AWAITING_ACCEPTANCE 로 변경
  • 주문 서비스: 주문 상태를 APPROVED 로 변경 트랜잭션 Tn에 대응하는 롤백 보상 트랜잭션 Cn이 존재. 언두하고 싶으면 Cn-1 을 차례로 실행.

사가 편성 로직: 일반 트랜잭션과 보상 트랜잭션의 순서화

  • 코레오그래피: 의사 결정과 순서화를 사가 참여자에게 맡김. 참여자끼리 이벤트 교환 방식으로 통신
  • 오케스트레이션: 사가 편성 로직을 사가 오케스트레이터에게 집중화. 사가 참여자에게 메시지를 보내 수행할 작업을 지시

비즈니스 로직 설계

  1. 도메인 모델은 대부분 상호 연관된 클래스가 거미줄처럼 뒤얽혀 있음
  2. 클래스가 여러 서비스에 산재된 마이크로서비스 아키텍처에서는 서비스 경계를 넘나드는 객체 레퍼런스를 제거해야 함
  3. 사가 패턴을 적용할 수 있는 비즈니스 로직을 설계해야 함

DDD 애그리거트 패턴으로 해결할 수 있다. 애그리거트는 한 단위로 취급 가능한 객체를 모아 놓은 것.

  • 애그리거트를 사용하면 객체 레퍼런스가 서비스 경계를 넘나들 일이 없음. 객체 참조 대신 PK를 이용하여 애그리거트가 서로 참조
  • 한 트랜잭션으로 하나의 애그리거트만 생성/수정할 수 있음. 따라서 애그리거트는 마이크로서비스 트랜잭션 모델의 제약 조건에 잘 맞음 따라서, acid 트랜잭션은 반드시 하나의 서비스 내부에만 걸리게 됨.

비즈니스 로직은 서비스에서 가장 복잡한 부분. 비즈니스 로직을 객체지향적으로 개발할 것인가, 절차적인 방법으로 개발할 것인가는 가장 중요한 의사 결정 항목.

  1. 절차적 트랜잭션 스크립트 패턴 비즈니스 로직이 너무 간단해서, 객체 지향 방식이 너무 지나칠 때 선택할 수 있다. 동작을 하는 클래스와 상태를 가진 클래스를 나누고 절차적으로 프로그램 코딩.
  2. 객체 지향적 도메인 모델 패턴 비즈니스 로직을 상태와 동작을 가진 클래스로 구성된 객체 모델로 구성한다. 상태/동작을 모두 갖고 있는 클래스가 잘 설계된 클래스이다.

도메인 모델 패턴을 적용하면 트랜잭션 스크립트 패턴때보다 서비스 메서드가 단순해짐. 서비스 메서드가 거의 항상 비즈니스 로직이 잔뜩 포함된 영속화 도메인 객체에 위임하기 때문.

객체 지향 설계의 장점

  1. 설계를 이해/관리하기 쉬움 하나의 거대한 클래스 대신 소수의 책임만 맡은 아담한 클래스 여럿으로 구성
  2. 테스트하기 쉬움
  3. 잘 알려진 설계 패턴을 응용할 수 있기 때문에 확장하기 쉬움(전략 패턴, 템플릿 메서드 패턴) 마이크로 서비스 아키텍쳐에서는 DDD가 더 나음

DDD

각 서비스는 자체 도메인 모델을 가지며, 어플리케이션 전체 도메인 모델의 문제점을 방지할 수 있음.

  • 엔터티: 영속적 신원을 가진 객체.
  • 밸류 객체: 여러 값을 모아놓은 객체
  • 팩토리: 생성자로 만들기 복잡한 객체 생성 로직이 구현된 객체 또는 메소드
  • 레포지토리: 엔터티를 저장하는 db 접근 로직을 캡슐화한 객체
  • 서비스: 엔터티, 밸류 객체에 속하지 않은 비즈니스 로직 구현 객체
  • 애그리거트

DDD 애그리거트 패턴

전통적인 객체지향 패턴은 비즈니스 객체들의 경계가 불분명하다. 어느 클래스가 Order 라는 비즈니스 객체의 일부인지 분명하지 않다. Order에 어떤 작업을 수행한다고 하자. 주문 품목, 지불 정보 등 다른 연관된 작업들도 함께 하고 있다. 따라서 개발자가 도메인 객체의 경계를 짐작하기가 힘들다. 경계가 불분명하면 마이크로서비스 아키텍처에서 문제가 생길 가능성이 높다. Order에서 최소 주문량이 있다고 하자. 두 소비자가 주문을 하는 동시에 본인의 예산을 초과했는지 결정한다고 해보자. 두 사람은 주문 단가를 낮추기 위해 품목을 수정하고, 각자 입장에서 보면 최소 주문량은 그래도 충족된다고 생각할 것이다. 하지만 낙관적 락 때문에 동시성 문제가 발생하고, 최소 주문 수량 이하로 내려갔지만 그래도 앱은 최소 구매 수량을 충족한다고 착각하게 된다.

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

애그리거트: 한 단위로 취급 가능한 경계 내부의 도메인 객체. 하나의 루트 엔터티와 하나 이상의 기타 엔터티 + 밸류 객체로 구성됨. 비즈니스 객체는 대부분 애그리거트로 모델링함. 주문, 소비자, 음식점 같은 명사가 바로 애그리거트. 일부가 아니라 전체 애그리거트를 업데이트 하므로 좀 전에 설명한 일관성 문제가 해결됨. 업데이트 작업은 애그리거트 루트에서 호출되기 때문에 불변 값이 강제됨. 동시성 역시 애그리거트 루트의 버전 번호나 db수준의 락으로 잠금하여 처리. 클라이언트가 직접 주문 수량을 수정할 수 없고 반드시 주문 애그리거트 루트에 있는 메서드를 호출해야 하기 때문에 최소 주문량같은 불변값이 강제되는 원리. 하지만 db에 있는 전체 애그리거트를 업데이트 할 필요는 없다. Order 객체와 수정된 OrderLineItem에 해당하는 로우만 업데이트 할 수도 있다.

도메인 이벤트 생성 및 발행

도메인이 발행하는 이벤트. 주문 생성됨. 서비스 클래스에서 직접 발행하거나, 또는 애그리거트 루트 안에서 발생한 이벤트를 모아놨다가 서비스 클래스에서 불러다가 한꺼번에 발행할 수도 있다. 도메인 이벤트는 결국 메시지로 바뀌어 아파치 카프카 같은 메시지 브로커에 발행됨. 예를 들어, 음식점 메뉴가 갱신될 때마다 음식점 서비스가 발행하는 이벤트를 구독하는 컨슈머. 주방 서비스의 레플리카 데이터를 항상 최신으로 유지하게 됨.

주방 서비스 비즈니스 로직

주방 서비스: 음식점이 주문을 관리할 수 있게 해주는 서비스.

메인 애그리거트

  • Restaurant
  • Ticket

    인바운드 어댑터

  • REST API: 음식점 점원이 사용하는 UI가 호출하는 api.
  • KitchenServiceCommandHandler: 사가가 호출하는 비동기 요청응담 api. KitchenService를 호출하여 Ticket을 생성수정
  • KitchenServiceEvnetConsumer: RestaruantService가 발행한 이벤트를구독. KitchenService를 호출하여 Restaurant를 생성/수정

    아웃바운드 어댑터

  • DB Adapter
  • DomainEventPublishingAdapter

    주문 서비스 비즈니스 로직

    메인 애그리거트

  • Order
  • Restaurant

    사가

  • OrderSevice
  • OrderRepository
  • RestaurantRepository
  • CreateOrderSaga

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

기존 ORM 로 영속화 할 때 문제

  1. 객체-관계 임피던스 부정합
  2. 애그리거트 변경 이력이 없다
  3. 감사 로깅은 구현하기 힘들고 오류도 자주 발생한다
  4. 이벤트 발행 로직이 비즈니스 로직에 추가된다 이 같은 문제를 해결하기 위해 “이벤트 소싱” 이라는 솔루션이 있다.

이벤트 소싱

기존에는 애그리거트=테이블, 필드 = 컬럼, 인스턴스 = 로우로 맵핑했지만, 이벤트 소싱 방식은 애그리거트를 db에 있는 이벤트 저장소에 일련의 이벤트로 저장함. 예를 들어, Order 애그리거트를 이벤트 소싱으로 저장한다면 Order를 ORDER 테이블에 로우 단위로 저장하는게 아니라, Order 애그리거트를 EVENTS 테이블의 여러 로우로 저장함. 각 로우는 주문 생성됨, 주문 승인됨, 주문 배달됨 등의 도메인 이벤트.

애그리거트를 로딩할 때 이벤트 테이블에서 가져와서 재연을 한다. apply한다. reduce 작업. 상태를 변경하는 이벤트에는 단지 id 만 들어있을수도 있고, 상태 변경을 하기 위해 부수적인 데이터도 담을 수 있다. 커맨드 -> 이벤트 생성 -> 이벤트 apply -> 상태 변경 이벤추에이트 프레임워크에서는 process, apply 함수를 각각 제공. process는 커맨드 객체를 받아서 이벤트 목록을 반환. Apply는 이벤트 적용. command 를 받아서 이벤트 목록에 따라 처리하는 하나의 메소드를 process와 apply로 분리하게 된다.

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

낙관적 잠금: 버전 컬럼을 이용하여, 마지막으로 애그리거트를 읽은 이후 변경되었는지 감지. 애그리거트 루트를 version 컬럼이 있는 테이블에 맵핑하고, 애그리거트가 업데이트 될 때마다 update 문으로 값을 하나씩 증가시킴. 이 방법으로 동시 업데이트를 막을 수 있음.

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

마이크로 서비스에서 고려해야 할 문제는 분산 트랜잭션 말고도 분산 데이터를 쿼리하는 것도 중요하다. 여러 서비스에 흩어져 있는 데이터를 쿼리하기가 그리 간단하지 않다.

  1. API 조합 패턴: 서비스 클라이언트가 데이터를 가진 여러 서비스를 직접 호출해서 그 결과를 조합. 가장 단순한 방법으로, 가급적 이걸 쓰자.
  2. CQRS(커맨드-쿼리 책임 분리) 패턴: 쿼리만 지원하는 하나 이상의 뷰 전용 DB를 유지하는 패턴. Api 조합 패턴보다 강력하나 구현하기 복잡함.

API 조합 패턴

findOrders 는 기본키로 주문 정보를 조회함. 주문 내역이 포함된 OrderDetails 객체를 반환함. 주문 상태 뷰가 구현된, 모밸 기기 또는 웹 애플리케이션 등의 프론트엔드 모듈이 이 메서드를 호출함. 마이크로서비스 아키텍처로 전환하면 데이터가 여러 서비스에 뿔뿔이 흩어지게 됨. 클라이언트가 주문 내역을 조회하려면 이런 모든 서비스에 요청을 해야 함.

Api조합 패턴은 데이터를 가진 서비스를 호출한 후 그 반환 결과를 조합해서 가져옴.

  • api 조합기: 프로바이더 서비스를 쿼리하여 데이터를 조회함
  • provider 서비스: 데이터의 부분을 갖고있는 서비스

Api 조합기가 provider를 pk로 조회해서 결과를 조인한다.

해결해야 할 문제

  1. 어느 컴포넌트를 쿼리 작업의 api 조합기로 선정할 것인가?

    1. 서비스 클라이언트: 비효율적
    2. api 게이트웨이: 외부 에서 불리우는 api라면 이 방법이 타당.
    3. api 조합기를 스탠드 얼론으로 구현: 내부적으로 여러 서비스가 사용하는 쿼리라면 동시에 호출하는게 가장 효율적이다. 하지만 어떤 결과는 동기적으로 호출해야 할 경우도 있다. 동기/비동기가 섞인 호출을 관리하는 것은 힘들다. 이럴 때 리액티브 프로그래밍을 도입하면 좋다.

단점: 오버헤드 증가, 가용성 저하, 데이터 일관성 저하(하나는 캔슬됐지만, 캔슬 안된 상태로 다른 데이터를 쿼리하면?)

거대한 데이터 뭉치를 인-메모리 조인으로 할 때는 CQRS 패턴이 적당하다.

  1. 어떻게 해야 효율적으로 취합 로직을 작성할 것인가?

CQRS 패턴

엔터프라이즈 애플리케이션은 여러 db를 최대한 활용한다. 트랜잭션을 걸어 레코드를 관리하고, 텍스트 검색 쿼리는 es나 solr 등을 이용해 구현한다. rdbms와 es를 모두 출력하게 동기화 하기도 하고, 주기적으로 rdb->es로 복사하는 경우도 있다. cqrs는 이런 종류의 아키텍쳐를 일반화한 것이다. 하나 이상의 쿼리가 구현된 하나 이상의 뷰db를 유지하는 기법.

CQRS의 필요성

fun findOrderHistory(consumerId, OrderHistoryFilter) 함수. 파라메터에 매칭된 다수의 항목 반환함. OrderHistoryFilter = 필터 조건. 어느 시점 이후 주문까지 반환할 것인가(필수), 주문 상태(옵션), 검색할 키워드(옵션) api 조합기 패턴으로는 이런 대량 데이터를 각 서비스 별로 쿼리한 이후 join 해서 가져오는게 매우 비효율 적이다.

fun findAvailableRestaurants() 이런 함수는 하나의 서비스에 국한된 쿼리도 가져오기 어렵다. RDBMS의 지리 공간 확장팩이 깔려있는 경우라면 쿼리가 쉬울 것이다. 하지만 해당 db에 그런 확장이 없다면? 데이터도 지리공간에 최적화된 데이터가 아니라면? 음식점 데이터의 레플리카를 전혀 다른 종류의 db에 저장하고 관리해야 한다. 다행히 레플리카를 동기화하는 문제는 cqrs로 해결할 수 있다.

관심사 분리의 필요성

또한, 해당 쿼리는 음식점 서비스에 있는 데이터를 조회하는 쿼리 작업. 음식점명, 주소, 요리, 메뉴, 옾느 시간 등 다양한 음식점에서 관리하는 다양한 속성 갖고 있다. 음식점 서비스에 쿼리를 구현해야 할 것 처럼 보이지만, 데이터 소유권으로 결정할 문제는 아니다.

관심사 분리를 어떻게 해야할지, 한 서비스에 너무 많은 책임을 부여하지 않을 수 있을지, 고민을 해야 한다. 음식점 서비스 개발 팀의 목표는 음식점들이 정보를 잘 관리하게 하는 일이지, 성능이 매우 중요한 대용량 쿼리를 만드는 일이 아니다. 또한, 본인이 잘못 만들 경우 소비자가 주문을 하지 못하게 될까 봐 노심초사 하게 된다. 음식점 쿼리는 주문 서비스 개발 팀에 맡기고, 본인들은 음식점 데이터만 제공하는 구조가 낫다. 해당 쿼리를 구현하기 위해 공간 정보 레플리카 db는 최종 일관된 상태여야 한다.

CQRS 개요

  1. Api를 조합하여 여러 서비스에 흩어진 데이터를 조회하려면 값비싸고 비효율적인 인-메모리 조인을 해야 한다
  2. 데이터를 가진 서비스는 필요한 쿼리를 효율적으로 지원하지 않는 db에, 또는 그런 형태로 데이터를 저장한다
  3. 관심사를 분리할 필요가 있다는 것은 데이터를 가진 서비스가 쿼리 작업을 구현한 장소로 적합하지 않다는 뜻 -> CQRS 패턴 이 해결책!

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

CQRS 패턴과 상관 없이 거의 모든 서비스는 다양한 CRUD 작업이 구현된 api를 갖고있다. 비CQRS 서비스에서는 이런 작업을 보통 db에 매핑된 도메인 모델로 구현함. 성능이 중요한 쿼리는 도메인 모델을 건너뛰고 직접 db에 접속하기도 한다. 하나의 영속적 데이터 모델은 커맨드와 쿼리를 모두 지원한다.

CUD용 db와 쿼리용 db를 따로 둔다.

외부 api 패턴

모바일 앱, 브라우저에서 실행되는 자바스크립트, 제휴사 애플리케이션 등 클라이언트가 호출하는 다양한 api들. 이걸 어떻게 관리할까? 웹 애플리케이션은 방화벽 안에 있어서 빠른 lan으로 연결됨. 하지만 다른 클라이언트는 방화벽 외부에 있어서 비싸고 느린 인터넷으로 통해 접근됨. 클라이언트가 직접 서비스를 호출하도록 할 수도 있다. 하지만 필요한 데이터를 가져오려면 여러 번 요청해야 하고, 효율도 떨어지고 ux가 나빠진다. 클라이언트가 서비스 및 api를 알아야 하는 구조라서 캡슐화가 되지 않고, 나중에 아키텍처와 api를 바꾸기도 어렵다. 클라이언트가 사용하기에 불편한 ipc를 서비스에서 사용할 때가 있다.

FTGO 모바일 클라이언트

소비자는 모바일 클라이언트로 주문 상태, 지불 상태, 음식점 관점에서의 주문 상태, 배달 중일 경우 현재 위치 및 예상 배달 시간 등을 본다. 이걸 한 눈에 볼 수 있는 주문 조회 뷰를 개발한다고 해보자.(CQRS)

연관된 서비스

  • 주문 서비스
  • 주방 서비스
  • 배달 서비스
  • 회계 서비스 모바일 클라이언트가 서비스를 직접 호출하면, 여러번 호출해야 한다. 모바일 앱이 api 조합기가 된 셈이다.

문제점

  1. 클라이언트가 요청을 여러 번 전송하기 때문에 UX가 나빠지며, 오래 걸리고, 배터리도 많이 소모한다
  2. 로직이 캡슐화가 되지 않아 프론트와 백엔드를 함께 수정해야 한다. 모바일 앱은 한번 배포하는데 오래 걸리고, 업데이트를 강제할 수도 없다. 그래서 백엔드 api의 진화에도 지장이 생길 수도 있다.
  3. 클라이언트에 비친화적인 IPC(gRPC, ...)를 사용할 수 없다
다른 클라이언트는?
  1. 웹 여기는 모바일 앱과 마찬가지 문제
  2. 서드파티에게 api를 제공하는 경우 서드파티에게 업데이트를 강요할 수 없다. 그리고 백엔드 팀은 구형 api를 영원히 지원해야 할 수도 있다. 따라서 서드파티 개발자에게 직접 서비스를 표출하는 대신 퍼블릭 api를 따로 제공하는게 낫다. api 게이트웨이라는 아키텍처 컴포넌트로 구현한다.

API Gateway Pattern

MSA에 외부 api 클라이언트들의 진입점에 해당하는 서비스를 구현한다.

역할

  • 요청 라우팅
  • API 조합해서 데이터 한번에 뿌려줌
  • API 게이트웨이는 클라이언트마다 적합한 API를 제공한다 -> 모바일 앱에 맞춘 api 따로, 웹에 맞춘 api 따로, ... BFF 패턴은 클라이언트마다 API를 따로 정의하는 패턴.
  • 다양한 유틸성 기능 구현

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

계층화된 API 모듈

API 게이트웨이는 계층 구조이다. 이 세 가지 API 모듈로 구성된다

  • 모바일 클라이언트 -> 모바일 API -> 공통 계층
  • 브라우저 자바스크립트 클라이언트 -> 브라우저 API -> 공통 계층
  • 서드파티 애플리케이션 -> 퍼블릭 API -> 공통 계층 각 api 모듈은
  • 서비스 api 하나에 매핑되는 작업은 해당하는 각각의 서비스로 요청을 보낸다
  • api를 조합하는 복잡한 작업은 사용자 정의 코드로 구현한다. 해당 코드는 각각 여러 서비스를 호출해서 결과를 조합하는 방식으로 요청을 처리한다.

이 api gateway의 소유권은 누가 갖을까? 전담 팀이 있다면 어떨까? 해당 팀이 병목이 될 수 있다. 그러니 각 웹퍼블릭 api 팀이 소유권을 갖고 관리해야 한다. 또한 같은 레포지토리를 공유하는 만큼, 서로가 서로에게 병목이 되어서는 안된다. 그러니 api 게이트웨이 배포 파이프라인을 완전 자동화 해야 한다. 문제는 같은 레포를 쓰다보니 운영의 책임 소재가 불분명해진다. “빌드한 사람이 책임자다” 라는 마이크로서비스 아키텍처의 철학과 맞지 않게 된다. 해결 방법은 각 클라이언트마다 api gateway를 따로 두는 BFF(Backends For Frontends) 패턴을 적용하는 것이다. 공통 코드를 줄이기 위해, api gateway 팀이 개발한 공유 라이브러리를 사용하고 같은 언어를 쓰는게 좋다.

GraphQL

클라이언트가 원하는대로 백엔드에 쿼리를 할 수 있음. 그래프 기반의 스키마로 서버 API를 구성함.

  • 스키마: 클라이언트가 날릴 수 있는 대략의 쿼리 정의
  • 리졸버 함수: 클라가 보낸 쿼리를 다양한 백엔드 서비스에 맵핑
  • 프록시 클래스: 서비스 호출

마이크로서비스 테스트 1부

마이크로서비스는 다양한 상호 작용 스타일과 IPC로 서로 통신함.

  • REST
  • 도메인 이벤트
  • 커맨드 메시지

두 서비스가 상호 작용하는지 테스트 하는 방법은 두 서비스를 모두 실행하고 통신을 일으키는 API를 호출한 후, 기대한 결과가 나오는지 확인. 하지만 이건 종단간 테스트. 종단간 테스트는 가능하면 작성하지 않는다. 너무 범위가 커서 결과가 못믿음직스럽다. 따라서 빠르고 간단하게 테스트 하려면 “컨슈머 주도 계약 테스트”를 활용해야 한다. API Gateway와 서비스간의 통신 규약이 맞는지 확인하는 테스트를 작성하자. 컨슈머는 api 게이트웨이, 프로바이더는 각 서비스. 프로바이더의 비즈니스 로직을 테스트하지는 않는다.

마이크로서비스 테스트 2부

  • DB 목킹
  • 메시징 테스트 - 계약에 의해서 컨슈머 퍼블리셔 테스트

메시징 테스트

“스프링 클라우드 컨트랙트” 라는 툴 - 컨슈머/프로듀서간 계약을 정의하고 관련 코드 자동 생성까지 해줌.

  • 컨슈머 -> 계약코드가 자동으로 발행해줌. 그걸 잘 읽었는지 테스트
  • 프로듀서 -> 메시지 발행기 호출. 채널로 발행. 계약코드가 자동생성해준 테스트 코드에서 잘 발행됐는지 테스트.