앞서 서술한 바와 같이 좋은 아키텍처는 다음을 지원해야 한다.

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

유스케이스

시스템의 아키텍처는 시스템의 의도를 지원해야 한다는 뜻이다.
만약 시스템이 장바구니 App이라면, 이 아키텍처는 장바구니와 관련된 유스케이스를 지원해야 한다.
실제로 아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에서도 유스케이스가 최우선이다.
아키텍처는 반드시 유스케이스를 지원해야 한다.

하지만 아키텍처는 시스템의 행위에 그다지 큰 영향을 주지 않는다.
행위와 관련하여 아키텍처가 열어 둘 수 있는 선택사항은 별로 없다.
하지만 영향력이 전부가 아니다. 아키텍ㅊ처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은

  1. 행위를 명확히 하고 외부로 드러내며
  2. 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것

장바구니 App이 좋은 아키텍처를 갖춘다면, 이 App은 장바구니 애플리케이션처럼 보일 것이다.
해당 시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러날 것이다.
이들 행위는 "일급 요소" 이며 시스템의 최상위 수준에서 알아볼 수 있으므로, 개발자가 일일이 찾아 헤매지 않아도 된다.
이들 요소는 클래스이거나 함수 또는 모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 이름을 갖는다
-> 이후에 다시 설명

운영

시스템의 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡는다.
IF) 초당 100,000명의 고객을 처리해야 한다면, 아키텍처는 이 요구와 관련된 각 유스케이스에 걸맞은 처리량과 응답시간을 보장해야 한다.
만약 시스템에서 수 밀리초 안에 3차원의 빅데이터 테이블에 질의해야 한다면, 이러한 운영 작업을 허용할 수 있는 아키텍처를 구조화해야 한다.

이러한 형태를 지원한다는 말은 시스템에 따라 다양한 의미를 지닌다.
어떤 시스템에서는 시스템의 처리 요소를 일련의 작은 서비스들로 배열하여, 서로 다른 많은 서버에서 병렬로 실행할 수 있게해야 한다.
다른 시스템은 경량의 수많은 스레드가 단일 프로세서에서 같은 주소 공간을 공유하도록 만들어야 한다.
또는 독립된 주소 공간에서 실행되는 소수의 프로세스만으로도 충분한 시스템도 있을 것이다.

이상하게 보일 수도 있지만, 이러한 결정은 ㄷ항상 열어 두어야 하는 선택사항 중 하나다.
-> 시스템/App에 어떤 선택을 해야할 지 모르기에?
IF) 시스템이 이미 모노리틱 구조를 갖는다면, 다중 프로세스/스레드/MSA가 필요해질 때 개선하기 어렵다.
그에 비해 아키텍처에서 각 컴포넌트를 적절히 격리하고 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는다면, 시간이 지나 운영에 필요한 요구사항이 바뀌더라도 쓰레드/프로세스/서비스로 구성된 기술 스펙트럼 사이를 전황하는 일이 쉬워질 것이다.

개발

아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행한다.
Conway 법칙이 작용하는 지점이 이곳이다.

시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.

많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면,
각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 개발하는 동안 팀끼리 서로를 방해하지 않아야 한다.
이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다.
그래야만 이들 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있다.

선택사항 열어놓기

좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킨다.
말은 쉽다.
현실에서는 이러한 균형을 잡기가 매우 어렵다.
대부분의 경우 모든 유스케이스를 알 수는 없으며, 운영하는 데 따르는 제약사항, 팀 구조 등을 알지 못한다.
이러한 사항들을 알더라도 시스템이 생명주기의 단계를 거쳐감에 따라 이 사항들도 변할 것이다.
우리가 도달하려는 목표는 시시각각으로 변한다

그러나 사라지지 않는 것도 있다.
몇몇 아키텍처 원칙은 구현하는 비용이 비싸지 않으며, 관심사들 사이에서 균형을 잡는 데 도움이 된다.
심지어 균형을 맞추려는 목표점을 명확히 그릴 수 없는 경우에도 도움이 된다.
시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 되며, 이를 통해 선택사항을 가능한 많이, 가능한 오랫동안 열어야 한다.

계층 결합 분리

유스케이스 측면을 보자, 아키텍트는 필요한 모든 유스케이스를 지원할 수 있는 시스템 구조를 원하지만, 유스케이스 전부를 알지는 못한다.
하지만 아키텍터는 시스템의 기본적인 의도는 분명히 알고 있다.
-> 시스템이 장바구니인지 주문 처리 인지 안다는 뜻이다.
따라서 아키텍트는 단일 책임 원칙과 공통 폐쇄원칙을 적용하여,
그 의도의 맥락에 따라서 다른 이유로 변경되는 것들은 분리하고, 동일한 이유로 변경되는 것들은 묶는다.

서로 다른 이유로 변경되는 것은 무엇일까? 몇가지 분명한 것이 있다.

  • UI는 업무 규칙과는 아무런 관련이 없다.
  • 유스케이스가 두가지 요소를 모두 포함한다면
    만약 유스케이스가 두자기 요소를 모두 포함한다면, 뛰어난 아키텍트는 유스케이스에서 UI부분과 업무 규칙 부분을 서로 분리하고자 할 것이다.
    이렇게 함으로써 두 요소를 서로 독립적으로 변경할 수 있을 뿐만 아니라, 유스케이스는 여전히 가시적이며 분명하게 유지할 수 있다.
    (UI, 비즈니스, 도메인 로직을 구분하는 것?)

업무 규칙은 그 자체가 App과 밀접한 관련이 있거나, 혹은 더 범용적일 수도 있다.
EX)

  • 입력 필드 유효성 검사는 App 자체와 밀접하게 관련된 업무 규칙이다.
  • 반대로 계좌의 이자 계산이나 재고품 집계는 업무 도메인에 더 밀접하게 연관된 업무 규칙이다.
    이들 서로 다른 두 유형의 규칙은 각자 다른 속도로, 다른 이유로 변경될 것이다.
    따라서 이들 규칙은 서로 분리하고, 독립적으로 변경할 수 있도록 만들어야 한다.

DB, 쿼리 언어, 스키마조차도 기술적인 세부사항이며, 업무 규칙이나 UI와는 아무런 관련이 없다.
시스템의 다른 측면과는 다른 속도로, 다른 이유로 변경된다.
결론적으로 이들은 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경할 수 있도록 해야 한다.

이제 우리는 시스템을 서로 결합되지 않은 수평적인 계층으로 분리하는 방법을 알게 되었다.
UI, App에 특화된 업무, App과는 독립적인 업무 규칙, DB

유스케이스 결합 분리

서로 다른 이유로 변경되는 것에는 또 무엇이 있을까? -> 유스케이스
주문 입력 시스템에서 주문을 추가하는 유스케이스는 주문을 삭제하는 유스케이스와는 틀림없이 다른 이유로 변경된다.

이와 동시에 유스케이스는 시스템의 수평적인 계층을 가로지르도록 자른 조각이기도 하다.
각 유스케이스는 UI의 일부, App 특화 업무 규칙의 일부, App 독립적 업무 규칙의 일부, DB 기능 일부를 사용한다.
따라서 우리는 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는 얇은 수직적인 유스케이스로 시스템을 분할할 수 있다.

  • 이와 같이 결합을 분리하려면 주문 추가 유스케이스의 UI와 주문 삭제 유스케이스의 UI를 분리해야 한다.
  • 유스케이스의 업무 규칙과 DB 부분도 마찬가지다.
    이런 식으로 시스템의 맨 아래 계층까지 수직으로 내려가며 유스케이스들이 각 계층에서 서로 겹치지 않게 한다.

-> 여기서 패턴을 볼 수 있다.
시스템에서 서로 다른 이유로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 추가할 수 있다.
또한, 유스케이스를 뒷받침하는 UI와 DB를 묶어서 각 유스케이스가 UI와 DB의 서로 다른 관점을 사용하게 되면,
새로운 유스케이스를 추가하더라도 기존 유스케이스에 영향을 주지 않는다.

결합 분리 모드

이렇게 결합을 분리하면 두 번째 항목인 운영 관점에서 어떤 의미가 있는지 살펴보자.
유스케이스에서 서로 다른 관점이 분리되었다면, 높은 처리량을 보장해야 하는 유스케이스와
낮은 처리량으로도 충분한 유스케이스는 이미 분리되어 있을 가능성이 높다.
UI와 DB가 업무 규칙과 분리되어 있다면, UI와 DB는 업무 규칙과는 다른 서버에서 실행될 수 있다.

간단히 말해 유스케이스를 위해 수행하는 그 작업(결합 분리)들은 운영에도 도움이 된다.
하지만 운영 측면에서 이점을 살리기 위해선 결합을 분리할 때 적절한 모드를 선택해야 한다.
예를 들어 분리된 컴포넌트를 서로 다른 서버에서 실행해야 하는 상황이라면, 이들 컴포넌트가 단일 프로세서의 동일한 주소 공간에 함께 상주하면 안된다.
분리된 컴포넌트는 반드시 독립된 서비스가 되어야 하고, 네트워크를 통해 통신해야 한다.

이러한 컴포넌트를 '서비스' 또는 마이크로서비스라고 하는데 그 구분은 모호하다.
실제로 서비스에 기반한 아키텍처를 흔히들 서비스 지향 아키텍처(SOA)라고 부른다.

개발 독립성

세 번째 항목은 개발이었다.
컴포넌트가 완전히 분리되면 팀 사이의 간섭은 줄어든다.
업무 규칙이 UI를 알지 못하면 UI에 중점을 둔 팀은 업무 규칙에 중점을 둔 팀에 영향을 줄 수 없다.
유스케이스 자체도 서로 결합이 분리되면 ㅁddOrder 유스케이스에 중점을 둔 팀이 ㅇeleteOrder 유스케이스에 중점을 둔 팀에 개입하지 못한다.

배포 독립성

유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 생긴다.
실제로 결합을 제대로 분리했다면 운영 중인 시스템에서도 계층과 유스케이스를 교체할 수 있다.

중복

SW에서 중복은 일반적으로 나쁜 것이다.
우리는 중복된 코드를 줄이고 제거하기 위해 많은 노력을 한다.
하지만 중복에도 종류가 있다. 그 중 하나는 진짜 중복이다.
이 경우 한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야 한다.

또 다른 중복은 거짓된 중복이다.
중복으로 보이는 두 코드 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.

예를 들어 두 유스케이스의 화면 구조가 매우 비슷하다고 가정해보자.
아키텍트는 이 구조에 사용할 코드를 통합하고 싶은 유혹을 강하게 느낄 것이다.
하지만 이는 진짜 중복일까? 가짜 중복일까?
-> 거짓된 중복일 가능성이 높다.
시간이 지나면서 두 화면은 서로 다른 방향으로 분기하며, 결국에는 다른 모습을 가질 가능성이 높다.

유스케이스를 수직으로 분리할 때 이러한 문제와 마주칠테고, 이 유스케이스를 통합하고 싶다는 유혹을 받게 될 것이다.
-> 이 유스케이스들이 서로 비슷한 화면, 비슷한 알고리즘, 비슷한 DB 쿼리와 스키마를 갖고있기 때문이다.
하지만 이 중복이 진짜 중복인지 확인하라.

마지막으로 계층을 수평으로 분리하는 경우, 특정 DB 레코드의 데이터 구조가 특정 화면의 데이트 구조와 비슷하다는 점을 발견할 수 있다.
이때 DB 레코드와 동일한 형태의 View Model을 만들어서 각 항목을 복사하는 것이 아니라,
DB 레코드를 있는 그대로 UI까지 전달하고 싶다는 유혹을 받을 수 있다.
-> ViewModel을 만들어라.

결합 분리 모드(다시)

결합 분리 모드로 다시 돌아가자. 계층과 유스케이스의 결합을 분리하는 방법은 다양하다.

  1. 소스 코드 수준에서 분리
  2. 바이너리 코드에서 분리
  3. 실행 단위(서비스) 수준에서 분리
  • 소스 코드 수준에서 분리

    • 소스 코드 모듈 사이의 의존성을 제어할 수 있다.
    • 이를 통해 하나의 모듈이 변하더라도 다른 모듈을 변경하지 않아도 됨
    • 이 모드에서는 모든 컴포넌트가 같은 주소 공간에서 실행되고, 서로 통신할 때는 함수 호출을 사용한다.
    • 흔히 모노리틱 구조라 부른다.
  • 배포 수준 분리 모드

    • jar 파일, DLL 처럼 배포 가능한 단위들 사이의 의존성을 제어 가능
    • 모듈이 변해도 다른 모듈을 재빌드하지 않아도 됨
    • 많은 컴포넌트가 여전히 같은 주소 공간에 상주하며, 단순한 함수 호출을 통해 통신할 수 있다.
  • 서비스 수준 분리 모드

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

어떤 것이 좋은가?
-> 프로젝트 초기에는 어떤 것이 최선인지 알 수 없다.
-> 또한 프로젝트가 커져감에 따라 최적인 모드가 달라질 수 있다.

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

또한, 서비스 수준의 결합 분리는 개발 시간 뿐만 아니라 시스템 자원에서도 비용이 많이 든다.
필요치도 않은 서비스 경계를 처리하는 데 드는 작업은 노력, 메모리, 계산량 측면에서 모두 낭비다.

초기에는 소스 코드 분리 모드를 사용하다가 개발/배포/운영 문제가 증가하면 그때, 서비스 수준으로 전환할지 고려한다.

좋은 아키텍처는 시스템이 모노리틱으로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고,
또 독립적인 서비스나 마이크로 서비스 수준까지 성장해야 한다.
또한 반대로 마이크로서비스를 모노리틱 구조로 되돌릴 수도 있어야 한다.

결론

물론 이렇게 하기는 매우 어렵다.
그리고 결합 분리 모드를 변경하기가 설정값 하나 바꾸는 것처럼 쉽지 않다.
결국 시스템의 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며, 이러한 변경을 예측해서 변경에 무리가 없도록 만들어야 한다는 점이다.

+ Recent posts