..

팀이 만들어졌기에 새로운 서비스가 필요하다는 주장의 반박

TL;DR

서비스 분리가 복잡도를 낮춘다는 믿음은 잘못되었고, 오히려 시스템 전체 복잡도를 높일 수 있습니다. 따라서 서비스를 분리하는 것이 전체적인 속도를 늦추게 됩니다. 속도를 높이면서 적절히 서비스를 나누는 방법에 대한 아이디어와 복잡도 계산 방법을 제안합니다.

urge to write this article

종종 일을 하다보면 “이 서비스는 왜 이렇게 복잡하게 나뉘어있어?”와 같은 생각을 하게 됩니다. MSA 붐 이후에 관련된 서비스들이 여러개의 협력으로 만들어지는 제품은 매우 흔한 패턴이 되었습니다. 흔히 콘웨이 법칙에 따라서 서비스를 설계해야한다는 주장과 함께, 하나의 서비스를 여러 팀에서 관리하는 경우 속도가 느려진다는 불평이 많습니다. 팀이 나눠지면 새로운 서비스를 만들어서 배포를 자유롭게 수행하고, 그린 필드 프로젝트를 시작하고 싶다는 충동이 오죠. 하지만 정말 그게 좋은 방향일까요?

복잡성이 속도를 좌우한다

속도가 전부다

우리는 언제나 속도를 외칩니다. 불확실한 세상에서 제품을 만들기 위하여 여러 기능을 만들고, 방향을 수정하고, 결국 목표를 향해 나아가는 것. 다들 빠른 속도를 원하고, 엔지니어링은 속도(방향과 속력)을 만족하기 위한 구체적인 방법들을 제시합니다. 더욱 안정적인 제품을 만들고, 빠르게 반응하도록 만들고, 멋지게 만들고, 기능을 추가하더라도 속도가 점점 느려지지 않게 만들죠.

속도는 복잡도와 큰 연관관계가 있습니다. 복잡한 프로그램을 구현하는 것은 간단한 프로그램을 구현하는것과 비교하여 몇배의 시간이 들거에요. 동일한 기능을 하는 프로그램이더라도 더 많은 복잡성은 시간을 더 많이 소요하게 만들어서 우리의 속력을 늦추죠. 그래서 우리는 복잡도를 아주 싫어합니다. 프로젝트를 가능한 최소한의 복잡도만을 가지도록 노력하죠. 끊임없는 리팩터링과 추상화, 관심사의 분리를 통해 얻는 것은 단순함이죠.

팀이 견딜 수 있는 복잡도

팀이 견딜 수 있는 복잡도를 넘어갈 때 마다 속도는 복잡도에 반비례함. 팀이 견딜 수 있는 복잡도는 아래와 같이 아주 나이브하게 모델링할 수 있음.

C_capacity = BaseCapacity(TeamSize) × SkillMultiplier × ToolMultiplier

where:
  BaseCapacity(n) = n × (1 - communication_overhead(n))
  SkillMultiplier = weighted_avg(experience × domain_familiarity)
  ToolMultiplier = (1 + tooling_level + abstraction_quality)

이 경우 우리 팀의 복잡도 수용량을 계산해보면 좋음. 우리팀은 150임.

서비스와 시스템에 대한 복잡도 모델

시스템의 복잡도가 중요하다는 것은 모두가 공감할만한 가치이다. 게다가 대부분의 서비스를 나누려는 사람들도 더 단순하게 시스템을 만들고싶다는 명분이 있다. 그러나 서비스를 나누는 것이 항상 복잡도를 낮추는 것일까? 이러한 질문에 답하기 위하여 서비스의 복잡도와 시스템의 복잡도를 모델링하는 것이 필요하다.

서비스의 복잡도라는 개념은 아주 미묘하고 애매해서 엄밀하게 정의하기 어렵다. 그러나 정의없이 느낌만으로 서비스의 복잡도를 측정하는 것이 더 좋지 않다. 직관은 강력하지만, 완벽하지 않다.

나는 서비스를 하나의 노드로 보겠다. 그리고 서비스의 fanin, fan-out을 기준으로 복잡도를 생각해보겠다. 아주 나이브한 정의이고, 당연히 다들 생각이 다르다보니 달라지겠다. 나는 Henry-Kafura Flow Metric과 비슷하게 모델링 하였다.

소프트웨어 공학의 고전인 Henry-Kafura 메트릭은 모듈 간의 결합도를 $(FanIn \times FanOut)^2$로 정의한다. 그러나 나는 조금 더 단순화하여서 계산하였다.

$Complexity(S) = N + \lambda \times (FanIn + FanOut)$

여기에서 N은 서비스를 유지하는 최소 복잡도이다. 나는 15정도로 생각하는데 이는 공용 라이브러리, 배포 파이프라인, cpu와 같은 리소스 관리, 트래픽 모니터링, 알림, 분산 서버 디버깅등을 포함한 휴리스틱 값이다. 아주 단순한 서비스의 경우 이를 5까지 낮추어도 된다. 서비스의 level에 따라서, SLO에 따라서 다르게 가져가도 좋을 것이다.

예시 서비스 S1

S1 서비스의 복잡도는 몇일까? 15 + (2 * 5) + (2 * 3)으로 31로 정의할 수 있다. 단일 복잡도 지표로 이정도가 좋은지, 안좋은지 알 수는 없다.

시스템에 대한 복잡도 계산

그러면 N개의 서비스의 복잡도는 어떻게 될까? 아래의 시스템의 전체 복잡도를 어떻게 정의할까?

Complexcity(TargetServices) = Sum(Complexcity(S)) \in TargetServices

특정 서비스들의 집합의 복잡도는 각 서비스의 복잡도의 합으로 볼 수 있다. 그럼 그래프의 마지막에 있는 서비스는 어떻게 될까? db나 cache에 접근하는 것도 외부로 본다면 어느정도 비슷하게 모델링할 수 있다.

Fallacies of Distributed Computing

분산 시스템에서 api 호출은 되게 많은 문제들을 야기하는데, 이는 내부 호출보다 더 케이스가 많아진다.

함수 호출은 메모리 내에서 나노초(ns) 단위지만, 네트워크 호출은 밀리초(ms) 단위다. 실패 확률도 0%에서 0.1% 이상으로 뛴다. 내부 호출을 외부 호출로 바꾸는 것은, 상수 시간($O(1)$) 작업을 확률적 실패($P(fail)$) 작업으로 바꾸는 행위다

모든 엣지(Edge)가 평등하지 않다. 조회(Read)는 캐싱하면 그만이지만, 상태 변경(Write)이 포함된 연결은 분산 트랜잭션을 요구하며 복잡도를 제곱으로 증가시킨다. 따라서 위 수식에서 Write API가 포함된 경우 가중치를 훨씬 높게 잡아야 한다.

좋은 추상화는 서비스의 복잡도를 줄입니다

우리는 위에서 서비스, 그리고 시스템의 복잡도 모델을 정의하고 계산해보았다. 위 모델이 우리의 직관을 어느정도 만족하는지 알아보기 위해 추상화가 잘 되어서 서비스가 잘 나누어지는 경우를 살펴보자.

서비스가 잘 나누어지지 않고, 마구 만들어지는 경우

너무 마구 만들어지면 기본 오버헤드가 지배함.

Universal Scalability Law

일단 나누고 합치면 된다는 주장의 반박

merge함수에 대한 정의

merge가 너무 커지는 경우에 대한 실제 예시

merge함수에 대한 특성

콘웨이 법칙의 반란, 다시 보는 맨먼스 미신

콘웨이 법칙에 따라서 시스템을 설계해야한다는 주장에 대한 반론

맨먼스 미신을 인용하면서 커뮤니케이션이 지수적으로 커지는 문제점 지적

콘웨이 법칙은 관찰 결과일 뿐이지, 꼭 그래야한다는 것은 아님

인지 부하가 줄어드는 관점이라고 하지만 실제로는 복잡도가 폭발함

조직 구조는 그렇게 쉽게 바뀌지 않고, 정말 독립적이지도 않음

MSA의 이점이 있는 경우

배포가 병목이 아닌 경우가 많음(리틀의 법칙)

정말 피처때문에 같이 영향을 받는다면 그걸 다른 서비스로 쪼갠다고 온전히 없어지지 않을 수 있음

적절한 전략으로 회피 가능

같은 코드 베이스에서 협업을 잘 하기 위한 인터페이스 정의 방법, arch unit, code owner 데이터베이스 스키마 분리도 가능

자원 격리가 있어야한다면 가능

그러면 어디까지 나누어야할까?

복잡도가 줄어드는 방향으로 가야함

U자형 곡선에 대한 최적화 이론

배포가 병목이면 그럴 수 있음

왜 이러한 실수를 반복할까?

  1. 업적의 가시성
  2. 책임 회피
  3. 팀 자율성에 대한 욕구