[Clean Architecture] 27장. '크고 작은 모든' 서비스들
|
Introduction
- 서비스 지향 아키텍처와 마이크로서비스 아키텍처는 최근에 큰 인기를 끌고 있음
- 이유
- 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다.
- 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다.
서비스 아키텍처?
- 먼저 서비스를 사용한다는 것이 본질적으로 아키텍처에 해당하는지에 대해 생각해보자.
- 시스템의 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의됨
- 단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 아키텍처 관점에서 꼭 중요하다고 볼 수는 없음
- 모든 서비스가 반드시 아키텍처 관점에서 중요해야만 한다는 뜻은 아님
- 기능을 프로세스나 플랫폼에 독립적이 되게끔 서비스들을 생성하면 의존성 규칙 준수 여부와 상관없이 큰 도움이 될 때가 많음
- 그러나 서비스 그 자체로는 아키텍처를 정의하지 않음
- 함수들의 구성 형태도 이와 비슷함
- 모노리틱 시스템이나 컴포넌트 기반 시스템에서 아키텍처를 정의하는 요소는 바로 의존성 규칙을 따르며 아키텍처 경계를 넘나드는 함수 호출들
- 반면 시스템의 나머지 많은 함수들은 행위를 서로 분리할 뿐이며, 아키텍처적으로는 전혀 중요하지 않음
- 서비스도 마찬가지
- 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않음
- 아키텍처적으로 중요한 서비스도 있지만, 중요하지 않은 서비스도 존재
- 이 장에서 우리가 관심을 가지는 서비스는 아키텍처적으로 중요한 서비스
서비스의 이점?
- 현재 인기를 끄는 서비스 아키텍처라는 통설에 대해 각 이점을 하나씩 따져보자.
결합 분리의 오류
- 시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점
- 서비스는 다른 서비스의 변수에 직접 접근할 수 없음
- 그리고 모든 서비스의 인터페이스에 반드시 잘 정의되어 있어야 함
- 이 말에는 어느 정도 일리가 있지만, 꼭 그런 것만은 아님
- 물론 서비스는 개별 변수 수준에서는 각각 결합이 분리됨
- 하지만 프로세서 내의 또는 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재함
- 더욱이 공유하는 데이터에 의해 이들 서비스는 강력하게 결합되어 버림
- 인터페이스가 잘 정의되어 있어야 한다는 이점에 대해서라면 이는 명백히 사실임
- 하지만 함수의 경우에도 전혀 다르지 않음
- 서비스 인터페이스가 함수 인터페이스보다 더 엄밀하거나, 더 엄격하고, 더 잘 정의되는 것은 아님
- 이러한 이점은 환상에 불과함
개발 및 배포 독립성의 오류
- 서비스를 사용함에 따라서 예측되는 또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점
- 이러한 개발 및 배포 독립서은 확장 가능한 것으로 간주됨
- 대규모 엔터프라이즈 시스템을 독립적으로 개발하고 배포 가능한 수천 개의 서비스들을 이용하여 만들 수 있다고 믿음
- 시스템의 개발, 유지보수, 운영 또한 비슷한 수의 독립적인 팀 단위로 분할할 수 있다고 여김
- 이러한 믿음에도 어느 정도 일리가 있지만, 극히 일부일 뿐
- 첫 번째로, 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 역사적으로 증명되어 옴
- 따라서 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아님
- 두 번째로 ‘결합 분리의 오류’에 따르면 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아님
- 데이터나 행위에서 어느 정도 결합되어 있다면, 결합된 정도에 맞게 개발, 배포, 운영을 조정해야만 함
야옹이 문제
- 9장에서 예를 들었던 택시 통합 시스템
- 확장 가능한 시스템을 구축하고 싶었기에, 수많은 작은 마이크로서비스를 기반으로 구축하기로 결정함
- 이제 이 시스템을 일 년 이상 운영해 왔다고 가정
- 새로운 서비스
- 사용자는 자신의 집이나 사무실로 야옹이를 배달해달라고 주문할 수 있는 서비스
- 회사는 도시 전역에 야옹이를 태울 다수의 승차 지점을 설정해야 할 것
- 야옹이 배달 주문이 오면, 근처의 택시가 선택되고, 승차 지점 중 한 곳에서 야옹이를 태운 후, 올바른 주소로 야옹이를 배달해야 함
- 택시 업체는 참여할 수도, 거부할 수도 있음
- 고양이 알러지 운전자는 당연히 제외하고, 일반 택시 승객 역시도 고려가 필요
- 지난 3일 사이에 야옹이를 배달했던 차량은 배차되지 않는 기능도 필요
- 이 기능을 구현하려면 서비스 중 어디를 변경해야 할까? 전부다
- 이 서비스들은 모두 결합되어 있어서 독립적으로 개발하고, 배포하거나, 유지될 수 없음
- 이게 바로 횡단 관심사 cross-cutting concern 가 지닌 문제
- 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하게 마련
객체가 구출하다
- 컴포넌트 기반 아키텍처에서는 이 문제를 어떻게 해결했을까?
- SOLID 설계 원칙을 잘 들여다보면, 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함을 알 수 있음
- 경계를 주목하자
- 의존성들이 의존성 규칙을 준사한다는 점도 주목하자
- 배차특화된 로직 부분은 Rides 컴포넌트로 추출
- 야옹이에 대한 신규 기능은 Kittens 컴포넌트로 추출
- 이 두 컴포넌트는 기존 컴포넌트들에 있는 추상 기반 클래스를 템플릿 메서드나 전략 패턴 등을 이용해서 오버라이드함
- 두 개의 신규 컴포넌트인 Rides 와 Kittens 가 의존성 규칙을 준수한다는 점에 다시 한 번 주목
- 야옹이 기능은 결합 분리되며, 독립적으로 개발하여 배포 가능
컴포넌트 기반 서비스
- 서비스에도 이렇게 할 수 있을까? 예
- 서비스가 반드시 모노리틱이어야 할 이유는 없음
- 서비스는 SOLID 원칙대로 설계할 수 있으며, 컴포넌트 구조를 갖출 수도 있음
- 이를 통해 서비스 내의 기존 컴포넌트들을 변경하지 않고도 새로운 컴포넌트를 추가할 수 있음
- 자바의 경우, 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각하라
- 새로운 기능 추가 혹은 기능 확장은 새로운 jar 파일로 만듦
- 이때 새로운 jar 파일을 구성하는 클래스들은 기존 jar 파일에 정의된 추상 클래스들을 확장해서 만듦
- 그러면 새로운 기능 배포는 기존 jar 파일을 재배포하는 문제가 아니라, 서비스를 로드하는 경로에 단순히 새로운 jar 파일을 추가하는 문제가 됨
- 다시 말해 새로운 기능을 추가하는 행위가 개방 폐쇄 원칙을 준수하게 됨
- 서비스들의 존재는 이전과 달라진 게 없지만, 각 서비스의 내부는 자신만의 컴포넌트 설계로 되어 있어서 파생 클래스를 만드는 방식으로 신규 기능을 추가할 수 있음
- 파생 클래스들은 각자의 컴포넌트 내부에 놓임
횡단 관심사
- 아키텍처 경계가 서비스 사이에 있지 않다는 사실
- 오히려 서비스를 관통하여, 서비스를 컴포넌트 단위로 분할함
- 모든 주요 시스템이 직면하는 횡단 관심사를 분리하려면, 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계해야 함
- 이 서비스들은 시스템의 아키텍처 경계를 정의하지 않음
- 아키텍처 경계를 정의하는 것은 서비스 내에 위치한 컴포넌트
결론
- 서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아님
- 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의됨
- 시스템의 구성 요소가 통신하고 실행되는 물리적인 메커니즘에 의해 아키텍처가 정의되는 것이 아님
- 서비스는 단 하나의 아키텍처 경계로 둘러싸인 단일 컴포넌트로 만들 수 있음
- 혹은 여러 아키텍처 경로로 분리된 다수의 컴포넌트로 구성할 수도 있음
- 드물게는 클라이언트와 서비스가 강하게 결합되어 아키텍처적으로 아무런 의미가 없을 때도 있음