17장, 마이크로서비스 아키텍처 스타일

2022.08.02

17.1 역사

마이크로서비스는 사용 초기부터 이름이 붙여졌고 2014년 3월 마틴 파울러(Martin Fowler)와 제임스 루이스(James Lewis)가 쓴 Microservices라는 유명한 블로그 게시글 덕분에 널리 퍼지게 되었다. 두 사람은 비교적 새로운 이 아키텍처 스타일에서 많은 공통점을 찾아냈고, 그들이 쓴 블로그 게시글은 호기심 많은 아키텍트들이 아키텍처를 정의하고 그 하부에 내재된 철학을 이해하는 데 도움을 주었다.

마이크로서비스는 소프트웨어 프로젝트의 논리적 설계 프로세스를 강조한 도메인 주도 설계 사상의 영향을 많이 받았다. 특히, 디커플링 스타일을 나타낸 경계 콘텍스트(bounded context) 개념은 마이크로서비스에 결정적인 영향을 미쳤다.

재사용은 유익하지만 커플링이 문제이다(소프트웨어 아키텍처 제1법칙). 재사용을 선호하는 시스템을 설계하다 보면 결국 상속이나 조합을 이용하여 재사용하기 위해 커플링이 맺어지게 된다.

고도의 디커플링이 아키텍트의 목표라면 재사용보다 중복을 우선할 것이다. 마이크로서비스의 주요 목표는 경계 콘텍스트의 논리적 개념을 물리적으로 모델링하는 고도의 디커플링이다.

17.2 토폴로지


마이크로서비스는 단일 목적만 가지기 때문에 오케스트레이션 기반의 서비스 지향 아키텍처와 같은 다른 분산 아키텍처보다 서비스 규모가 훨씬 작다. 실제로 각 서비스에는 데이터베이스 및 기타 종속적인 컴포넌트 등 서비스가 독립적으로 작동되는 데 필요한 모든 것들이 준비되어 있다.

17.3 분산

마이크로서비스는 분산 아키텍처를 형성한다. 서비스 자체 프로세스로 실행되며, 원래 물리적인 컴퓨터를 의미했지만 이제는 가상 머신과 컨테이너로 빠르게 진화했다. 서비스를 이 정도로 디커플링하면 애플리케이션을 호스트하는 육중한 멀티테넌트 인프라 아키텍처에서 자주 발생하는 문제들을 간단히 해결할 수 있다. 예를 들어, 애플리케이션 서버를 이용해 다수의 애플리케이션을 관리할 떄에도 네트워크 대역폭, 메모리, 디스크 공간 등 여러 가지 혜택을 운영 측면에서 재사용할 수 있다. 그러나 지원해야 할 애플리케이션이 계속 늘어나면 결국 일부 리소스는 공유 인프라의 제약을 받을 수 밖에 없다.

이처럼 뭔가 공유함으로써 불거지는 문제들은 각 서비스를 자체 프로세스로 분리하면 모두 자연스럽게 해소된다. 이제는 클라우드 리소스와 컨테이너 기술을 활용해 도메인 레벨, 운영 레벨 모두 디커플링의 이점을 누릴 수 있다.

마이크로서비스의 분산 속성 탓에 성능은 다소 부정적이다. 아무래도 네트워크 호출은 메서드 호출보다 오래 걸리고 엔드 포인트마다 보안 검증 절차를 거치면 그만큼 처리 시간이 소요되므로 시스템을 설계하는 아키텍트는 서비스 세분도에 대해 심사숙고해야 한다.

마이크로서비스는 분산 아키텍처의 일종이므로 숙련된 아키텍트라면 개발자가 서비스 경계를 넘나드는 트랜잭션을 사용하지 않도록 권고해야 한다. 이 아키텍처는 서비스를 얼마나 세분화할 것인가를 잘 결정하는 것이 성공의 관건이다.

17.4 경계 콘텍스트

마이크로서비스의 근본 철학은 경계 컨텍스트(bounded context)개념이다. 서비스마다 도메인이나 워크플로를 모델링하는 개념이다. 클래스, 기타 부속 컴포넌트, 데이터베이스 스키마 등 애플리케이션 작동에 필요한 모든 것들이 각 서비스에 들어간다.

각각의 마이크로서비스는 어느 한 도메인이나 그 서브 도메인을 나타낸다. 여러 면에서 마이크로서비스는 도메인 주도 설계의 논리적인 개념을 물리적으로 구현한 것이다.

17.4.1 세분도

아키텍트는 마이크로서비스의 알맞은 세분도를 찾기 위해 고심하다가 종종 서비스를 너무 잘게 나누는 실수를 저지르곤 한다.

마이크로서비스라는 용어는 명칭(label)이지, 명세(description)가 아니다.

마틴파울러

많은 개발자는 마이크로서비스라는 용어를 마치 지령(commandment)처럼 받아들여 서비스를 지나치게 세분화하기 시작했다.

서비스 경계(service boundary)는 도메인이나 워크플로를 캡처하는 것이 목표이다. 어느 애플리케이션에서 당연하게 여겨지는 경계가 다른 시스템 파트에서는 너무 단위가 클 수도 있고, 어떤 비지니스 프로세스는 다른 비지니스 프로세스보다 더 단단히 커플링되어 있을지 모른다.

아키텍트가 적절한 경계를 찾는데 도움이 될 만한 몇 가지 가이드라인을 제시하겠다.

목적

가장 확실한 경계는 바로 이 아키텍처 스타일의 본래 의도인 도메인이다. 각 마이크로서비스가 기능적으로 매우 응집되어 있고 전체 애플리케이션을 대표하여 하나의 핵심 기능을 제공하는 것이 가장 이상적인 모습이다.

트랜잭션

여러 엔티티가 함께 개입하여 작동되는 트랜잭션은 아키텍트에게 좋은 서비스 경계 후보입니다. 분산 아키텍처에서 트랜잭션은 문제가 될 소지가 있으므로 그런 문제를 방지할 수 있도록 설계하는 것이 바람직하다.

코레오그래피2

도메인 격리는 아주 잘 되어 있지만 서로 광범위한 통신을 해야 제대로 작동되는 서비스 세트를 구축할 경우, 아키텍트는 통신 오버헤드를 줄이기 위해 더 큰 서비스로 다시 뭉치는 것을 고려해야 할 수도 있다.

좋은 서비스 설계안을 도출하는 유일한 방법은 이터레이션이다. 처음 한 번 시도로는 완벽한 세분도, 데이터 종속성, 통신 스타일을 찾아내기 어렵지만, 여러 가지 옵션을 반복해서 적용해 보면 좋은 방향으로 설계를 다듬어갈 수 있다.

17.4.2 데이터 격리

마이크로서비스는 경계 콘텍스트 개념에 따라 데이터를 격리해야 한다. 마이크로서비스 아키텍처는 통합 지점으로 사용되는 공유 스키마, 데이터베이스 등 모든 종류의 커플링을 없애려고 한다.

데이터 격리는 아키텍트가 서비스 세분도를 살필 때 반드시 고려해야 할 팩터이다.

마이크로서비스 아키텍처에서는 어떻게 하면 아키텍처 전체에 데이터를 분산시킬 수 있을지 결정해야 한다. 도메인을 어떤 팩트에 대한 진실 공급원으로 식별하여 그 값을 가져올 수 있게 잘 조정하든지, 아니면 데이터베이스 복제나 캐시 기술로 정보를 분산시키든지, 뭔가 구체적인 방안이 필요하다.

데이터 격리만으로도 골치가 아프지만 긍정적인 부분도 있다. 여러 팀이 단일 데이터베이스의 속박에서 벗어나게 되어 각 서비스마다 단가, 스토리지 종류, 그 밖의 여러 요소들을 저울질하여 적합한 도구를 선택할 수 있다.

17.5 API 레이어

마이크로서비스 다이어그램을 보면 대부분 필수는 아니지만 여러 시스템 컨슈머 사이에 API 레이어가 있다. API 레이어는 프록시를 경유하여 간접화하거나 네이밍 서비스 같은 운영 장치에 물려서 유용한 작업을 수행하기 좋은 위치에 있기 때문에 이 아키텍처에서 많이 쓰인다.

API 레이어는 다양한 용도로 활용할 수 있지만 이 아키텍처의 기본 철학에 충실하려면 API 레이어를 중재자나 오케스트레이션 도구로 사용하지 말아야 한다. 모든 비지니스 로직은 경계 콘텍스트 내부에서 일어나야 하며, 오케스트레이션 등의 다른 로직을 중재자에 넣는 것은 규칙 위반이다.

17.6 운영 재사용

마이크로서비스가 커플링보다 복제를 선호한다고 하였다. 그러면 모니터링, 로깅, 회로 차단기 등의 운영 관심사와 같이 실제로 커플링이 더 유리한 아키텍처 부분은 어떻게 처리해야 할까? 전통적인 서비스 지향 아키텍처의 철학에 따르면 도메인이든 운영이든 가급적 많은 기능을 재사용하는 것이 좋다. 마이크로 서비스 아키텍트는 이 두 가지 관심사를 분리하고자 한다.

여러 마이크로서비스를 구축한 이후에 잘 살펴보면 각 마이크로서비스에 공통적인 요소가 있고 그 유사성을 활용하면 더 유리한 부분이 있음을 알게 된다. 예를 들어, 서비스 팀마다 자체 모니터링 체제를 구축하도록 허용하면 팀별로 알아서 잘하리라 장담할 수 있을까? 또 업그레이드 같은 문제는 어떻게 처리할까?

이 문제를 해결하는 방법이 바로 사이드카 패턴(sidecar pattern)이다.


위 그림에서 보다시피, 공통 운영 관심사를 각 서비스마다 별도의 컴포넌트에 두고, 해당 팀이나 공유 인프라팀이 소유할 수 있도록 하자. 사이드카 컴포넌트는 팀이 서로 커플링되면 더 유리한 모든 운영 관심사를 도맡아 처리한다. 가령 모니터링 도구를 업그레이드할 때가 되면 공유 인프라팀이 사이드카를 업데이트하는 방식으로 각 마이크로서비스는 신기능을 받아 사용할 수 있다.

각 서비스에는 공통 사이드카가 포함돼 있으므로 서비스 메시(service mesh)를 구축하면 로깅, 모니터링 등의 관심사를 아키텍처 전체적으로 일원화화여 제어할 수 있다. 공통 사이드카 컴포넌트는 모든 마이크로서비스에 대해 일관된 운영 인터페이스를 제공한다.


각 사이드카는 서비스 플레인에 연결되어 자신의 서비스에 일관된 인터페이스를 제공한다.


서비스 메시 자체는 개발자가 서비스를 전체적으로 엑세스할 수 있는 콘솔 역할을 한다.

위의 그림에서 보다시피, 전체 메시에서 각 서비스는 하나의 노드이다. 서비스 메시는 각 팀이 모니터링 레벨, 로깅, 그 밖의 공통 운영 관심사 등 운영과 커플링된 부분을 글로벌하게 제어하는 콘솔이다.

아키텍트는 마이크로서비스 아키텍처에 탄력성을 부여하는 수단으로 서비스 디스커버리(service discovery)를 사용한다. 어느 하나의 서비스를 직접 호출하는게 아니라, 모든 요청이 서비스 디스커버리 도구를 거치도록 하면 요청 수와 빈도를 모니터링할 수 있고 필요시 서비스 인스턴스를 늘려 확장성/탄력성을 줄 수 있다.

17.7 프런트엔드

유저 인터페이스와 백엔드 역시 분리되는 모습이 가장 좋다. 마이크로서비스 초기 비전에는 유저 인터페이스가 DDD 원칙에 충실한 경계 콘텍스트의 일부로 포함되어 있었지만, 웹 애플리케이션 및 여타 외부 제약조건에서 필요로 하는 분할의 실용성 때문에 이 목표는 달성하기 어렵다. 그래서 마이크로서비스 아키텍처의 유저 인터페이스는 보통 두 가지 스타일로 나타난다.


유저 요청을 처리하기 위해 단일 유저 인터페이스가 API 레이어를 통해 호출하는 모놀리식 프런트엔드이다. 이 프런트엔드는 리치 데스크톱(rich desktop), 모바일, 웹 애플리케이션의 형태로 구현한다. 요즘은 자바스크립트 웹 프레임워크를 응용한 단일 유저 인터페이스로 개발하는 웹 애플리케이션이 대세인 것 같다.


마이크로프런트엔드는 유저 인터페이스 레벨의 컴포넌트를 백엔드 서비스로 활용하여 유저 인터페이스를 동기적인 수준으로 세분화하고 격리한다. 각 서비스는 자기 서비스에 해당하는 유저 인터페이스를 내보내고, 프런트엔드는 그렇게 내보내진 유저 인터페이스 컴포넌트를 조정한다. 이런 식으로 유저 인터페이스에서 백엔드 서비스에 이르기까지 서비스 경계를 분리함으로써 전체 도메인을 단일 팀 내부에 통합시키는 것이다.

17.8 통신

마이크로서비스를 구축하는 아키텍트와 개발자는 데이터 격리와 통신 모두에 영향을 미치는 적절한 세분도를 찾고자 노력한다. 올바른 통신 스타일을 발견하는 것 또한 팀이 서비스를 디커플링하면서 유용한 방향으로 조정하는 데 도움이 된다.

아키텍트는 동기로 할지, 비동기로 할지, 근본적인 통신 방식을 결정해야 한다. 일반적으로 마이크로서비스 아키텍처는 프로토콜 인지 이종 간 상호 운용성(protocol-aware heterogeneous interoperability)을 활용하는데, 이 긴 용어는 아래에서 하나씩 설명하겠다.

프로토콜 인지, protocol-aware

마이크로서비스는 운영 커플링을 방지하고자 중앙 통합 허브를 갖고 있지 않기 때문에 각 서비스는 다른 서비스를 호출하는 방법을 알고 있어야 한다. 즉, 서비스는 다른 서비스를 호출할 때 어떤 프로토콜을 사용할지 알아야 한다. 아키텍트는 여러 서비스가 상대방을 호출하는 방식(REST 레벨, 메시지 큐 등)을 표준화한다.

이종, heterogeneous

마이크로서비스는 분산 아키텍처라서 각 서비스마다 구현 기술 스택이 상이할 수 있다. 이종이란, 서비스마다 사용하는 플랫폼이 저마다 다른 폴리그랏(polyglot)환경을 완벽하게 지원한다는 뜻이다.

상호 운용성, interoperability

여러 서비스가 서로 호출한다는 뜻이다. 마이크로서비스에서 트랜잭셔널 메서드 호출은 권장하지 않지만, 어쨋거나 서비스는 네트워크를 통해 다른 서비스를 호출하여 정보를 주고받으면서 협력해야 한다.

비동기 통신은 이벤트와 메시지를 주로 사용하며 내부적으로 이벤트 기반 아키텍처를 활용한다. 브로커 패턴과 중재자 패턴은 마이크로서비스에서 각각 코레오그래피 패턴과 오케스트레이션 패턴으로 나타난다.

17.8.1 코레오그래피와 오케스트레이션

코레오그래피(choreography)는 브로커 이벤트 기반의 아키텍처와 통신 스타일이 동일하다. 즉, 이 아키텍처는 중앙의 중재자가 따로 없고 경계 콘텍스트 철학에 충실하다. 따라서 아키텍트는 서비스 간에 분리된 이벤트를 구현하는 것이 자연스럽다고 생각한다.

도메인/아키텍처 동형성(domain/architecture isomorphism)은 특정한 문제에 어떤 아키텍처 스타일이 얼마나 적합한지 평가할 때 아키텍트가 잘 살펴보아야 할 핵심 특성이다. 이 용어는 아키텍처의 형상이 특정 아키텍처 스타일에 어떻게 매핑되는지 기술한다.

마이크로서비스 아키텍트는 디커플링을 추구하므로 마이크로서비스의 형상은 브로커 이벤트 기반 아키텍처를 닮았고 이 두 패턴은 서로 공생 관계이다.


유저는 유저 위시 리스트의 상세 정보를 요청한다. 하지만 필요 정보가 전부 다 CustomerWishList 서비스에 있는 건 아니어서 CustomerDemographics 서비스를 호출해 모자란 정보를 보충한 결과를 유저에게 반환한다.

마이크로서비스 아키텍처는 서비스 지향 아키텍처처럼 전역 중재자를 따로 두지 않으므로 여러 서비스를 조정해야 할 경우에는 아래처럼 스스로 로컬 중재자를 만들 수 있다.


개발자는 주어진 고객의 전체 정보를 조회하는 호출을 조정하는, 이 일만 담당하는 서비스를 만든다. 유저가 중재자를 호출하면 이 중재자는 다른 서비스를 호출한다.

코레오그래피 아키텍트는 최대한 디커플링한다는 아키텍처 스타일의 철학을 고집함으로써 가장 많은 이점을 이끌어내려고 하지만, 에러 처리, 조정 같은 공통적인 문제는 코레오그래피 환경에서 훨씬 더 복잡해진다.


처음 호출된 서비스는 자신의 다른 도메인 책임과 더불여 여러 타 서비스를 전체적으로 조정하는 중재자 역할도 겸한다. 이런 패턴을 프런트 컨트롤러 패턴(front controller pattern)이라고 한다. 어떤 한 서비스가 명목상 더 복잡한 중재자 노릇을 하는 셈인데, 이 패턴은 서비스 복잡도가 증가하는 단점이 있다.


비지니스 워크플로에 필요한 복잡한 처리를 담당하면서 조정 역할도 수행하는 중재자를 두면 서비스 간 커플링은 발생하지만 어느 한 서비스가 조정 작업을 전담하므로 다른 서비스는 거의 영향을 받지 않는다. 사실, 도메인/워크플로는 내재적으로 커플링되는 경우가 많다. 아키텍트가 할 일은, 도메인과 아키텍처 두 마리 토끼를 모두 쫓는 방향으로 커플링을 가장 잘 나타낼수 있는 방법을 찾아내는 것이다.

17.8.2 트랜잭션과 사가

분산 애플리케이션에서는 데이터베이스 역시 동일한 수준의 디커플링이 필요하므로 모놀리식 애플리케이션에서 별 문제가 아니었던 원자성 문제도 대두된다.

서비스 경계를 넘나드는 트랜잭션은 그 자체로 마이크로서비스 아키텍처의 핵심 디커플링 원칙에 위배된다.(그리고 가장 나쁜 형태의 동적 커네이선스 값 커네이선스를 유발한다.) 마이크로서비스 아키텍처를 구축한 후 뭔가 트랜잭션으로 서비스를 엮어야 할 필요가 생겼다면 십중팔구 설계를 지나치게 세분화한 것이다. 트랜잭션 경계는 서비스 세분도를 가늠할 수 있는 일반적인 지표 중 하나이다.

마이크로서비스에 트랜잭션을 걸지 마세요. 대신 세분도를 바로잡으세요!


서비스는 여러 서비스 호출에 대해 중재자 노릇을 하면서 트랜잭션을 조정한다. 중재자는 트랜잭션을 구성하는 파트를 하나씩 호출하여 성공/실패 여부를 기록하고 그 결과에 따라 흐름을 조정한다. 만사가 계획대로 흘러간다면 서비스의 모든 값과 해당 데이터 베이스 레코드는 동기 업데이트될 것이다.


위 그림에서 트랜잭션의 첫 번째 파트는 성공했지만 두 번쨰 파트가 실패한 경우, 중재자는 지금까지 성공한 모든 트랜잭션 파트에게 과거에 처리했던 내용을 언두(undo)하라는 요청을 보낸다. 이런 종류의 트랜잭션 조정을 보상 트랜잭션 프레임워크(compensating transaction framework)라고 한다. 여기에 비동기 요청이 끼어들고, 특히 보류된 트랜잭션 상태에 따라 새로운 요청이 등장하면서 설계가 무척 복잡해진다. 또 네트워크 레벨에서도 조정 트래픽이 꽤 많이 발생한다.

트랜잭션 작업마다 두(do)/언두(undo) 로직을 개발하는 식으로 보상 트랜잭션 프레임워크를 구현할 수도 있다.

여러 서비스에 트랜잭션을 걸어주는 것이 기술적으로 불가능한 것이 아니지만, 이럴거면 굳이 마이크로서비스 패턴을 선택할 이유가 없다. 그래도 예외가 있기 마련이니, 필요한 경우 사가 패턴을 조금씩 곁들여 사용할 것을 권고한다.

여러 서비스에 트랜잭션을 걸어야 하는 경우도 있다. 하지만 아키텍처 전반적으로 트랜잭션이 남용된다면 뭔가 단단히 잘못된 것이다.

17.9 아키텍처 특성 등급

아키텍처 특성별점
분할 유형도메인
퀀텀 수하나 또는 여러 개
배포성⭐⭐⭐⭐
탄력성⭐⭐⭐⭐⭐
진화성⭐⭐⭐⭐⭐
내고장성⭐⭐⭐⭐
모듈성⭐⭐⭐⭐⭐
전체 비용
성능⭐⭐
신뢰성⭐⭐⭐⭐
확장성⭐⭐⭐⭐⭐
단순성
시험성⭐⭐⭐⭐

마이크로서비스는 당연히 도메인 중심적인 아키텍처로서 각 서비스의 경계와 도메인이 일치해야 한다. 또한 현대 아키텍처 중에서 가장 독보적인 퀀텀을 갖고 있는데, 여러 방면에서 퀀텀 측정이 의미하는 바를 가장 잘 나타내는 사례이다. 극도의 디커플링을 추구하는 이 아키텍처의 철학은 수많은 이들의 두통을 유발하지만, 잘만 만들면 어마어마한 이점을 누릴 수 있다.

17.10 더 읽을거리

  • 샘 뉴먼(Sam Newman)의 마이크로 서비스 아키텍처 구축 (한빛미디어, 2017)
  • 마크 리처즈(Mark Richards)의 Microservices vs Service-Oriented Architecture (O'reilly, 2016)
  • 마크 리처즈(Mark Richards)의 Microservices AntiPatterns and Pitfalls (O'reilly, 2016)