040. (Clean Architecture) 16. 독립성

16. 독립성

좋은 아키텍처는 다음을 지원해야 한다.

  • 시스템의 유스케이스
  • 시스템의 운영
  • 시스템의 개발
  • 시스템의 배포

16.1. 유스케이스

첫 번재 주요 항목인 유스케이스의 경우, “시스템의 아키텍처는 시스템의 의도를 지원해야한다”는 뜻이다.

아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에서도 유스케이스가 최우선이다.

아키텍처는 반드시 유스케이스를 지원해야 한다.

하지만 아키텍처는 시스템의 행위에 그다지 큰 영향을 주지 않으며, 행위와 관련하여 아키텍처가 열어 둘 수 있는 선택사항은 거의 없다.

하지만 영향력이 전부는 아니다.

좋은 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은 행위를 명확히 하고 외부로 드러내며,

이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것이다.

이러한 행위들은 일급 요소(first-class element) 이며 시스템의 최상위 수준에서 알아볼 수 있으므로, 개발자가 일일이 찾아다닐 필요가 없다.

또한 이들 요소는 클래스이거나 함수 또는 모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 이름을 가진다.

16.2. 운영

시스템의 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡는다.

시스템이 어떠한 요구사항을 처리해야한다면 아키텍처는 이 요구사항과 관련된 각 유스케이스에 걸맞는 처리량을 요구하게 되며,

이러한 운영 작업을 허용할 수 있는 형태로 반드시 아키텍처를 구조화해야 한다.

구조화된 아키텍처 형태를 지원한다는 말은 시스템에 따라 다양한 의미를 지닌다.

어떤 시스템에서는 시스템의 처리 요소를 일련의 작은 서비스들로 배열하여, 서로 다른 많은 서버에서 벙렬로 실행할 수 있게 만들어야함을 의미한다.

또 다른 시스템에서는 수많은 경량 스레드가 단일 프로세서에서 같은 주소 공간을 공유하도록 만든다는 뜻일 수도 있다.

여기서도 중요한 것은 유연함이다.

아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고, 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는다면,

시간이 지나 운영에 필요한 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 어렵지 않을 것이다.

16.3. 개발

아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행한다.

콘웨이의 법칙(Conway’s law) 이 작용하는 지점이 바로 여기다.

많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보해야한다.

이를 통해 개발하는 통안 팀들이 다른 팀들을 서로 방해하지않도록 해야한다.

이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다.

그래야만 이들 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있다.

콘웨이의 법칙은 아래와 같은 뜻이다.
소프트웨어 구조는 해당 소프트웨어를 개발한 조직의 커뮤니케이션 구조를 닮게 된다.

반대로 역 콘웨이 전략(Reverse Conway Maneuver) 라는 것도 있다.
조직의 구조를 아키텍처에 고스란히 반영되도록 설계하여 개발팀과 서비스를 느슨하게 결합시킨다.

16.4. 배포

아키텍처는 배포 용이성을 결정하는 데 중요한 역할을 한다.

이때의 목표는 즉각적인 배포(immediate deployment) 이다.

좋은 아키텍처를 수십 개의 작은 설정 스크립트나 속성 파일을 약간씩 수정하는 방식을 사용하지 않는다.

또한 꼭 필요한 디렉터리나 파일을 수작업으로 생성되게 내버려 두지 않는다.

좋은 아키텍처라면 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 한다.

다시 말하지만, 이러한 아키텍처를 만들려면 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 한다.

여기엔 마스터 컴포넌트(메인 컴포넌트라고도 한다)도 포함되는데, 마스터 컴포넌트는 시스템 전체를 하나로 묶고,

각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 한다.

16.5. 선택사항 열어놓기

좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킨다.

하지만 이 균형을 잡기란 매우 어렵다.

대부분의 경우 모든 유스케이스를 파악하기란 쉽지 않으며, 운여하는 데 따르는 제약사항, 팀 구조, 배포 요구사항도 알지 못하기 때문이다.

설사 위의 사항들을 알고 있다하더라도, 시스템이 생명주기의 단계를 하나씩 거쳐감에 따라 이 사항들도 동적으로 변한다는 문제점이 있다.

하지만 이런 변화 속에서도 변하지 않는 것들이 있다.

  • 몇몇 아키텍처 원칙은 구현 비용이 비교적 낮으며, 관심사들 사이에서 균형을 잡는 데 도움이 된다.
  • 몇몇 아키텍처 원칙은 균형을 맞추려는 목표점을 명확히 그릴 수 없는 경우에도 도움이 된다.
  • 몇몇 아키텍처 원칙은 시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 된다.

위의 항목들을 통해 선택사항을 가능한 한 많이, 가능한 한 오랫동안 열어둘 수 있게 해준다.

16.6. 계층 결합 분리

아키텍트는 필요한 모든 유스케이스를 지원할 수 있는 시스템 구조를 원하지만, 유스케이스 전부를 알지는 못한다.

그저 시스템의 기본적인 의도를 알고 있을 뿐이다.

따라서 아키텍트는 단일 책임 원칙과 공통 폐쇄 원칙을 적용하여, 그 의도의 맥락에 따라서 다른 이유로 분리되는 것들은 분리하고

동일한 이유로 변경되는 것들은 묶는다.

서로 다른 이유로 변경되는 것은 무엇일까?

확실한 건 사용자 인터페이스가 변경되는 이유는 업무 규칙과는 아무런 관련이 없다는 것이다.

만약 유스케이스가 두 가지 요소를 모두 포함하는 경우, 사용자 인터페이스와 업무 규칙을 분리해야 한다.

이를 통해 사용자 인터페이스와 업무 규칙을 서로 독립적으로 변경될 수 있게 유지할 수 있게 된다.

업뮤 규칙은 그 자체가 애플리케이션과 밀접한 관련이 있거나, 더 범용적인 경우일 수도 있다.

따라서 사용자 인터페이스와 업무 규칙은 각자 다른 속도로, 그리고 다른 이유로 변경되게 된다.

기술적인 세부사항은 어떨까?

기술적인 세부사항인 데이터베이스, 쿼리 언어, 스키마는 사용자 인터페이스와도 업무 규칙과도 아무런 관련이 없다.

따라서 아키텍트는 기술적인 세부사항은 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경할 수 있도록 해야한다.

결론적으로 시스템은 서로 결합되지않는 수평적인 계층으로 분리되어야 하며,

사용자 인터페이스, 애플리케이션에 특화된 업무 규칙, 애플리케이션과는 독립적인 업무 규칙, 데이터베이스 등의 계층으로 구성된다.

16.7. 유스케이스 결합 분리

서로 다른 이유로 변경되는 것에는 유스케이스도 있다.

유스케이스는 시스템을 분할하는 자연스러운 방법임과 동시에 시스템의 수평적인 계층을 가로지르는 수직적인 조각이기도 하다.

각 유스케이스는 사용자 인터페이스의 일부, 애플리케이션에 특화된 업무 규칙, 애플리케이션과는 독립적인 업무 규칙, 데이터베이스의 일부 기능을 차용한다.

따라서 계층 결합 분리를 통해 구조화된 시스템의 수평적 구조를 가로지르는 수직적인 유스베이스로 시스템을 분할할 수 있다.

이와 같이 결합을 분리하다보면 어떠한 패턴을 발견할 수 있다.

시스템에서 서로 다른 이유로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 된다.

또한 슈크에시느는 뒷받침하는 사용자 인터페이스와 데이터베이스를 서로 묶어서, 각 유스케이스가 사용자 인터페이스와 데이터베이스의 서로 다른 관점을 사용하게 되면

새로운 유스케이스의 추가가 기존 유스케이스에 영향을 주는 경우는 거의 없어지게 된다.

16.8. 결합 분리 모드

상술했듯 결합을 분리하면 운영 관점에서도 유의미한 결과를 얻게 된다.

유스케이스에서 서로 다른 관점이 분리되었따면, 높은 처리량을 보장해야하는 유스케이스와 낮은 처리량으로도 충분한 유스케이스는 이미 분리되어있을 가능성이 높다.

사용자 인터페이스와 데이터베이스가 업무 규칙과 분리되어있다면, 사용자 인터페이스와 데이터베이스는 업무 규칙과는 다른 서버에서도 실행이 가능해진다.

간단히 말해 유스케이스를 위해 수행하는 결합 분리는 운영에 도움이 된다.

다만 운영 측면에서 이점을 살리기 위해서 결합을 분리할때 적절한 모드를 선택해야 한다.

예를 들어 분리된 컴포넌트를 서로 다른 서버에서 실행해야 하는 상황이라면, 이들 컴포넌트가 단일 프로세서의 동일한 주소 공간에 함께 상주하는 형태로 만들어져서는 안된다.

분리된 컴포넌트는 반드시 독립된 서비스가 되어야 하고, 일종의 네트워크를 통해 서로 통신해야 한다.

많은 아키텍트가 위와 같은 컴포넌트를 서비스 혹은 마이크로서비스라고 부르지만, 구분 기준이 다소 모호한 부분이 있다.

실제로 서비스에 기반한 아키텍처를 흔히 서비스 지향 아키텍처(service-oriented architecture) 라고 부른다.

핵심은 컴포넌트를 서비스 수준까지도 분리해야할 수 있어야한다는 것이며, 동시에 아키텍처는 선택권을 열어둔다는 점을 기억해야 한다.

결합 분리 모드에 대해 좀 더 깊이 들어가기 전에 개발과 배포에 대해서 먼저 살펴보도록 하자.

16.9. 개발 독립성

좋은 아키텍처는 시스템의 개발을 지원해야 한다.

컴포넌트가 완전히 분리되면 팀 사이에 간섭은 줄어들게 된다.

예를 들어 업무 규칙이 사용자 인터페이스를 모른다면, 사용자 인터페이스에 중점을 둔 팀은 업무 규칙에 중점을 둔 팀에 그다지 영향을 줄 수 없을 것이다.

기능 팀, 컴포넌트 팀, 계층 팀 혹은 다른 형태의 팀이라도 계층과 유스케이스의 결합이 분리되는 한 시스템의 아키텍처는 그 팀의 구조를 뒷받침해줄 것이다.

16.10. 배포 독립성

유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 확보된다.

실제로 결합이 제대로 분리된 시스템은 운영중일지라도 계층과 유스케이스를 교체할 수 있게 된다.

참고 이러한 형태를 핫스왑(hot swap) 이라고 한다.

새로운 유스케이스를 추가하는 일은 시스템의 나머지는 그대로 둔채 새로운 파일이나 서비스 몇개를 추가하는 정도로 단순한 일이 된다.

16.11. 중복

아키텍처는 때때로 중복에 대한 공포를 느끼게된다.

일반적으로 소프트웨어에서 중복은 나쁜 것이 맞으며 개발자들 또한 중복된 코드를 좋아하지 않는다.

하지만 중복에도 여러 종류가 존재하며, 그 중 하는 진짜 중복이다.

진짜 중복에서는 한 인스턴스의 변경이, 그 인스턴스의 복사본 전체에 동일한 변경을 적용하게 된다.

또 다른 종유로를 거짓된 중복이다.

중복으로 보이는 두 코드 영역이 서로 다른 속도와 서로 다른 이유로 인해 각자의 경로로 발전한다면 이 두 코드는 중복이 아니다.

예를 들어 두 유스케이스의 화면 구조가 매우 비슷하다고 가정해보자.

두 유스케이스는 마치 중복처럼 느껴지기때문에 통합하고 싶은 느낌이 든다.

하지만 시간이 지나면서 두 화면은 서로 다른 방향으로 분기하게 되어, 우발적 중복임을 뒤늦게 알아챌 수 있다.

이러면 비용을 들여 통합한 두 유스케이스를, 또 분리하기 위해 비용을 투입해야만 한다.

유스케이스를 수직으로 분리할 때마다 중복 문제는 계속 마주치게 되지만, 무의식적으로 중복을 제거해버리는 잘못을 경계해야 한다.

가장 먼저 해야할 것은 중복이 진짜 중복인지 확인하는 것이다.

16.12. 결합 분리 모드(다시)

다시 결합 분리 모드로 돌아와봐자.

계층과 유스케이스의 결합을 분리하는 방법은 다양하다.

소스 코드 수준에서 분리할 수도 있으며, 바이너리 코드 수준과 서비스 수준에서도 분리할 수 있다.

각 수준별 분리 모드는 아래와 같이 정리할 수 있다.

1. 소스 수준 분리 모드

소스 코드 모듈 사이의 의존성을 제어할 수 있다.

이를 통해 하나의 모듈이 변경되더라도 다른 모듈을 변경하거나 재컴파일하지않도록 만들 수 있다.

이 모드에서는 모든 컴포넌트가 같은 주소 공간에서 실행되고, 서로 통신할때는 간단한 함수 호출을 수행하며, 컴퓨터 메모리에는 하나의 실행 파일만이 로드된다.

이런 구조를 모노티릭 구조라고 한다.

예시로는 루비의 Gem을 들 수 있다.

2. 배포 수준 분리 모드

배포 가능한 단위에서의 의존성을 제어할 수 있다.

한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지않도록 만들 수 있다.

많은 컴포넌트가 여전히 같은 주소 공간에 상주하며, 단순한 함수 호출을 통해 통신할 수 있다.

어떤 컴포넌트는 동일한 프로세서의 다른 프로세스에 상주하고, 프로세스간 통신, 소켓 또는 공유 메모리를 통해 통신한다.

배포 수준 분리 모드의 중요한 특정인 결합이 부리된 컴포넌트가 독립적으로 배포할 수 있는 단위로 분할되어있다는 점이다.

예시로는 jar 파일, Gem파일, DLL, 공유 라이브러리 등을 들 수 있다.

3. 서비스 수준 분리 모드

의존하는 수준을 데이터 구조 단위까지 낮추고, 네트워크 패킷을 통해서만 통신하도록 만들 수 있다.

이를 통해 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적이게 된다.


어떤 모드가 사용성이 좋을까?

아쉽게도 프로젝트 초기 단계에서는 어떤 모드가 최선인지 알기 어려우며, 프로젝트가 성숙해갈수록 최적인 모드가 달라질 수 있다.

당장에는 단일 서버로도 시슽메을 안정적으로 실행할 수 있겠지만, 시스템이 성장하면서 별도 서버에서 실행해야하는 컴포넌트가 생기는 것은 충분이 예측 가능한 일인 것처럼 말이다.

다행히 초기에는 시스템이 하나의 서버에서 실행되는 동안 소스 수준에서 결합을 분리하는 것으로도 충분하다.

이후 프로젝트 성숙도에 따라 배포 가능한 단위를 넘어 서비스 수준까지 분리해나가는 것이다.

한 가지 해결책은 단순하게 서비스 수준에서의 분리를 기본 정책으로 삼는 것이지만, 비용이 많이 들고 결합이 큰 단위(coarse-grained)에서 분리된다는 문제가 있다.

또한 마이크로서비스가 아무리 작다하더라도 충분히 작은 단위에서 분리될 가능성은 거의 없다.

서비스 수준의 결합 분리가 지닌 또 다른 문제점은 개발 리소스뿐만 아니라 시스템 리소스 측면에서도 많은 비용이 든다는 점이다.

필요하지않은 서비스 경계를 처리하는 데 드는 작업은 모두 낭비에 가깝다.

이처럼 컴포넌트가 서비스화될 가능성이 있다면 컴포넌트 결합을 분리하되 서비스가 되기 직전에 멈추는 방식을 권장한다.

그리고 컴포넌트들을 가능한 한 오랫동안 동일한 주소 공간에 남겨두어 서비스에 대한 선택권을 열어둘 수 있다.

이 방식을 사용하면 초기에는 컴포넌트가 소스 코드 수준에서 분리되며, 배포나 개발에서 문제가 발생하면 일부 결합을 배포 수준까지 분리해서 대응할 수 있다.

개발, 배포, 운영 측면의 문제가 증가하게되면 서비스 수준으로 전환할 배포 단위를 신중하게 선택하여 서비스화하는 방향으로 시스템을 변경해나간다.

시간이 흘러 시스템의 운영 요구사항이 감소하게 되면, 서비스 수준까지 결합 분리했던 것들이 배포 수준, 소스 수준의 결합 분리로 서서히 내려온다.

좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고

독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야한다.

또한 좋은 아키텍처라면 나중에 상황이 바뀌었을 때 진행 방향을 돌려 모노리틱 구조로 되돌릴 수도 있어야 한다.