책 스터디 간단 정리 내용

1장 더도 덜도 아닌 딱 마이크로서비스

  • 마이크로서비스는 independent deployability 해야한다.
    • 서비스가 느슨하게 결합 -> 그러려면 서비스 경계를 먼저 찾아야함
    • 경계를 정의하는 방법은 뭘까? -> 매번 고민..
  • 데이터베이스 공유는 하지말자.
  • 모놀리스
    • 모든 코드를 단일 프로세스 하나로 패키징
    • 멀티모듈 방식으로도 가능
      • 모듈 경계 잘 설정하기
  • 잘 정의된 서비스 인터페이스 뒤에 데이터베이스를 숨기면 서비스가 노출 대상의 범위를 제한하고 데이터 표현 방식을 변경할 수 있다
  • 구현 결합도
    • 개발자가 직접 컨트롤 가능
  • 시간적 결합도. 메시지가 전송되는 시점과 메시지가 처리되는 방식
    • 캐시를 사용하면 나름 해결됨
    • 또는 비동기 전송
  • 배포 결합도
    • 전체가 모놀리스면 모든 게 반드시 함께 배포되어야 함
    • 마이크로 서비스로 분해해 배포 범위를 줄여 배포 위험을 낮출 수 있음
    • 범위가 줄어들어서 문제를 찾기도 쉬워짐
  • 도메인 결합도
    • 어떤 도메인에서 어떤 데이터를 공유해야 하는지 고민
  • 이런 비즈니스 도메인을 중심으로 서비스를 모델링하는 구현 방법이 바로 도메인 주도 설계
    • aggregate는 수명 주기가 있으므로 state machine으로 구현할 수 있다
    • bounded context는 구현 세부사항을 숨김. aggregate가 여러 개
    • 둘 다 잘 정의된 인터페이스로서 응집력의 단위를 제공함
    • 그래서 둘 다 서비스의 경계로 작동할 수 있음

마이크로서비스의 목표는 ‘가능한 한 작은 인터페이스를 유지하는 것’

2장 마이그레이션 계획하기

  • 작업에 착수하는 시간이라던지, 배포하는 시간이 느릴 수 있다.
  • 도메인에 낮선 경우, 너무 조기에 시스템을 마이크로서비스로 분해하면 높은 비용이 발생할 수 있다
  • 서비스 2개도 관리하기 버겁다면 마이크로서비스 10개를 관리하는 일은 더 어려울 것이다
  • 모든 사람이 마이크로서비스를 하기 때문에 따라한다는 생각은 끔찍할 뿐이다
  • 부차적인 목표를 위해 행해진 작업이 유용할 수도 있으나, 그것이 핵심 목표를 방해하거나 주의를 흩트린다면 이런 목표들은 뒤로 빠져야 마땅하다.
    • 트래픽 증가 처리를 위해 마이크로서비스 도입과 함께 코틀린 언어 도입 시도, 등등…
  • 부수적인 이익을 위해 새로운 기술을 혼용하지 마라. 당장 직면한 구체적인 문제를 해결하기 위한 기술을 도입하라.
  • 점진적인 마이그레이션을 하라. 분해 과정에서 발생하는 영향을 습득하고 이해하자
  • 직도메인 모델의 관계들을 직접 그려보면서 파악해보자
  • 의존성이 없는 것부터 차례대로 분해
  • 여러 서비스들 중 어떤 서비스가 분해하기 좋을지 우선순위를 매겨보자.
  • 다른 조직이 수행한 작업에서 영감을 얻지만, 다른 누군가에게 맞아 떨어진 이론이 자신의 상황에서도 효과가 있다고 가정해서는 안 된다.
  • 각 단계를 작게 만들면, 매몰 비용 오류의 함정을 피하기가 좀 더 쉬워진다.

일을 시작하기 전 왜 그 일을 해야하는지가 가장 중요하다. 기술지향보다 중요한 건 고객 지향

3장 모놀리스 분할

  • 기존 코드베이스를 먼저 이해하고 모듈식 모노리스로 변경하자
    • 때때로 팀들은 기존 코드 수정만으로 충분한 성과를 거두기에 애당초 마이크로 서비스가 필요하지 않다는 사실을 깨닫게 된다
    • 모놀리스에서 코드를 복사하기를 원하지만, 회소한 당장은 모놀리스 자체에서 이 기능을 제거하고 싶지는 않다는 점이다. 왜일까? 일정 기간 동안 모놀리스의 기능을 그대로 두면 롤백 포인트를 확보하거나, 혹은 두 가지 구현을 병행 실행하는 기회를 얻을 수 있다. 또한 이후에 마이그레이션이 성공적으로 완료되면, 모놀리스에서 기능을 제거할 수 있다.
  • 교살자 무화가 애플리케이션 패턴
    • 새로운 구현이 준비되면, 모놀리스에 대한 호출을 신규 마이크로서비스에 대한 호출로 전환하게끔 라우팅할 수 있어야 한다. (프록시 사용)
    • HTTP를 사용하면 리디렉션 관리르 위한 옵션이 다양함 (nginx 등등…)
    • 사용중인 프로토콜의 마이그레이션을 원한다면, 기존 프로토콜과 새 프로토콜을 모두 지원하는 서비스를 사용해 매핑을 서비스 자체에 적용하는 방식이 훨씬 더 낫다.
      • 기존 프로토콜이 SOAP, 새로운 프로토콜이 gRPC라면 둘 다 받을 수 있도록 만든 후, SOAP -> gRPC 매퍼를 만들면 된다.
    • 메시지큐를 사용할 경우, 동일한 대기열에 대해 더 많은 유형의 컨슈머가 있고 필터링 규칙이 복잡해질수록 문제가 발생할 수 있는 가능성도 커진다.
  • UI 컴포지션 패턴
    • UI를 통해 마이크로서비스 아키텍쳐에서 제공하는 기능을 결합
  • 추상화에 의한 분기 패턴
    • 대체할 기능을 위한 추상화 생성
    • 새로운 추상화를 사용하기 위해 기존 기능을 이용하는 클라이언트를 변경
    • 기능을 대체에 추상화를 새롭게 구현
    • 추상화를 전환
    • 추상화를 정리하고 기존 구현 제거
  • 병행 실행 패턴
    • 기존 시스템과 새로운 시스템으로 병행 실행시킨 후, 결과를 비교하는 방법도 있다 (변경되는 기능의 위험성이 높다고 간주되는 경우 사용)
  • 협업자 데코레이터 패턴
    • 프록시를 사용해 호출을 가로채어 기존 모놀리스에 변경을 가하지 않고 결과를 적용시킬 수 있다. (기존 모놀리스의 결과 응답을 프록시에서 캐치해 다른 마이크로서비스에 보냄)
    • 해당 마이크로서비스가 다시 기존 모놀리스를 호출하지 않도록 주의 (순환 종속성을 일으킬 수 있음)
  • 변경 데이터 캡처 패턴
    • DB에서 변경된 데이터를 캡쳐해 마이크로서비스를 호출할수도 있다 (프로그램을 이해하기 어려워지고 DB 작업에 종속되므로 가급적 하지 말기)
    • DB 트리거를 사용해 호출
    • 트랜잭션 commit 시 트랜잭션 로그를 통해 호출 가능
    • 해당 패턴은 데이터를 복제할 필요가 있을 경우에만 사용하자

서버 앞에 프록시가 있는 게 여러모로 결합도를 낮추는데 도움되는듯 기존 기능은 그대로 둔 채 신규 기능 개발하고 점진적으로 마이그레이션

4장 데이터베이스 분해

  1. 공유 데이터베이스 패턴
    1. 말 그대로 데이터베이스가 공유되고 있는 것
    2. 그러나 각 마이크로서비스가 자체적인 데이터를 소유할 수 있도록 데이터베이스를 분리하는 방식이 거의 항상 선호된다.
    3. 데이터베이스 엔진의 단일 인스턴스는 여러 스키마를 제공할 수 있으며, 각 스키마는 논리적으로 데이터를 격리한다
  2. 데이터베이스 뷰 패턴
    1. 외부 시스템은 뷰로부터 데이터를 읽도록 전환되어 데이터를 은닉하고 스키마에서 직접 읽을 필요가 없어 성능을 높일 수 있다. 그러나 사전에 계산된 뷰가 어떻게 업데이트되는지와 관련해 어려움이 생기며, 이는 뷰에서 stale 데이터 집합을 읽을 수도 있다는 의미다.
    2. 더불어 뷰를 지원하지 않는 DB엔진이 있고 기반 스키마와 뷰가 동일 데이터베이스 엔진에 존재해야 하는 등의 여러 제약이 있을 가능성이 높다. 이는 물리적인 배포 결합도를 증가시켜 잠재적인 단일 장애 지점을 만들 수 있다.
    3. 기반 스키마를 변경하면 뷰도 업데이트해야할 수 있다. 또한 뷰의 소유권도 확인해야 한다
    4. 그러면 언제 뷰를 써야할까? 기존 모놀리스 스키마를 분해하는 방식이 실용적이지 않다고 여겨지는 상황일 때 뷰를 사용할 수 있따. 그냥 모놀리스 스키마를 사용하는 것보단 나으니까?
  3. 데이터베이스 래핑 서비스 패턴
    1. 앱에서 데이터베이스에 직접 접근하는 게 아니라 래퍼 역할을 하는 서비스를 하나 만들어서 DB 접근이 필요하면 그 서비스를 호출하도록 만든다.
    2. 이러면 공유할 대상을 제어할 수 있다.
    3. 테스트 목적을 위한 스텁을 직접 추가할 수도 있고 기존 테이블 구조에 매핑할 수 있는 뷰를 표현하는 과정에 제약이 없다.
  4. DaaS 인터페이스 패턴
    1. 읽기 전용 외부 공개 데이터베이스를 만들고 기반 데이터베이스가 변경될 때 그 외부 데이터베이스를 채우는 방식. 외부 데이터베이스에 채워줄 때 매핑 엔진이 필요하다.
      1. 매핑 엔진은 기반 데이터베이스와 외부 데이터베이스의 일관성을 유지하기 위함
    2. 즉, 읽기 전용이 필요한 클라이언트가 있을 경우 유용하다
    3. 외부에 공개된 데이터베이스에서 읽는 클라이언트는 이미 stale한 데이터를 보고있을 수도 있다. 그래서 이 DB가 마지막으로 업데이트된 시기에 대한 정보를 외부에 공개해서 어느정도 인지를 시켜줘야될 수도 있다
  5. 집계를 외부에 공개하는 모놀리스 패턴
    1. 단순히 모놀리스 자체에서 API를 제공하는 걸 말하는 것 같다.
    2. 서비스를 추출할 때, 필요한 데이터에 접근하기 위해 새로운 서비스가 거꾸로 모놀리스를 호출하는 경우는 모놀리스의 데이터베이스에 직접 접근하는 경우보다 조금 더 작업이 필요할 가능성이 높지만 장기적으로는 훨씬 더 좋은 방식이다.
  6. 데이터 소유권 변경 패턴
    1. 기존 모놀리스에서 송장 테이블에 접근하던 것을 없애고 송장 서비스에 API를 호출해서 송장 데이터를 얻을 수 있도록 한다.
    2. 만약 읽기 접근만 필요하다면 모놀리스에서 송장 테이블에 접근이 필요할 때 뷰를 활용할 수 있다.
  7. 애플리케이션에서 데이터 동기화 패턴
    1. 새 버전의 애플리케이션이 배포되어 두 DB에 모든 데이터를 기록한다. (읽기 쓰기 모두)
      1. 읽기는 기존 DB만, 쓰기는 기존 DB와 새로운 DB에 한다
      2. 그리고 읽기를 새로운 DB에만 하도록 옮긴다
    2. 두 DB가 항상 동기화되어있어 롤백 시나리오에 아주 유리하다.
  8. 예광탄 기록 패턴
    1. 작은 데이터 집합을 기준으로 모놀리스에서 쓰기만 하고 새로운 서비스에서 읽고 쓰도록 한다.
    2. API 콜을 두 번(기존 모놀리스, 분리된 마이크로서비스)씩 해서 양쪽에 쏠 수 있으나 둘 중 한 곳에서는 실패할 경우, 오류 조건을 처리해야해서 두 시스템간 불일치가 발생할 수 있다.

뷰를 쓰는 이유를 잘 모르겠다. 정보를 은닉할 수 있다는 장점 말고는 없어보인다. 마이그레이션할 때도 DB에 직접 접근하지 말고 API Call로 대체하자. 기존 DB와 새로운 DB에 함께 쓰고, 읽기를 새로운 DB로 옮기자.

  • 논리적인 분해는 하나의 DB 서버에 스키마 분리
  • 물리적 분해는 여러 DB 서버에 스키마 분리
  • 논리적 분해는 독자적인 변경과 정보 은닉이 가능한 반면, 물리적 분해는 잠재적으로 시스템 견고성을 개선하고 자원 경합을 제거하는 과정에 도움이 되므로 처리량이나 대기 시간을 향상시킬 수 있다

물리적 분해까지 하는 게 좋으나 사실상 그렇게까지 하진 않는듯. 상황에 따라 하는 게 좋아보인다. 너무 오버스펙으로 가도 안 좋고

  • 데이터베이스를 먼저 분할할까, 아니면 코드를 먼저 분할할까?
    • 경계 컨텍스트를 잘 구분해서 분리해보자 (스키마를 여러 개로)
    • 마이크로서비스로 결코 이동하지 않더라도 데이터베이스를 뒷받침하는 스키마를 명확하게 분리하면 특히 많은 사람들이 모놀리스에서 작업하는 경우 정말 큰 도움이 될 수 있다

어떤 것을 먼저 분리하든 경계 컨텍스트를 잘 구분하는 것이 중요하고, 처음에 스키마부터 명확하게 분리해놓으면 모놀리스에서 작업할 때도 많은 도움이 될 수 있다.

어떤 서비스에서 해당 열에 속한 데이터를 소유할 지에 대해 결정을 내려야 한다.

  • 외래 키 관계를 코드로 이동
    • 외래 키로 조인하는 관계를 애플리케이션 단에서 조인하도록 할 수 있다. 직접 조인할 서비스를 호출해서 연결할 수 있으나 이렇게 하면 당연히 시간이 늘어난다. 성능상 문제가 있을 수 있다

스키마를 분리했을 때 데이터 일관성에 대해 유의가 필요하다. 이미 참조가 되어있다면 해당 참조된 행을 삭제할 수 없기 때문. 이때는 삭제하지 않거나 Yn컬럼을 추가하는 게 방법이 될 수 있다

  • 중복 정적 참조 데이터
    • 서비스마다 독자적인 테이블 사본을 유지하는 것. 중복된 데이터 사본이 생기면 중복된 모든 데이터를 관리해줘야하기 때문에 데이터 불일치 문제가 발생할 수 있다. 그래서 데이터 불일치 유무가 문제가 되지 않는 서비스에서만 사용 가능하다
  • 전용 참조 데이터 스키마
    • 하나의 테이블에만 참조하는 것. 해당 DB에 문제가 있을 때 단일 장애 지점이라는 게 문제며 스키마 형식을 변경하면 전부 영향이 가기 때문에 문제가 된다.
  • 정적 참조 데이터 라이브러리
    • Common 같은 코드 라이브러리를 직접 만들어서 참조할 수 있도록 한다.
    • 이러면 운영 환경에 다른 버전의 라이브러리가 배포되어있을 수 있으므로 조심해야 한다
    • 서비스마다 데이터의 다른 버전을 사용해도 되는 소량의 데이터인 경우에 괜찮다
  • 정적 참조 데이터 서비스
    • 전담 서비스를 만들어 참조하는 것
    • 추가 작업이 많이 들기 때문에 힘들 수 있다
    • 코드에서 데이터 자체의 수명 주기를 관리할 경우에는 괜찮음

국가 코드가 모든 서비스에서 항상 일관성을 유지할 필요가 없다면, 나는 이 정보를 공유 라이브러리에 보관할 것 같다. (이런 유형의 데이터는 본질적으로 단순하고 데이터 크기도 작다)

  • 모든 DB가 ACID 트랜잭션을 제공하지 않는다
  • 분산 트랜잭션
    • 2단계 커밋 알고리즘
      • 투표 단계와 커밋 단계라는 두 단계로 나뉜다. 투표 단계에서 중앙 코디네이터는 트랜잭션에 참가할 모든 워커에게 연락하고 일부 상태 변경이 가능한 지 여부를 확인하도록 요청한다.
      • 워커가 변경할 수 있다고 알려준 직후에 변경사항이 즉시 적용되지 않는다는 사실이 중요하다. 즉, 워커는 미래 어느 시점에서 그 변경을 수행할 수 있음을 보증하고 있다
      • 투표하지 않은 워커가 있는 경우, 모든 워커에게 롤백 메시지를 보내 로컬에서 정리할 수 있도록 보장한다
      • 워커와 코디네이터 사이의 대기 시간이 길수록, 워커가 응답을 처리하는 속도가 느릴수록, 불일치 구간은 더 커질 수 있다.
      • 2단계 커밋은 일반적으로 수명이 매우 짧은 연산에만 사용된다
    • 사가 패턴
      • 사가 실패 시,
        • 역방향 복구 : 장애 복구와 이후에 일어나는 정리 작업인 롤백이 포함됨 (보상 트랜잭션)
        • 정방향 복구 : 오류가 발생한 지점에서 데이터를 가져와 트랜잭션을 재시도 처리
      • 중간에 실패하면 완료했던 얘들도 되돌려야하기 때문에 보상 트랜잭션(이전에 커밋된 트랜잭션을 취소하는 연산)이 필요하다
      • 오케스트레이션 기반 사가, 연출된 사가 패턴이 있는데 잘 모르겠음..

모든 서비스가 나눠져있으므로 커밋을 취소하기 위해선 보상 트랜잭션 구현 중요. 나머지는 추후 알아보기…

5장 마이크로서비스 도입 과정에서 직면하는 문제와 해법

  • 서비스 수와 개발자 수가 늘어남에 따라 누구나 코드를 변경할 수 있는 소유권 문제가 발생할 수 있다
  • PR을 적용시켜 코드 소유권을 명확하게 할 수 있도록 하자
  • 외부에 공개하는 계약(Contract)에 대해 기존 호환성을 깨뜨리는 행위는 좋지 않다. 가능한 기존 호환성을 유지하고 천천히 마이그레이션 하자
  • 리포팅같이 모든 데이터를 함께 봐야하는 기능이 필요하다면 리포팅 전용 데이터베이스를 만들고 그곳에 넣자
  • 마이크로서비스에서 모니터링같은 경우, 로그 집계 시스템, 분산 추적 시스템을 사용하자.
  • 로컬에서 개발하되 실행할 서비스의 수는 줄이고 싶은 경우, 로컬/원격 개발자 워크 플로우를 쉽게 만들어주는 텔레프레즌스라는 도구를 사용할 수도 있다.
  • 서비스 수가 많아질수록 견고성과 회복탄력성을 겸비해야한다. (네트워크 패킷이 손실되거나 네트워크 호출 시간이 초과되거나 컴퓨터가 죽는 등…) 이를 해결하기 위해 비동기 통신을 도입한다든가, 몰리는 자원 경합을 피할 수 있도록 설계한다든가, 서킷 브레이커 등으로 빠르게 실패를 일으켜서 문제를 피할 수 있다.
  • 작동하고 있으나 아무도 히스토리를 모르는 오래된 서버가 존재할 수 있다. biz ops 도구를 활용하자.

당연한 이야기가 많았음. 건질 건 서비스 수가 많아질수록 견고성과 회복탄력성을 겸비해야된다 정도..?