Microservices Up & Running 본문

Programming

Microservices Up & Running

halatha 2022. 2. 3. 01:10

기술적 요구를 기반으로 경계를 그리는 것은 안티 패턴이다. 제임스 루이스와 마틴 파울러에 따르면 마이크로서비스는 기술적 요구가 아닌 ‘비즈니스 기능을 중심으로 구성’되어야 한다. 마찬가지로 데이비드 파나스 Davial Pumus는 시간에 따른 설계 변경의 모듈식 캡슐화를 기반으로 시스템을 분해할 것을 권장한다. 두 접근 방식 모두 서버리스 함수의 경계와는 일치하지 않는다.

서비스 경계를 정할 때 다음과 같은 설계를 위해 노력해야 한다고 제안했다.
느슨한 결합: 서비스는 서로를 인식하지 않고 독립적이어야 하며, 한 서비스에서 코드를 수정하더라도 다른 서비스에 영향을 주지 않아야 한다.
높은 응집력: 서비스에 있는 기능은 관련성이 높아야 하며 관련 없는 기능은 다른 곳에 캡슐화되어야 한다. 이렇게 하면 기능의 논리적인 단위를 변경해야 하는 경우 한 곳에서 변경할 수 있으므로 변경 사항을 릴리스하는 시간 (중요 메트릭)을 최소화할 수 있다.
비즈니스 기능과 연결: 기능의 수정이나 확장에 대한 대부분의 요청은 비즈니스 요구에 따라 이루어지기 때문에 서비스 경계가 비즈니스 기능의 경계와 일치한다면 느슨한 결합과 높은 응집력의 설계 요건을 쉽게 만족시킬 수 있다.
이러한 설계 원칙은 매우 유용한 것으로 입증되었으며 마이크로서비스 실무자들 사이에서 널리 채택되었다. 하지만 이는 상당히 높은 수준의 의욕만 앞선 원칙이며 실무자들에게 필요한 서비스의 크기에 대한 지침을 제공하지 않는다. 더 실용적인 방법론을 찾기 위해 많은 사람이 도메인 주도 설계로 눈을 돌렸다. 방법론의 핵심은 복잡한 시스템을 분석할 때 전체 시스템을 대표하는 단일 통합 도메인 모델을 찾는 것을 피해야 한다는 것이다.
대규모 프로젝트에서는 여러 모델이 공존하고 있으며, 이는 많은 경우에 잘 작동한다. 서로 다른 모델은 각각의 콘텍스트 내에 존재한다.
에반스는 복잡한 시스템이 기본적으로 여러 도메인의 모음이라는 것을 확인한 후 제한된 콘텍스트 bounded context의 개념을 도입했다. 구체적으로 그는 다음과 같이 말했다.
제한된 콘텍스트는 각 모델의 적용 가능성의 범위를 정의한다.
제한된 콘텍스트를 사용하면 더 큰 시스템의 다른 부분을 구현하고 런타임에서 실행해도 시스템에 존재하는 독립적인 도메인 모델을 손상시키지 않는다. 제한된 콘텍스트를 정의한 후 에반스는 유비쿼터스 언어 Ubiquitous Language의 개념을 확립하여 제한된 콘텍스트의 최적화된 경계를 식별하는 공식을 제공했다.
유비쿼터스 언어의 의미를 이해하기 위해서는 잘 정의된 도메인 모델이 도메인을 설명하는 정의된 용어 및 개념에서 공통 어휘를 제공한다는 것을 알아야 한다. 주제별 전문가와 엔지니어는 공통 언어를 통해 비즈니스 요구사항과 구현 고려 사항 사이의 균형을 이루며 함께 협업한다. DDD에서는 이러한 공통 언어 또는 공용 어휘를 유비쿼터스 언어라 한다. 여기서 동일한 단어가 [그림 4–1]과 같이 서로 다른 경계의 콘텍스트에서 다른 의미를 전달할 수 있음을 알아야 한다.
동일한 기본 영어 단어가 서로 다른 맥락에서 상당히 다른 의미로 사용된다는 것을 알 수 있다. 하지만 특정 도메인 모델의 제한된 맥락 내에서 용어 (유비쿼터스 언어)의 유비쿼터스 의미에만 동의하면 되기 때문에 이는 문제가 되지 않는다. DDD에 따르면 어떤 용어가 의미를 바꾸는지 관찰하면 콘텍스트의 경계를 식별할 수 있다.
DDD에서 도메인 모델을 논의할 때 떠오르는 모든 용어가 유비쿼터스 언어로 만들어지는 것은 아니다. 제한된 콘텍스트의 개념은 유비쿼터스 언어의 일부이며 다른 모든 개념은 제외해야 한다.

많은 사람이 DDD를 본격적으로 실천하는 것보다 더 DDD에 대해 이야기만 하는 주된 이유는 크게 두 가지가 있다. 이는 복잡하고 비용이 많이 든다는 것이다…간단하게 말해서 DDD 기술과 경험을 가진 사람이 충분하지 않고 학습 곡선이 가파르다… DDD는 이러한 노력을 투자할 가치가 있을까? 답은 그렇다’이다. 특히 대규모의 중요하고 비용이 많이 드는 시스템에서 DDD는 많은 이점을 가진다. 하지만 이는 시간이 많이 걸리고 비싼 작업이기 때문에 단지 일부 마이크로서비스의 크기를 조정하기 위해 바쁜 사람들에게 참여를 요청하기가 쉽지 않았다.
이벤트 스토밍은 DDD의 개념에 기반을 두고 몇 주 또는 몇 달이 아닌 몇 시간 만에 제한된 콘텍스트를 찾을 수 있게 도와준다. 이벤트 스토밍의 도입은 특히 서비스 크기 조정을 위해 DDD의 낮은 적용 가능성을 위한 돌파구다. 물론 DDD를 완전하게 대체할 수 있는 것은 아니며 공식적인 DDD의 모든 이점을 다 제공하지는 않는다. 하지만 제한된 콘텍스트 발견에는 꽤 효과적인 프로세스다.

(DDD뿐만이 아니라 이미 입증된 방법 혹은 수많은 사례를 가진 방법이어도 새로운 걸 도입할 때는 언제나 저항이 뒤따르고 이겨내는 건 정말 힘들다. 비개발자들에겐 더욱 그렇다)

이벤트 소싱이 시스템의 도메인 객체가 아닌 이벤트를 저장하는 것에 대한 데이터 모델링 접근 방식이라 언급했다.
이벤트 소싱은 ‘사실’과 ‘상태(구조적 모델)’를 저장하는 것이다. ‘상태’는 사실에서 일차적으로 파생된 값이며 일시적이다.
여기서 ‘사실’은 이벤트 발생의 대표적인 값을 의미한다.
이벤트 소싱 시스템의 예는 실생활에서 쉽게 찾을 수 있다. 회계 일지 accounting fourmal)는 고전적인 이벤트 저장 방식 사례 중 하나다. 회계사는 개별거래를 기록하고 잔액은 모든 거래를 합산한 결과다. 회계사는 ‘상태’를 기록하지 않으며 각 거래 후 결과 잔액을 기록한다. 마찬가지로, 체스 게임에서 경기를 기록한다면 체스 말을 이동한 후 보드에 있는 모든 말의 위치를 기록하지 않을 것이다. 대신에 개별적인 이동을 저장하고 이동후 보드의 상태는 일어난 모든 이동의 합이 될 것이다.

CAP 정리는 데이터 공유를 사용하는 단일 시스템이 일관성 consistency, 가용성 availability , 분할 내성 partition tolerence 의 모든 조건을 만족할 수 없다고 강조한다. 하지만 CQRS를 활용하여 여러 시스템을 사용하고 데이터 공유를 최소화한다면 이야기는 달라진다. 이 경우 이벤트 스토어에서 일관성의 우선순위를 정하고 쿼리 인덱스에서 가용성의 우선순위를 정할 수 있다. 쿼리 인덱스에 사용하는 시스템은 일관성이 깨질 수 있지만 신뢰할 수 있는 소스가 아니므로 필요한 경우 이벤트 스토어에서 다시 인덱싱할 수 있다.
이벤트 소싱과 CORS의 두 번째 주요한 장점은 감사 가능성 auditability 과 관련이 있다. 관계형 데이터 모델을 사용하면 인플레이스 in-place 업데이트를 한다. 예를 들어 고객의 주소나 전화번호가 올바르지 않다고 판단하면 해당 테이블에 바로 업데이트한다. 하지만 고객이 나중에 이러한 기록에 이의를 제기하면 어떻게 해야 할까?? 관계형 모델에서는 히스토리가 없기 때문에 복구할 방법을 찾을 수 없다. 하지만 이벤트 소싱에서는 모든 변경 사항에 대한 기록이 안전하게 보존되고 과거의 고객 값이 무엇인지, 언제 어떻게 업데이트되었는지 확인할 수 있다.

(CQRS를 사용하면 CAP의 한계를 극복할 수 있다는 건 처음 안 사실)

로깅은 이벤트 소싱과 어떻게 다를까? 우리는 단지 전통적인 로깅에 대해 이야기하면서 새로운 이름으로 브랜딩하는 것일까? 대답은 절대 아니다. 아키텍처에서 진실의 근원 the source of truth 이 되는 시스템은 무엇일까? 로그가 현재 상태와 일치하지 않는다면 어떤 것을 ‘신뢰’해야 할까? 이벤트 소싱에서 ‘상태’는 계산된 값이므로 더 정확하다. 스플렁크 로그의 경우 일부 버그를 찾기 위해 두 번씩 확인하더라도 진실의 근원이 관계형 모델일 가능성이 높다. 신뢰할 수 있는 이벤트 로그가 진실의 근원이라면 데이터 모델링 접근 방식으로 이벤트 소싱을 사용하고 있는 것이다. 그렇지 않다면 아무리 많은 로그를 생성하더라도 이는 이벤트 소싱이 아니다.

조직에 표준을 수립할 때에는 기술적인 방법’과 ‘무엇’을 제시하기 전에 사람들이 프로세스의 ‘이유’에 대해 공감할 수 있어야 한다. 이를 위해서는 명확한 목표를 제시하는 것이 중요하다.
짧은 시간 내에 코드 설정이 가능해야 한다: 우리의 목표는 코드에 익숙하지 않은 새로운 개발자가 한 시간 이내에 마이크로서비스를 잘 설정할 수 있는 환경을 제공하는 것이다.
새로운 마이크로서비스는 빠르고, 쉽고, 예측 가능하게 만들어야 한다: 각 표준 스택에 대해 잘 정의된 템플릿을 제공하는 것은 코드의 고품질을 유지하는 동시에 개발 속도를 높이는 좋은 방법이다.
품질 관리는 자동화되어야 한다: 소프트웨어 테스트는 자동화하여 기업의 품질 관리 과정에서 사람의 실수를 방지한다.
개발자 경험을 위한 10가지 워크스페이스 가이드라인
1. 도커를 유일한 종속성으로 만든다.
2. 실행 환경이 원격 환경인지 로컬 환경인지는 중요하지 않다.
코드는 환경에 관계없이 실행되어야 한다. 예를 들어 개발자가 자신의 랩탑에서 실행하는 IDE의 원격개발/SFTP 플러그인을 통해 클라우드 서버에서 실행하든 상관없이 실행되어야 한다. 이것이 불가능한 경우에는 예외의 원인을 정리하여 이를 문서화해야 한다.
3. 서로 다른 기술 스택의 워크스페이스를 준비한다.
하지만 여러 개의 기술 스택을 수용하더라도 전체 애플리케이션에서 사용하는 프로그래밍 언어와 데이터베이스 시스템은 2~3개로 제한해야 한다. 그렇지 않으면 너무 많은 선택 옵션으로 팀이 혼란을 느낄 수 있으며 유지 보수에 심각한 오버헤드가 발생할 수 있다.
4. 단일 마이크로서비스를 실행하는 것과 여러 개의 하위 시스템으로 구성된 마이크로서비스를 실행하는 것은 간단해야 한다.
5. 가능하면 데이터베이스는 로컬에서 실행한다.
환경 격리를 위해 모든 데이터베이스 시스템은 로컬 환경에 대한 도커 방식의 대안을 제공해야 하며 구성 변경을 통하여 AWS와 같은 클라우드 서비스로 쉽게 전환할 수 있어야 한다.
로컬 환경에서 S3 대체: https://min.io
AWS 서비스 대체: https://github.com/localstack/localstack
6. 컨테이너화 가이드라인을 구현한다.
① 코드 런타임이 컨테이너화되었더라도 개발자는 호스트 머신(랩탑, EC2 개발 서버)에서 편집기를 사용하여 코드를 수정할 수 있어야 한다. 하지만 전체 실행 테스트/디버그는 컨테이너 안에서 수행되어야 한다.
② 도커 컴포즈 Docker Composs는 일반적으로 도커파일Dockerfiel이 할 수 있는 모든 작업을 수행할 수 있으므로 개발자가 쉽게 혼동할 수 있다. 따라서 둘 사이의 차이점을 확실히 아는 것이 중요하다. 도커 파일은 컨테이너 이미지를 빌드하고 도커 컴포즈는 복잡한 통합을 포함하여 로컬에서 컨테이너를 실행한다. 도커파일로 빌드된 이미지는 쿠버네티스, AWS ECR. 도커 스월 Procker Swam 또는 다른 종류의 상용 컨테이너 런타임에서 직접 실행할 수 있어야 한다. 로컬/개발 환경의 컨테이너 이미지가 항상 프로덕션에서 실행하는 이미지와 동일할 필요는 없다. 로컬/개발 이미지는 일반적으로 사용성을 위해 최적화하고 프로덕션 이미지는 보안과 성능을 위해 최적화한다. 이러한 방식의 좋은 예는 멀티스테이지 빌드 multistage build 다.
③ 프로덕션 용도로 가벼운 이미지를 사용하고 로컬/개발 용도로 더 많은 기능을 갖춘 이미지를 생성하려면 도커파일에서 멀티스테이지 빌드를 활용한다.
④ 개발자 사용 경험이 중요하다. 코드의 핫 리로당 — nbauding 기능을 구현하거나 디버거를 즉시 연결할 수 있는 것과 같은 기능을 구현하는 것은 매우 중요하다.
7. 데이터베이스 마이그레이션을 위한 간단한 규칙을 정한다.
데이터 스키마 변경은 수동 작업 없이 코드화하여 적용해야 한다. 다음은 마이크로서비스 환경에서 데이터 관리를 돕는 원칙들이다.
① 데이터베이스 스키마에 대한 모든 변경 사항은 ‘데이터베이스 마이그레이션’ 스크립트에 코드화되어야 한다. 마이그레이션 파일은 이름을 지정하여 날짜별로 정리한다.
② 데이터베이스 마이그레이션은 스키마 변경과 샘플 데이터 삽입을 모두 지원해야 한다.
③ 데이터베이스 마이그레이션 실행은 make start를 통해 프로젝트 시작에 포함되어야 하며 반드시 실행되어야 한다.
④ 데이터베이스 마이그레이션 실행은 자동화되어야 하며 빌드(통합 및 PR용 기능 브랜치 빌드 등)에 포함되어야 한다.
⑤ 데이터 마이그레이션 작업이 어떤 환경에서 실행되는지(또는 생략되는지) 지정할 수 있어야 한다. 예를 들어 프로덕션 환경에서는 샘플 데이터를 생성하는 과정을 건너뛸 수 있어야 한다.
⑥ 이러한 규칙은 관계형, 컬럼형, NoSQL 등 모든 데이터 스토리지 시스템에 적용된다.
7 참고 예제
• 플라이웨이 Forways는 데이터베이스 마이그레이션 도구 중 하나다. https://flywaydb.org/
• 카산드라 데이터베이스 마이그레이션에 대한 내용은 다니엘 미란다 Pianet Mranda, 루카스 브루니알티 Lucas Brunitah, 토빈 풀턴 Tobin Fution이 작성한 블로그 포스트 참고
• MySQL 데이터베이스에 노드의 db-migrate-sql을 사용하는 예제’를 참고
8. 실용적인 테스트 자동화 방법을 정한다.
① 모든 코드는 메인 브랜치와 병합되기 전에 충분한 양의 의미 있는 테스트가 작성되어야 한다.
② 팀은 자바의 JUnit와 같이 개발 플랫폼/스택에서 일반적으로 사용하는 테스트 접근 방식과 프레임워크를 활용한다. 동일 스택(고, 자바 등)의 코드베이스는 동일한 접근 방식을 사용해야 하며, 누가 언제 작성했는지에 따라 다른 테스트 작업을 수행해서는 안 된다.
③ 적절한 외부 도구를 사용하여 성능 테스트나 승인 테스트를 수행할 수 있다. 이러한 도구는 서비스 코드 및 저장소에 완전히 통합되어야 하며 사용과 실행은 기본 솔루션과 같이 간단해야 한다. 서비스 개발자는 일반적으로 외부 테스트 도구를 실행하기 위해 추가 설정이 필요 없어야 하며 maketest-all 과 같은 명령어로 테스트를 쉽게 실행할 수 있어야 한다.
④ 개별 마이크로서비스의 경계를 넘는 자동화된 테스트에는 특별한 주의가 필요하다. 더 높은 수준(API나 UI에서 마이크로서비스를 호출하는 것과 같은)에서 테스트가 적용되어야 하거나 경우에 따라서는 테스트 오케스트레이션과 자동화를 수용하기 위한 전용 저장소가 필요할 수 있다.
⑤ 코드 린팅ing 및 정적 분석 도구를 설정하고, 조직의 스타일에 맞는 일관된 구성을 린팅 도구에 적용해야 한다.
9. 분기 및 병합 규칙을 정한다.
브랜치 관리 원칙을 정해야 한다.
① 모든 개발은 feature나 bug 브랜치에서 진행된다.
② 브랜치를 메인 브랜치로 병합하려면 브랜치의 모든 테스트를 통과해야만 한다(브랜치를 위해 스핀업 된 임시 클러스터의 통합 테스트 포함).
③ 풀 요청 중에는 코드 검토자가 커밋 및 푸시 이후의 테스트 결과를 쉽게 확인할 수 있어야 한다.
④ 코드 린팅 및 정적 분석 과정에서 오류가 발생하면 코드가 브랜치로 푸시되거나 메인 브랜치로 병합되는 것을 방지해야 한다.
10. 공통 사항은 makefile에 코드화한다.
마이크로서비스 makefile에는 다음과 같은 표준 타겟을 정의하고 구현하기를 권한다.
다중 마이크로서비스 워크스페이스 예제: https://github.com/inadarei/microservices-workspace
마크다운 형식의 makefile 템플릿: https://github.com/implementing-microservices/guidelines/blob/main/common-ms-makefile-targets.md

PlantUML

11.1 마이크로시스템에서의 변경
마이크로시스템에서 변경은 문제problem나 버그bug 수정이 아닌 기능feature 추가가 되어야 한다. 즉, 시스템을 변경하여 소프트웨어를 개선하고 더 나은 가치를 제공해야 한다. 사람들은 소프트웨어 변경에 대해 생각할 때 비즈니스나 사용자의 요청에서 비롯되는 외부 요인을 생각한다. 예를 들어 시스템을 변경해야 하는 몇 가지 일반적인 이유는 다음과 같다.
• 신제품 출시 지원• 사용자 경험을 저하시키는 버그 해결• 새로운 파트너와 통합
이는 모두 변경이 필요한 중요한 이유이며 아키텍처는 이러한 유형의 변경을 가능한 비용 효율적으로 처리하기 위해 고민한다. 또한 마이크로서비스 스타일이 최적화 기술이라는 점을 이해하는 것도 중요하다. 이는 다음과 같은 시스템의 본질적인 변경 또한 고려해야 한다는 것이다.
• 코드 복잡성을 줄이기 위해 마이크로서비스 분할
• 인프라 코드와 실제 환경을 동기화하기 위해 인프라 재배포
• 변경 사항을 더 빠르게 전달하기 위해 CI/CD 파이프라인 최적화
외부의 변경 요인을 지원해야 하는 것은 물론 중요하다. 하지만 마이크로서비스 시스템에서 최고의 가치를 얻으려면 본질적인 변경 또한 계획하고 실행해야 한다. 이러한 지속적인 개선 방식을 실천하는 좋은 방법은 데이터 및 측정을 사용하여 결정을 내리는 것이다.
11.1.1 데이터 기반 의사 결정
소프트웨어 개발의 고전적인 문제는 오버 엔지니어링과 조기 최적화다. 이는 발생하지 않을 문제를 해결하기 위한 소프트웨어와 아키텍처를 설계할 때 나온다. 또는 예측된 문제를 해결하기 위한 솔루션이 문제 자체보다 비용이 더 많이 드는 경우다.
마이크로서비스 시스템에도 또한 이러한 문제점이 발생할 수 있다. 그렇기 때문에 변경 (특히 본질적인 개선과 같은) 시점에 데이터와 측정을 사용하여 의사 결정을 내리는 것이 중요하다. 데이터가 없다면 추측을 통해 실제 개선이 필요하지 않은 시스템 부분에 힘을 쓸 수 있다. 또한, 다른 긴급한 문제를 감지 못할 수도 있다. 그러므로 데이터를 통해 의사 결정을 최적화하지 않는다면 리소스는 항상 부족할 수밖에 없다.
제품 팀은 데이터를 사용하여 원하는 변경에 대해 정보에 기반한 결정을 내린다.이와 같이 소프트웨어의 개선과 최적화 계획을 세우려면 다음과 같은 프로젝트, 설계, 런타임 메트릭 정보를 수집하는 것이 좋다.
• 마이크로서비스당 변경 시간
• 마이크로서비스당 변경 빈도
• 변경 요청당 변경된 마이크로서비스 수
• 마이크로서비스의 코드 줄 수(제약 조건이 아닌 참고용 데이터)
• 마이크로서비스당 런타임 지연 시간
• 마이크로서비스 간 종속성
11.1.2 변경의 영향
소프트웨어 변경이 서비스에 미치는 잠재적 영향은 매우 다양하다. 특히 현대 조직에서는 구현 시간, 조정 시간, 다운타임, 소비자 영향과 같은 주요 네 가지가 문제가 되는 것으로 알려져있다. 마이크로서비스 시스템에서 변경 비용을 검토할 때에는 이러한 네 가지 영역을 고려해야한다.
구현 시간: 변경 비용의 핵심은 실제로 변경하는 데 걸리는 시간이다. 이는 현재 상태를 이해하고 원하는 변경을 수행 및 테스트하고 프로덕션 환경에 업데이트하는 데 필요한 시간을 포함한다. 구현 시간에서 중요한 요소는 변경될 구성 요소의 가독성, 학습 용이성, 유지 보수성이다.
조정 시간: 변경 사항을 구현하기 위해서는 대부분의 경우 팀 간 의사소통이 필요하다. 조정 시간은 구현 시간의 일부이지만 별도로 분리하여 설명할 필요가 있다. 사실 조정 시간은 매우 중요하기 때문에 이 책에서도 몇 차례 언급했다. 조정 시간은 리소스에 접근하고 변경 활동에 권한과 동의를 얻는 데 소요되는 시간과 대규모 조직에서 일반적으로 발생하는 조직적 마찰’이 포함될 수 있다. 조정 시간은 조직 설계 및 구성의 요소가 되기도 한다.
다운타임: 다운타임은 변경 사항이 구현되는 동안 시스템이나 시스템 구성 요소를 사용할 수 없는 시간을 측정한 것이다. 다운타임은 몇 년 전까지만 해도 소프트웨어 변경 프로세스의 일부로 받아들여졌다. 하지만 기술 팀이 변경 사항에 대한 다운타임을 최소화해야 한다는 인식이 늘어났고, 시스템을 지속적으로 사용할 수 있는 ‘제로 다운타임zero downtime 변경 모델을 적용하기 위해 노력하는 것이 일반적이다.
소비자 영향: 놓치기 쉬운 영향 중 하나는 변경이 시스템 사용자에게 미치는 비용이다. 다운타임은 소비자 영향을 한 가지 형태로 다루지만 ‘제로 다운타임’ 모델조차도 피할 수 없는 예측 불가능한 영향이 있을 수 있다. 예를 들어 인프라 모듈 변경은 마이크로서비스 개발 팀에 광범위한 영향을 미칠 수 있다. 마찬가지로 인터페이스를 변경하면 이를 사용하는 모든 구성 요소의 코드가 영향을 받을 수 있다.

11.2.2 마이크로서비스 변경
시스템에서 변경해야 하는 대부분의 사항은 마이크로서비스에 있다. 새로운 제품을 제공하거나, 사용자 경험이 작동하는 방식을 변경하거나, 시스템을 약간 수정하려는 경우에는 마이크로서비스 하위 시스템의 변경이 필요할 것이다. 이는 새로운 마이크로서비스를 만들고 기존 서비스의 로직을 업데이트하거나 서비스를 폐기, 분할 또는 결합하는 것을 의미한다.
마이크로서비스 구현 비용
마이크로서비스 변경과 관련하여 주요 구현 비용은 코드 이해, 유지 보수, 테스트에서 발생한다. 우리의 아키텍처에서는 구현 비용을 낮추기 위해 몇 가지 중요한 결정을 내렸다.이벤트 스토밍을 사용하여 마이크로서비스의 규모를 정한다. 모든 마이크로서비스는 마이크로서비스 부트스트랩을 사용한다. 마이크로서비스를 위한 CI/CD를 사용한다.
마이크로서비스 조정 비용
마이크로서비스 엔지니어링 및 릴리스 작업을 위한 독립성을 위해 다음과 같은 마이크로서비스의 독립성을 높일 수 있는 몇 가지 결정을 따른다.
• 모든 마이크로서비스는 한 팀만 소유한다.
• 모든 마이크로서비스에는 자체 저장소와 CI/CD 파이프라인이 있다.
이러한 결정을 종합하면 마이크로서비스 코드를 변경하는 팀의 자율성이 높아진다.
팀 간 조정을 줄이는 것 외에도 ‘적절한 서비스 경계와 팀 규모를 제한하기로 결정한 것은 팀내 조정 비용도 상대적으로 낮게 유지하는 것을 보장한다. 마이크로서비스 코드 변경에 대한 조정 비용을 줄이는 것은 우리가 구축한 아키텍처의 주요 목표라고 할 수 있다.
전반적으로 아키텍처 내에서 마이크로서비스 변경의 조정 비용은 낮다. 이는 우리가 앞서 내린 운영 모델, 도구, 설계와 관련된 결정 덕분이다.
마이크로서비스 다운타임
우리가 최적화한 또 다른 변경 영역은 개별 마이크로서비스가 변경될 때 필요한 다운타임을 최소화하는 것이다. 이는 플랫폼 수준에서 도입한 도구와 인프라 덕분이다. 다운타임 비용을 낮추는 핵심은 마이크로서비스 릴리스에 카나리 배포 패턴 (11장 ‘카나리 배포절 참조)을 사용하는 것이다.
마이크로서비스 소비자 영향
지금까지 우리는 마이크로서비스 코드 변경에 중점을 두었다. 서비스의 논리, 유효성 검사, 동작이 반영되기 때문에 코드는 변경 빈도가 높은 부분이다. 하지만 때로는 마이크로서비스의 인터페이스(또는 API)를 변경해야 하는 경우가 있으며, 이로 인해 큰 문제가 발생할 수 있다.
API 변경으로 인한 소비자 영향을 최소화하는 가장 좋은 방법은 몇 가지 모범 관행을 준수하는 것이다. 예를 들어 이미 릴리스한 내용은 변경하지 않으며, 새로운 데이터를 허용하는 클라이언트 코드를 작성하고 관련된 매개 변수를 필수로 지정하지 않는다.
일부 마이크로서비스 실무자들은 이러한 종류의 설계 원칙 외에도 인터페이스를 변경할 때 팀간 조정 비용을 최소화하는 방법으로 계약 테스트 contract test를 도입했다. 계약 테스트에서 소비자와 공급자는 인터페이스 사용 방법을 설명하는 계약을 공유한다. 이를 통해 공급자는 계약 테스트를 독립적으로 실행하고 변경 사항이 API를 사용하는 기존 클라이언트에 영향을 미치는지 확인할 수 있다.
API 변경은 어렵기 때문에 변경 비용을 합리적으로 만들기 위해서는 좋은 설계 사고와 계획이 필요하다.
11.2.3 데이터 변경
마이크로서비스 아키텍처를 유지하는 데 있어 가장 어려운 부분은 데이터를 다루는 것이다. 데이터 모델은 일반적으로 변경하기 가장 어려운 것으로 알려져 있다. 지속성 계층은 소프트웨어 시스템에서 매우 필요한 부분이지만 데이터 구조를 변경할 때에는 상황이 복잡해질 수 있다. 소프트웨어 구성 요소는 데이터 시스템에 의존성을 갖고 있고 데이터를 변경하면 시스템에 영향을 미치기 때문에 큰 변경 비용이 발생한다.
데이터 구현 비용
가장 기본적인 수준에서 데이터 모델 변경 비용은 구조, 형식, 관계가 얼마나 복잡한지와 변경하는 데 필요한 도구와 언어에 영향을 받는다. 복잡한 값과 다양한 데이터 타입, 고유한 키, 까다로운 값이 있을 때 모델의 복잡성이 증가한다. 데이터 모델 자제를 잘 이해해야 안전하게 변경할 수 있다… 우리는 마이크로서비스가 자제 데이터를 소유해야 한다는 결정을 내렸다. 이러한 결정만으로도 모델의 범위와 크기를 제한하는 데 도움이 되어 코드 변경비용을 줄일 수 있을 것이다… 코드와 마찬가지로 구현 비용을 지속적으로 측정하여 서비스와 데이터 모델이 경계를 넘어서는 크기로 확장되지 않게 해야 한다.
데이터: 조정 비용
데이터 독립성의 가장 큰 장점은 조정 비용의 감소다. 마이크로서비스가 자체 데이터를 소유한다고 결정하면 다른 팀이나 시스템 소유자와의 조정 없이 데이터 구조를 자유롭게 변경할 수있다. 이는 여러 팀이 공유된 데이터 서비스를 사용하며 모든 데이터 사용자에 걸쳐 변경 사항을 신중하게 조정해야 하는 기존 모델과는 완전히 다르다.
하지만 독립적인 데이터 접근 방식에는 숨겨진 비용이 있다. 우리는 우리의 아키텍처를 빠르고 자율적인 로컬 변경을 위해 최적화했다. 이는 시스템 전체에서 변경해야 하는 경우에 더 많은 비용이 발생한다. 예를 들어 항공기 식별자 코드를 전역으로 변경해야 하는 경우, 이를 사용하는 데이터 모델을 구현한 모든 팀 간에 조정이 필요하다. 이 경우에 공유 데이터베이스를 사용하는 것보다 더 많은 비용이 발생할 수 있다.
TIP 시스템에서 여러 데이터 모델을 동시에 변경하는 경우가 많다면 마이크로서비스 경계를 다시 검토해야 한다.는 신호일 수 있다.
데이터 다운타임
우리의 독립적인 데이터 모델은 조정과 관련하여 몇 가지 큰 이점을 제공한다. 하지만 이는 제로 다운타임 데이터 모델 변경을 위해 구축되지는 않았다…기존 버전에 영향을 주는 데이터 모델 변경이 필요한 경우 가장 간단한 방법은 기존 마이크로서비스 버전을 삭제하고 데이터 변경을 구현한 새로운 마이크로서비스 인스턴스로 교체하는 것이다. 쿠버네티스 환경에서는 서비스에 미치는 영향을 최소화하면서 이를 수행할 수 있다. 제로 다운타임을 구현하려면 블루 그린 배포를 활용한다.
데이터: 소비자 영향
마이크로서비스가 자체 데이터를 소유한다는 결정을 내렸기 때문에 데이터 모델 변경의 영향은 서비스 자체로 제한된다. 따라서 서비스의 소비자에게 직접 영향을 주지 않고 데이터를 자유롭게 변경할 수 있다. 데이터 모델은 마이크로서비스 내에 캡술화되어 있기 때문에 마이크로서비스 팀이 더 자유롭게 변경할 수 있다. 하지만 앞서 언급했듯이 약간의 다운타임이 발생할 수 있다.
실제로 데이터 모델을 변경하려면 코드와 인터페이스의 변경도 필요할 수 있다. 하지만 소비자에게 직접적인 영향을 미칠 변경 사항을 구현하기 전에 먼저 데이터 모델 변경을 수행할 수 있도록 이러한 수정 사항을 분리하거나 시차를 두고 조정할 수 있다.

1986년 프레더릭 브룩스은 소프트웨어 복잡성에 대한 그의 논문 No Silver Bullet(은빛 총알은 없다)에서 다음과 같이 언급했다.
앞으로 10년 이내에 생산성, 신뢰성, 단순성을 10배 향상시킬 수 있는 단일 기술이나 관리 기법은 존재하지 않는다.
브룩스는 이러한 현상의 원인이 소프트웨어 시스템에는 필수적인 복잡성이 존재하기 때문이라고 설명한다. 코드 베이스에는 자체적인 구현 선택과 관련된 부수적 복잡성 accidental complexity 이 항상 존재한다. 복잡성의 대부분은 소프트웨어 시스템에 관련된 것이 아닌 도메인 자체를 모델링하는 문제와 관련이 있다. 브룩스는 시스템이 표현하려는 모델을 나타내는 정교한 데이터 세트, 데이터 간 관계, 알고리즘, 호출 흐름을 ‘본질적 복잡성essential complexity’이라 부른다. 본질적 복잡성을 넘어서 시스템을 단순화하려고 하면 핵심 모델에서 벗어나 더 이상 동일한 시스템이 되지 않는다.
중요한 것은 ‘No Silver Bullet를 읽은 사람들이 의문을 품는 것과 같이 “마이크로서비스가 본질적인 복잡성을 제거할 방법이 없다는 브룩스의 주장을 깨뜨렸는가?” 또는 “마이크로서비스 아키텍처가 부수적인 시스템 복잡성을 해결하고 개선할 수 있는가?” 다.
사실은 그렇지 않다. 마이크로서비스의 개념은 부수적인 복잡성과 더 나은 코딩 스타일을 위한 방법론이 아니라 본질적으로 다른 접근 방식이다. 마이크로서비스는 본질적인 복잡성을 제거할 수는 없지만 시스템의 한 부분에서 다른 부분으로 본질적인 복잡성을 이동할 수 있다.
간단히 말해 소프트웨어 시스템을 구축할 때 필요한 구현 부분(코드)과 운영 부분(배포와 오케스트레이션)에서 우리는 코드를 여러 개의 작은 마이크로서비스로 분할하여 더 간단하게 만들 수 있다. 그러나 이러한 변화는 운영을 어렵게 만든다. 한 부분을 간단하게 만들었지만 다른 부분은 어려워졌기 때문에 별 차이가 없는 것처럼 보인다. 하지만 ‘더 어려운 부분인 운영의 복잡성을 자동화할 수 있다면 실제로는 더 좋은 결과를 얻을 수 있다.
지난 10년 동안 소프트웨어 운영 자동화는 상당히 발전했다. 앤서블 Ansible, 퍼핏 Pupper, 셰프Chet. 테라폼, 도커, 쿠버네티스와 같은 운영 자동화 도구와 서버리스 함수, 다양한 클라우드 서비스를 사용하여 1986년에 브룩스가 상상했던 그 어떤 것보다 복잡한 운영을 훨씬 더 간단하게 만들 수 있었다. 하지만 실제로 코드를 작성하는 것은 1980년대와 마찬가지로 여전히 어렵다 (물론 몇 가지 발전이 있었지만 중요할 만큼은 아니다). 따라서 코딩에서 운영으로 복잡성을 전환한다면 소프트웨어 구축을 더 쉽게 만들 것이다.
TIP 마이크로서비스는 단순화를 제공한다… 마이크로서비스 아키텍처는 우리가 자동화, 설계, 코딩할 수 없는 영역에서 자동화에 능숙한 운영 영역으로 복잡성을 전환하는 것이다…
12.1.1 마이크로서비스 사분면
복잡성에 대해 더 자세히 알아보자. 시스템 이론에서는 까다로운 complicate 시스템과 복잡한 complex 시스템을 구분한다. 이는 의사 결정을 위한 시네핀 프레임워크Cynefin framework(https://hbr.org/2007/11/a-leaders-framework-for-decision-making)를 통해 더욱 확장되고 대중화되었다. 까다로운 시스템은 매우 정교하고 이해하기 어려울 수 있지만 본질적으로 예측 가능하며 한정된 수의 잘 정의된 규칙을 기반으로 한다. 복잡한 시스템은 본질적으로 비결정적이며 자유롭게 상호작용하는 많은 요소로 구성되어 결과적으로 새로운 행동을 일으킬 수 있다. 이러한 용어를 모놀리스와 마이크로서비스로 분류하면 모놀리스는 까다로운 시스템이며, 마이크로서비스는 복잡한 시스템에 더 가깝다.
• 마이크로서비스는 구현이 복잡하지만 단순한 아키텍처다.
• 모놀리스는 쉬운(하지만 단순한 것은 아닌) 아키텍처지만 구현이 까다롭다.

마이크로서비스 아키텍처의 핵심은 조정 비용을 최소화하는 것이다.
하지만 올바른 방향으로 나아가는지 어떻게 알 수 있을까? 물론 조정 비용을 절감하는 것이 우리의 핵심 목표지만 ‘조정 비용’을 값으로 측정할 수는 없다. 일부 팀은 ‘속도’ 또는 ‘안정성’을 측정하려고 하지만 이러한 값 또한 파생적이며 측정이 어렵기 때문에 마찬가지로 문제가 된다. 속도와 안정성의 증가를 인지할 수는 있지만 인과 관계를 주장하기 위해 새로운 속도를 무엇과 비교할 것인가? 아무도 마이크로서비스와 모놀리스 아키텍처로 동일한 시스템을 구축하지 않는다. 속도의 증가는 직관적인 보람을 느낄 수는 있지만 비과학적이다. 안정성 증가를 측정하려는 시도 또한 마찬가지다.
대신에 필자는 세 가지 측정 지표를 제안한다.
• 모든 팀에서 자율적인 팀의 평균 규모
• 자율적인 팀이 다른 팀을 기다리지 않고 작업할 수 있는 평균 시간(작업 대기는 일반적으로 중요한 종속성에 의해 발생한다)
• 성공적인 배포 빈도
세 가지 메트릭을 지속적으로 측정하고 전환이 올바른 방향으로 진행되고 있는지 확인함으로써 팀은 모든 마이크로서비스 특성에서 완벽을 달성해야 한다는 불안에서 벗어나 장기적인 성공을 위해 스스로를 해방시킬 수 있을 것이다.

(동일한 작업을 하는 팀의 규모는 점차 감소해야 하고, 팀이 독립적으로 작업할 수 있는 평균 시간과 배포 빈도는 증가해야 함)

Comments