Featured image of post 27. 크고 작은 모든 서비스들

27. 크고 작은 모든 서비스들

5부 - 아키텍처

서비스 지향 아키텍처와 마이크로서비스 아키텍처는 최근에 큰 인기를 끌고 있는데 이유는 다음과 같다.

  • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다.
  • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다.

서비스 아키텍처?

서비스를 사용한다는 것이 본질적으로 아키텍처에 해당하는지에 대해 생각해보면 명백히 사실이 아니다.

시스템의 아키텍처는 의존선 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다.

이러한 관점에서 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 아키텍처 관점에서 꼭 중요하다고 볼 수는 없다.

기능을 프로세스나 플랫폼에 독립적이 되게끔 서비스들을 생성하면 의존성 규칙 준수 여부와 상관 없이 큰 도움이 될 때가 많으나, 서비스 그 자체로는 아키텍처를 정의하지 않는다.

위에 언급한대로 시스템에서 아키텍처를 정의하는 요소는 의존성 규칙을 따르며 아키텍처 경계를 넘나드는 행위 자체이며, 결국 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다고 볼 수 있다.

서비스의 이점?

많은 사람들이 생각하는 서비스의 이점에 대해 아키텍처 관점의 근본적인 문제를 살펴보면 이러한 부분들이 완벽한 해결책은 아님을 알 수 있다.

결합 분리의 오류

시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점중 하나는 서비스 사이의 결합이 확실히 분리된다는 점이다.

  • 각 서비스는 서로 다른 프로세스에서 실행되므로 서비스는 다른 서비스의 변수에 직접 접근이 불가능하다.
  • 모든 서비스의 인터페이스는 반드시 잘 정의되어 있어야 한다.

서비스들이 변수를 통해 직접 접근할 수는 없지만 프로세서 내의 또는 네트워크 상의 공유 자원과 이로 인한 공유하는 데이터에 의해 서비스들이 강력하게 결합된다.

  • 서비스 사이를 오가는 데이터 레코드에 새로운 필드를 추가하면, 이 필드를 사용하는 모든 서비스는 반드시 변경되어야한다.
  • 필드에 담긴 데이터를 해석하는 방식을 사전에 완벽하게 조율해야 한다.

이로 인해 서비스들은 레코드에 강하게 결합되고, 서비스들 사이는 서로 간접적으로 결합되어버린다.

개발 및 배포 독립성의 오류

전담팀이 서비스를 소유하고 운영하게 되므로 각자 개발 및 배포할 수 있게되어 확장 가능한, 확장이 용이한 구조라고 생각하지만, 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.

  • 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 확장 가능한 시스템을 구축할 수 있다는 사실이 역사적으로 증명되어왔다.

결합 분리 오류에서 언급한 내용을 토대로 살펴보면 서비스라고 해서 항상 독립적으로 개발, 배포, 운영할 수 있는 것은 아니며, 결합된 정도에 맞게 조정해야한다.

야옹이 문제

이전에 예시로 들었던 택시 통합 시스템을 수많은 작은 마이크로 서비스를 기반으로 구축했다.

이러한 아키텍처에서 야옹이 배달 서비스를 런칭하겠다고 가정한다.

요구사항

  • 사용자는 집이나 사무실로 야옹이를 배달해달라고 주문할 수 있다.
    • 야옹이를 태울 다수의 승차 지점을 설정해야한다.
    • 승차 지점 중 한 곳에서 야옹이를 태운 후, 올바른 주로소 야옹이를 배달해야한다.

제약사항

  • 운전자가 고양이 알러지가 있는 경우 해당 운전자는 이 서비스에서 제외되어야한다.
  • 일반 택시 승객이 알러지가 있을 수 있으므로, 배차를 신청한 고객이 알러지가 있다면, 야옹이를 배달했던 차량은 배차에서 제외한다.

서비스 다이어그램을 살펴봤을 때 마이크로서비스들은 모두 결합되어 있어 독립적으로 개발하고 배포하거나 유지할 수 없다.

따라서 야옹이 배달 기능을 추가하려면 구성된 모든 마이크로서비스를 변경해야 한다.

이는 횡단 관심사(Cross-Cutting Concern)가 지닌 문제로 기능적 분해는 새로운 기능이 기능적 행위를 횡단하는 상황에서 매우 취약하다.

객체가 구출하다

SOLID 설계원칙에서, 다향적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 한다.

이를 통해 컴포넌트 아키텍처에서는 아래와 같은 전략을 활용할 수 있다.

원래 서비스 로직 중 대다수가 이 객체 모델의 기반 클래스들 내부로 녹아들었다.

  • 배차에 특화된 로직 부분은 Rides 컴포넌트로 추출되었다.
  • 야옹이에 대한 신규 기능은 Kittens 컴포넌트에 들어있다.
  • 이 두 컴포넌트는 기존 컴포넌트들에 있는 추상 기반 클래스를 템플릿 메서드나 전략 패턴 등을 이용해서 오버라이드한다.
  • 신규 컴포넌트 Rides, Kittens가 의존성 규칙을 준수한다.
  • 이 기능을 구현하는 클래스들은 UI의 제어 하에 팩토리가 생성한다.

이 전략을 따르더라도 야옹이 기능을 구현하려면 TaxiUI는 변경해야 하지만, 그 외의 것들은 변경할 필요가 없다.

따라서 야옹이 기능은 결합이 분리되며, 독립적으로 개발하여 배포할 수 있다.

컴포넌트 기반 서비스

서비스가 반드시 소규모 단일체(monolith)일 이유는 없다.

서비스는 SOLID 원칙대로 설계할 수 있으며 컴포넌트 구조를 갖출 수도 있다.

java의 경우 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각할 수 있다.

새로운 기능 추가 혹은 기능 확장은 기존 jar 파일에 정의돈 추상 클래스들을 확장하여 새로운 jar 파일로 만든다면, 새로운 기능 배포는 서비스를 재배포하는 문제가 아니라, 서비스를 로드하는 경로에 단순히 새로운 jar 파일을 추가하는 문제가 된다.

각 서비스의 내부는 자신만의 컴포넌트 설계로 되어 있어서 파생 클래스를 만드는 방식으로 신규 기능을 추가할 수 있다.

횡단 관심사

아키텍처 경계는 서비스 사이에 있지 않고 오히려 서비스를 관통하며, 서비스를 컴포넌트 단위로 분할한다.

모든 주요 시스템이 직면하는 횡단 관심사를 처리하려면, 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계해야한다.

서비스들은 시스템의 아키텍처 경계를 정의하지 않으며, 아키텍처 경계를 정의하는 것은 서비스 내에 위치한 컴포넌트이다.

결론

서비스는 시스템의 확정성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아니다.

시스템의 아키텍처는 구성 요소가 통신하고 실행되는 물리적인 매커니즘에 의해 아키텍처가 정의되는 것이 아닌, 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.

  • 서비스는 단 하나의 아케텍처 경계로 둘러싸인 단일 컴포넌트로 만들 수 있다.
  • 여러 아키텍처 경계로 분리된 다수의 컴포넌트로 구성할 수도 있다.
  • 클라이언트와 서비스가 강하게 결합되어 아키텍처적으로 아무런 의미가 없을 때도 있다.