1장. 결합도와 응집력

문법식·2022년 8월 26일
0

마이크로서비스 경계를 정의할 때 결합도와 응집력 사이의 균형을 이해해야 한다. 결합도는 한 가지를 바꾸면 다른 것도 바꿀 필요가 있는 방식을 말하며, 응집력은 관련된 코드를 그룹으로 묶는 방식을 뜻한다. 두 개념은 직접적으로 연결되어 있다. 콘스탄틴의 법칙에서 둘 사이의 관계를 명확하게 설명한다.

구조는 응집력이 높고 결합도가 낮을 때 안정적이다.

콘스탄틴의 법칙은 잘 적용된다. 밀접하게 관련된 두 코드가 있는 경우, 관련 기능이 두 코드에 분산되어 있으므로 응집력이 떨어진다. 또한 관련된 코드가 변경될 경우에는 두 코드 모두 변경될 필요가 있기에 높은 결합도를 보인다.
코드 시스템의 구조가 변경되면, 분산 시스템의 서비스 경계에 걸친 변경 비용이 너무 높으므로 처리하는 데 비용이 많이 들 것이다. 아마도 서비스 계약을 위한 변경의 영향을 처리하기 위해, 하나 이상의 독립적으로 배포 가능한 서비스에 변경을 수행할 경우 큰 장애물이 된다.
모놀리스의 문제점은 결합도와 응집력이 너무 자주 반대로 움직이는 것이다. 응집력이 있는 코드를 함께 변경이 가능하게 유지하는 대신, 우리는 관련 없는 모든 유형의 코드를 가져와서 한데 붙여 놓는다. 마찬가지로, 느슨한 결합은 실제로 존재하지 않는다. 즉 코드에서 한 행은 충분히 쉽게 변경할 수 있을지도 모르지만, 모놀리스의 나머지 부분에 잠재적인 영향을 미치지 않으면서 해당 변경사항을 배포할 수는 없으며, 전체 시스템을 확실히 재배포해야 한다.
또한 우리의 목표는 최대한 독립적인 배포 가능성의 개념을 수용하는 것이다. 즉 특정 서비스를 변경한 다음에 이와 무관한 다른 어떤 서비스도 변경하지 않고서도 해당 변경사항만 운영 환경에 배포하기를 원하기 때문에 시스템 안정성은 중요하다. 이렇게 작동하려면 우리가 의존하는 서비스들이 안정성을 갖춰야 하며, 우리에게 의존하는 서비스들에도 안정적인 계약을 제공해야 한다.


응집력

응집력을 설명하는 가장 간결한 정의 중 하나는 "함께 바뀌고 함께 머무는 코드"다. 우리의 목적에 맞춰보면, 상당히 좋은 정의다. 우리는 비즈니스 기능을 쉽게 변경할 수 있도록 마이크로서비스 아케틱처를 최적하하려 한다. 따라서 가능한 한 적은 장소에서 변경할 수 있는 방식으로 기능을 그룹으로 묶어야 한다.


결합도

대체로 우리는 응집력을 선호하지만 결합도는 경계한다. '결합도가 높은' 항목이 많을수록 함께 변경해야 하는 내용도 더 많아진다. 그러나 결합도의 유형은 다양하며 각 유형마다 다른 해법이 필요할 수 있다. 결합도의 유형마다의 해법을 알아보겠다.

구현 결합도

구현 결합도는 가장 위험한 형태의 결합도지만, 다행히도 가장 쉽게 결합도를 낮출 수 있다. 구현 결합도에서는 B 구현 방식에 따라 AB에 결합되며, B 구현이 변경될 때에 A 또한 변경된다.
문제를 해결하는 방법은 여러 가지다. 그 중 하나를 선택하면 된다. 해결 방법을 바꾸려 할 때 이것이 컨슈머를 망가뜨리지 않도록 해야 한다.
구현 결합도의 고전적이고 일반적인 예는 데이터베이스를 공유하는 형태로 나타난다. 뮤직 사를 예로 설명하겠다. 주문 서비스는 시스템에 배치된 모든 주문의 음반을 포함한다. 추천 서비스는 고객에게 이전 구매를 기반으로 그들이 구매할 가능성이 높은 음반을 제안한다. 여기서, 추천 서비스는 데이터베이스에서 이 데이터에 직접 접근한다.
추천에는 고객이 넣은 주문 정보가 필요하다. 어느 정도까지는 피할수 없는 도메인 결합도다. 그러나 이런 성황에서는 구체적인 스키마 구조, SQL 언어, 심지어 행의 내용까지 우리의 구현 내용과 결합된다. 주문 서비스가 열 이름을 변경하거나 고객 주문 테이블을 분리한다면, 개념적응로 여전히 주문 정보를 포함하지만 추천 서비스가 이 정보를 가져오는 구현 방식을 망가뜨린다. 이런 구현 세부사항을 감추는 것이 더 바람직한 방법이며, 방법은 다음 2가지가 있다.

  • 추천 서비스는 API 호출을 통해 필요한 정보에 접근하게 한다
  • 주문 서비스가 데이터베이스 형태로 데이터 집합을 게시하도록 만든다. 이는 컨슈머가 대량으로 접근하기 위해 사용됨을 의미한다. 주문 서비스가 적절히 데이터를 게시할 수 있는 한, 공개 계약을 유지하므로 주문 서비스 내에서 변경된 사항은 컨슈머에게 보이지 않는다. 또한 컨슈머에게 공개된 데이터 모델을 개선해 필요에 따라 조율할수 있는 기회를 열어 놓는다.

이 2가지 방법은 정보 은닉을 사용하고 있다. 잘 정의된 서비스 인터페이스 뒤에 데이터베이스를 숨기면 서비스가 노출 대상의 범위를 제한하고 데이터 표현 방식을 변경할 수 있다.
서비스 인터페이스를 정의할 때는, '외부에서 내부로'라는 사고 방식도 유용하다. 먼저 서비스 컨슈머의 입장에서 생각해 서비스 인터페이스를 설계한 다음, 해당 서비스 계약을 구현한다.

시간적 결합도

시간적 결합도는는 주로 분산 환경에서 동기식 호출의 주요 도전과제 중 하나인 실행 시간에 발생하는 문제다. 메시지가 전송되는 시점과 메시지가 처리되는 방식이 시간과 관련 되어 있는 경우, 시간적 결합도가 존재한다고 말한다. 아래의 예를 들어 설명하겠다.

         1. 주문 정보 획득             2. 고객 정보 획득 
[창고] -------------------> [주문] --------------------> [고객]
         동기식 HTTP 호출              동기식 HTTP 호출

여기서 주문에 대해 필요한 정보를 가져오기 위해 창고 서비스에서 주문 서비스로 향하는 동기식 HTTP 호출을 볼 수 있다. 요청을 충족하기 위해 주문 서비스는 계속해서 동기식 HTTP 호출을 통해 고객 서비스에서 정보를 가져와야 한다. 이와 같은 전체 연산을 완료하려면 창고, 주문, 고객 서비스가 모두 작동하고 네트워크로 연결돼야 한다. 3가지 서비스는 모두 시간적으로 결합되어 있다.
이 문제는 다양한 방법으로 해소할 수 있다. 먼저 캐싱 사용을 고려해보면 된다. 주문 서비스가 고객 서비스에서 필요한 정보를 캐시하면 주문 서비스는 몇몇 경우에 다운스트림 서비스와 시간적 결합도를 피할 수 있다. 또한 요청을 보내기 위해 메시지 브로커 같은 서비스를 사용하는 비동기 전송을 고려할 수도 있다. 메시지 브로커가 메시지를 다운스트림 서비스로 보내 놓으면 다운스트림 서비스가 여유가 생긴 시점에 해당 메시지를 처리한다.

배포 결합도

정적으로 링크된 여러 모듈로 구성된 단일 프로세스가 있다고 해본다. 모듈 중 한 곳에서 코드 한 줄이 변경되었으며 해당 변경사항을 배포해야 한다. 이렇게 하려면, 심지어 변경되지 않은 모듈을 포함해 전체 모놀리스를 배포해야 한다. 모든 것이 반드시 함께 배포되어야 하므로 배포 결합도가 존재한다.
정적으로 링크된 프레스스의 예와 같이 배포 결합도가 강제될지도 모르지만, 릴리스 기차와 같이 관행에 따른 선택의 문제가 될 수 있다.
배포에는 위험이 따른다. 배포와 관련된 위험을 줄이는 방법은 여러 가지가 있는데, 그중 하나는 변경할 필요가 있는 사항만 바꾸는 것이다. 더 큰 프로세스를 독립적으로 배포 가능한 마이크로서비스로 분해에 배포 결합도를 줄일 수 있다면, 배포 범위를 줄임으로써 개별 배포의 위험을 낮출 수도 있다.
릴리스 규모가 작을수록 위험 부답도 줄어든다. 잘못될 것이 적기 때문이다. 변경을 줄였기 때문에 뭔가 잘못되었어도 문제를 찾아내 해결하기가 쉬워진다. 릴리스 규모를 줄이는 방법을 찾는 것은 지속적인 배포의 핵심이며, 빠른 피드백과 맞춤식 릴리스 방법의 중요성을 뒷받침한다. 릴리스 범위가 작을수록 출시도 쉽고, 안전하며, 빠른 피드백을 얻을 수 있다.
당연한 이야기지만, 배포 결합도를 줄이는 데는 반드시 마이크로서비스가 필요하지 않다. 얼랭(Erlang) 같은 런타임은 새로운 버전의 모듈을 실행 중인 프로세스로 재시작 없이 배포하게 만들어준다.

도메인 결합도

기본적으로, 여러 독립적인 서비스로 구성된 시스템은 서비스 간에 상호작용이 있어야만 동작한다. 마이크로서비스 아키텍처에서 도메인 결합도는 결과며, 서비스 간의 상호작용은 실제 도메인에서 일어나는 상호작용을 모델링한다. 주문하려면 고객의 쇼핑 바구니에 어떤 물품이 있는지 알아야 한다. 제품 배송을 원하면 어디로 배송할지 알아야 한다. 마이크로서비스 아키텍처에서, 분명히 이 정보들은 서로 다른 서비스에 포함될 것이다.
뮤직 사의 구체적인 예를 들어본다. 물품을 보관하는 창고가 있다. 고객이 CD를 주문하면, 창고에서 일하는 작업자들은 어떤 물품을 선택하고 포장해야 할지, 물품을 어디로 보내야 할지를 알아야 한다. 따라서 주문에 대한 정보는 창고 작업자들과 공유해야 한다.
주문 처리 서비스는 주문의 모든 세부사항을 창고 서비스로 전송한 다음, 물품을 포장하도록 만든다고 해본다. 이와 같은 작업의 일부로서, 창고 서비스는 고객 ID를 사용해 별도로 분리된 고객 서비스에서 고객에 대한 정보를 가져오므로, 우리는 물품을 발송할 때, 고객에게 통지하는 방법을 알고 있다.
이 상황에서 우리는 전체 주문을 창고와 공유하고 있는데, 합리적이지 않을 수 있다. 창고는 포장할 대상 물품과 배송지에 대한 정보만 필요하기 때문이다. 물품 가격이 얼마인지 알 필요가 없다. 또한 매우 광법위한 공유를 위해 접근 제어가 필요한 정보에 문제가 생길 수도 있다. 만일 전체 주문을 공유할 경우, 전혀 무관한 서비스에 신용카드 세부정보를 노출할 수도 있다.

이보다는 '창고' 서비스가 요구하는 정보만 포함한 '물품 선택 명령'이라는 새로운 도메인 개념을 생각할 수도 있다. 이는 정보 은닉의 또 다른 예다.
필요할 경우, 창고 서비스가 고객에 대해 알아야할 필요성조차 없애는 방식으로, 결합도를 더욱 줄일 수 있다. '물품 선택 명령'에 고객 정보를 포함한 모든 적절한 세부정보를 제공하면 된다.
이런 접근 방식이 작동하려면 특정 시점에서 주문 처리가 고객 서비스에 접근해 '물품 선택 명령'을 먼저 생성해야 하지만, 주문처리는 여러 가지 이유 때문에 고객 정보에 접근할 필요가 있으므로 이는 큰 문제가 되지 않는다. 이와 같이 '물품 선택 명령'을 '보내는' 과정은 주문 처리에서 창고 서비스 API 호출이 수행된다는 것이다.

대안으로 주문처리에서 창고가 소비하도록 이벤트를 발생시키는 방식도 가능하다. 창고가 소비하는 이벤트를 발생시키는 방시긍로, 종속성을 효과적으로 뒤집을 수 있다. 우리는 주문 전송 과정을 보증하기 위해, 주문 처리 서비스로부터 창고 서비스로 초점을 이동한다.
2가지 접근법 모두 고유한 장점이 있으며, 상황에 맞게 잘 선택하면 된다. 기본적으로 창고 서비스가 작업을 수행하려면 주문에 대한 몇 가지 정보가 필요하다. 이런 수준의 도메인 결합도를 피할 수는 없다. 그러나 우리가 공유하려는 개념이 무엇인지, 또 그 개념을 어떻게 공유해야 하는지를 신중하게 생각한다면 결합도수준을 줄이려는 목표를 달성할 수 있다.

profile
백엔드

0개의 댓글