마이크로서비스 도입, 이렇게 한다
책 스터디 간단 정리 내용
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장 데이터베이스 분해
- 공유 데이터베이스 패턴
- 말 그대로 데이터베이스가 공유되고 있는 것
- 그러나 각 마이크로서비스가 자체적인 데이터를 소유할 수 있도록 데이터베이스를 분리하는 방식이 거의 항상 선호된다.
- 데이터베이스 엔진의 단일 인스턴스는 여러 스키마를 제공할 수 있으며, 각 스키마는 논리적으로 데이터를 격리한다
- 데이터베이스 뷰 패턴
- 외부 시스템은 뷰로부터 데이터를 읽도록 전환되어 데이터를 은닉하고 스키마에서 직접 읽을 필요가 없어 성능을 높일 수 있다. 그러나 사전에 계산된 뷰가 어떻게 업데이트되는지와 관련해 어려움이 생기며, 이는 뷰에서 stale 데이터 집합을 읽을 수도 있다는 의미다.
- 더불어 뷰를 지원하지 않는 DB엔진이 있고 기반 스키마와 뷰가 동일 데이터베이스 엔진에 존재해야 하는 등의 여러 제약이 있을 가능성이 높다. 이는 물리적인 배포 결합도를 증가시켜 잠재적인 단일 장애 지점을 만들 수 있다.
- 기반 스키마를 변경하면 뷰도 업데이트해야할 수 있다. 또한 뷰의 소유권도 확인해야 한다
- 그러면 언제 뷰를 써야할까? 기존 모놀리스 스키마를 분해하는 방식이 실용적이지 않다고 여겨지는 상황일 때 뷰를 사용할 수 있따. 그냥 모놀리스 스키마를 사용하는 것보단 나으니까?
- 데이터베이스 래핑 서비스 패턴
- 앱에서 데이터베이스에 직접 접근하는 게 아니라 래퍼 역할을 하는 서비스를 하나 만들어서 DB 접근이 필요하면 그 서비스를 호출하도록 만든다.
- 이러면 공유할 대상을 제어할 수 있다.
- 테스트 목적을 위한 스텁을 직접 추가할 수도 있고 기존 테이블 구조에 매핑할 수 있는 뷰를 표현하는 과정에 제약이 없다.
- DaaS 인터페이스 패턴
- 읽기 전용 외부 공개 데이터베이스를 만들고 기반 데이터베이스가 변경될 때 그 외부 데이터베이스를 채우는 방식. 외부 데이터베이스에 채워줄 때 매핑 엔진이 필요하다.
- 매핑 엔진은 기반 데이터베이스와 외부 데이터베이스의 일관성을 유지하기 위함
- 즉, 읽기 전용이 필요한 클라이언트가 있을 경우 유용하다
- 외부에 공개된 데이터베이스에서 읽는 클라이언트는 이미 stale한 데이터를 보고있을 수도 있다. 그래서 이 DB가 마지막으로 업데이트된 시기에 대한 정보를 외부에 공개해서 어느정도 인지를 시켜줘야될 수도 있다
- 읽기 전용 외부 공개 데이터베이스를 만들고 기반 데이터베이스가 변경될 때 그 외부 데이터베이스를 채우는 방식. 외부 데이터베이스에 채워줄 때 매핑 엔진이 필요하다.
- 집계를 외부에 공개하는 모놀리스 패턴
- 단순히 모놀리스 자체에서 API를 제공하는 걸 말하는 것 같다.
- 서비스를 추출할 때, 필요한 데이터에 접근하기 위해 새로운 서비스가 거꾸로 모놀리스를 호출하는 경우는 모놀리스의 데이터베이스에 직접 접근하는 경우보다 조금 더 작업이 필요할 가능성이 높지만 장기적으로는 훨씬 더 좋은 방식이다.
- 데이터 소유권 변경 패턴
- 기존 모놀리스에서 송장 테이블에 접근하던 것을 없애고 송장 서비스에 API를 호출해서 송장 데이터를 얻을 수 있도록 한다.
- 만약 읽기 접근만 필요하다면 모놀리스에서 송장 테이블에 접근이 필요할 때 뷰를 활용할 수 있다.
- 애플리케이션에서 데이터 동기화 패턴
- 새 버전의 애플리케이션이 배포되어 두 DB에 모든 데이터를 기록한다. (읽기 쓰기 모두)
- 읽기는 기존 DB만, 쓰기는 기존 DB와 새로운 DB에 한다
- 그리고 읽기를 새로운 DB에만 하도록 옮긴다
- 두 DB가 항상 동기화되어있어 롤백 시나리오에 아주 유리하다.
- 새 버전의 애플리케이션이 배포되어 두 DB에 모든 데이터를 기록한다. (읽기 쓰기 모두)
- 예광탄 기록 패턴
- 작은 데이터 집합을 기준으로 모놀리스에서 쓰기만 하고 새로운 서비스에서 읽고 쓰도록 한다.
- API 콜을 두 번(기존 모놀리스, 분리된 마이크로서비스)씩 해서 양쪽에 쏠 수 있으나 둘 중 한 곳에서는 실패할 경우, 오류 조건을 처리해야해서 두 시스템간 불일치가 발생할 수 있다.
뷰를 쓰는 이유를 잘 모르겠다. 정보를 은닉할 수 있다는 장점 말고는 없어보인다. 마이그레이션할 때도 DB에 직접 접근하지 말고 API Call로 대체하자. 기존 DB와 새로운 DB에 함께 쓰고, 읽기를 새로운 DB로 옮기자.
- 논리적인 분해는 하나의 DB 서버에 스키마 분리
- 물리적 분해는 여러 DB 서버에 스키마 분리
- 논리적 분해는 독자적인 변경과 정보 은닉이 가능한 반면, 물리적 분해는 잠재적으로 시스템 견고성을 개선하고 자원 경합을 제거하는 과정에 도움이 되므로 처리량이나 대기 시간을 향상시킬 수 있다
물리적 분해까지 하는 게 좋으나 사실상 그렇게까지 하진 않는듯. 상황에 따라 하는 게 좋아보인다. 너무 오버스펙으로 가도 안 좋고
- 데이터베이스를 먼저 분할할까, 아니면 코드를 먼저 분할할까?
- 경계 컨텍스트를 잘 구분해서 분리해보자 (스키마를 여러 개로)
- 마이크로서비스로 결코 이동하지 않더라도 데이터베이스를 뒷받침하는 스키마를 명확하게 분리하면 특히 많은 사람들이 모놀리스에서 작업하는 경우 정말 큰 도움이 될 수 있다
어떤 것을 먼저 분리하든 경계 컨텍스트를 잘 구분하는 것이 중요하고, 처음에 스키마부터 명확하게 분리해놓으면 모놀리스에서 작업할 때도 많은 도움이 될 수 있다.
어떤 서비스에서 해당 열에 속한 데이터를 소유할 지에 대해 결정을 내려야 한다.
- 외래 키 관계를 코드로 이동
- 외래 키로 조인하는 관계를 애플리케이션 단에서 조인하도록 할 수 있다. 직접 조인할 서비스를 호출해서 연결할 수 있으나 이렇게 하면 당연히 시간이 늘어난다. 성능상 문제가 있을 수 있다
스키마를 분리했을 때 데이터 일관성에 대해 유의가 필요하다. 이미 참조가 되어있다면 해당 참조된 행을 삭제할 수 없기 때문. 이때는 삭제하지 않거나 Yn컬럼을 추가하는 게 방법이 될 수 있다
- 중복 정적 참조 데이터
- 서비스마다 독자적인 테이블 사본을 유지하는 것. 중복된 데이터 사본이 생기면 중복된 모든 데이터를 관리해줘야하기 때문에 데이터 불일치 문제가 발생할 수 있다. 그래서 데이터 불일치 유무가 문제가 되지 않는 서비스에서만 사용 가능하다
- 전용 참조 데이터 스키마
- 하나의 테이블에만 참조하는 것. 해당 DB에 문제가 있을 때 단일 장애 지점이라는 게 문제며 스키마 형식을 변경하면 전부 영향이 가기 때문에 문제가 된다.
- 정적 참조 데이터 라이브러리
- Common 같은 코드 라이브러리를 직접 만들어서 참조할 수 있도록 한다.
- 이러면 운영 환경에 다른 버전의 라이브러리가 배포되어있을 수 있으므로 조심해야 한다
- 서비스마다 데이터의 다른 버전을 사용해도 되는 소량의 데이터인 경우에 괜찮다
- 정적 참조 데이터 서비스
- 전담 서비스를 만들어 참조하는 것
- 추가 작업이 많이 들기 때문에 힘들 수 있다
- 코드에서 데이터 자체의 수명 주기를 관리할 경우에는 괜찮음
국가 코드가 모든 서비스에서 항상 일관성을 유지할 필요가 없다면, 나는 이 정보를 공유 라이브러리에 보관할 것 같다. (이런 유형의 데이터는 본질적으로 단순하고 데이터 크기도 작다)
- 모든 DB가 ACID 트랜잭션을 제공하지 않는다
- 분산 트랜잭션
- 2단계 커밋 알고리즘
- 투표 단계와 커밋 단계라는 두 단계로 나뉜다. 투표 단계에서 중앙 코디네이터는 트랜잭션에 참가할 모든 워커에게 연락하고 일부 상태 변경이 가능한 지 여부를 확인하도록 요청한다.
- 워커가 변경할 수 있다고 알려준 직후에 변경사항이 즉시 적용되지 않는다는 사실이 중요하다. 즉, 워커는 미래 어느 시점에서 그 변경을 수행할 수 있음을 보증하고 있다
- 투표하지 않은 워커가 있는 경우, 모든 워커에게 롤백 메시지를 보내 로컬에서 정리할 수 있도록 보장한다
- 워커와 코디네이터 사이의 대기 시간이 길수록, 워커가 응답을 처리하는 속도가 느릴수록, 불일치 구간은 더 커질 수 있다.
- 2단계 커밋은 일반적으로 수명이 매우 짧은 연산에만 사용된다
- 사가 패턴
- 사가 실패 시,
- 역방향 복구 : 장애 복구와 이후에 일어나는 정리 작업인 롤백이 포함됨 (보상 트랜잭션)
- 정방향 복구 : 오류가 발생한 지점에서 데이터를 가져와 트랜잭션을 재시도 처리
- 중간에 실패하면 완료했던 얘들도 되돌려야하기 때문에 보상 트랜잭션(이전에 커밋된 트랜잭션을 취소하는 연산)이 필요하다
- 오케스트레이션 기반 사가, 연출된 사가 패턴이 있는데 잘 모르겠음..
- 사가 실패 시,
- 2단계 커밋 알고리즘
모든 서비스가 나눠져있으므로 커밋을 취소하기 위해선 보상 트랜잭션 구현 중요. 나머지는 추후 알아보기…
5장 마이크로서비스 도입 과정에서 직면하는 문제와 해법
- 서비스 수와 개발자 수가 늘어남에 따라 누구나 코드를 변경할 수 있는 소유권 문제가 발생할 수 있다
- PR을 적용시켜 코드 소유권을 명확하게 할 수 있도록 하자
- 외부에 공개하는 계약(Contract)에 대해 기존 호환성을 깨뜨리는 행위는 좋지 않다. 가능한 기존 호환성을 유지하고 천천히 마이그레이션 하자
- 리포팅같이 모든 데이터를 함께 봐야하는 기능이 필요하다면 리포팅 전용 데이터베이스를 만들고 그곳에 넣자
- 마이크로서비스에서 모니터링같은 경우, 로그 집계 시스템, 분산 추적 시스템을 사용하자.
- 로컬에서 개발하되 실행할 서비스의 수는 줄이고 싶은 경우, 로컬/원격 개발자 워크 플로우를 쉽게 만들어주는 텔레프레즌스라는 도구를 사용할 수도 있다.
- 서비스 수가 많아질수록 견고성과 회복탄력성을 겸비해야한다. (네트워크 패킷이 손실되거나 네트워크 호출 시간이 초과되거나 컴퓨터가 죽는 등…) 이를 해결하기 위해 비동기 통신을 도입한다든가, 몰리는 자원 경합을 피할 수 있도록 설계한다든가, 서킷 브레이커 등으로 빠르게 실패를 일으켜서 문제를 피할 수 있다.
- 작동하고 있으나 아무도 히스토리를 모르는 오래된 서버가 존재할 수 있다. biz ops 도구를 활용하자.
당연한 이야기가 많았음. 건질 건 서비스 수가 많아질수록 견고성과 회복탄력성을 겸비해야된다 정도..?