This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @fabiopitino 2023-05-22

Hexagonal Rails Monolith

요약

TL;DR: Rails 모놀리스를 big ball of mud 상태에서 모듈식 모놀리스로 변환하여 육각형 아키텍처(또는 포트 및 어댑터 아키텍처)를 사용합니다. 일관된 기능 도메인을 도메인 주도 설계(Domain-Driven Design) 관행을 사용하여 별도의 디렉터리 구조로 추출합니다. 로깅, 데이터베이스 도구, 계측 등과 같은 인프라 코드를 젬으로 추출하여 본질적으로 lib/ 디렉터리가 필요하지 않도록 합니다. (접합부) 애플리케이션 서비스와 같은 기능적 도메인의 어떤 부분이 통합을 위해 공개 사용되고(포트) 어떤 부분이 비공개되어 캡슐화된 세부 정보인지 정의합니다. 외부 레이어의 어댑터로 Web, Sidekiq, REST, GraphQL 및 Action Cable을 정의합니다. 단일 몰리스 내의 모듈 간의 개인 정보 및 의존성을 강제하는 데 Packwerk를 사용합니다.

GitLab 모놀리스를 위한 육각형 아키텍처

상세내역

flowchart TD u([사용자]) -- 직접 상호 작용 --> AA[애플리케이션 어댑터: WebUI, REST, GraphQL, git, ...] AA --에서 추상화를 사용함 --> D[애플리케이션 도메인] AA --에 따라 --> 플랫폼 D --에 따라 --> 플랫폼[플랫폼: 젬, 설정, 프레임워크, ...]

애플리케이션 도메인

애플리케이션 코어(기능적 도메인)는 GitLab 제품에 고유한 비즈니스 로직, 정책 및 데이터를 설명하는 모든 코드로 구성됩니다. 이는 별도의 최상위 bounded contexts로 구분됩니다. 바운더리 컨텍스트는 루비 모듈 형태로 표현됩니다. 이는 기존 네임스페이스 명명 가이드라인을 따르지만 보다 구조적으로 합니다.

모듈은 다음과 같아야 합니다:

  • 내부 로직, 상태 및 데이터를 캡슐화할 수 있을 만큼 충분히 깊어야 합니다.
  • 가능한 사용자가 안전하게 사용할 수 있는 공개 인터페이스여야 하며 문서화가 잘 되어 있어야 합니다.
  • 응집성이 있어야 하며 기능을 설명하는 기능의 SSoT(single source of truth)을 대표해야 합니다.

기능 카테고리는 모듈이 깊게 들어갈만큼 큰 제품 영역을 나타냅니다. 이를 통해 작은 최상위 모듈이 폭증하지 않도록 합니다. 또한 코드베이스가 보편적 언어를 따를 수 있도록 돕습니다. 하나의 팀은 여러 기능 카테고리에 대한 책임을 지고 있으며, 따라서 여러 bounded contexts에 대한 비전을 소유하게 됩니다. 때로는 기능 카테고리의 소유권이 변경될 수 있지만, 새로운 bounded contexts를 새로운 소유자에게 매핑하는 변경이 매우 쉬워집니다. 또한, 기능 카테고리를 사용하는 것은 GitLab 팀 구성원 또는보다 넓은 커뮤니티의 구성원으로서 코드베이스를 네비게이트하는 데 도움이 됩니다.

여러 기능 카테고리가 강하게 관련되어 있다면 단일 bounded context에 그룹화될 수 있습니다. 기능 카테고리가 부모 기능 카테고리의 맥락에서만 관련이 있는 경우 부모의 bounded context에 포함될 수 있습니다. 예를 들어, 빌드 아티팩트는 CI 기능 카테고리의 맥락에 존재하며 단일 bounded context에 Merge될 수 있습니다.

애플리케이션 도메인은 외부 레이어(예: 애플리케이션 어댑터)의 지식이 없으며 플랫폼 코드에만 의존합니다. 이를 통해 도메인 코드를 비즈니스 로직의 SSoT으로 만들어 재사용 가능하고 WebUI 또는 REST API로부터 요청이 왔는지 여부에 관계없이 테스트할 수 있습니다.

외부 레이어와 내부 레이어 사이의 의존성이 필요한 경우(도메인 코드가 어댑터의 인터페이스에 의존성을 가짐) 이를 주입하는 의존성 제어 기법을 사용하여 해결할 수 있습니다.

애플리케이션 어댑터

어댑터는 컴포넌트와 외부 세계 간의 접착제입니다. 외부 응용 프로그램 컴포넌트의 요구 사항을 나타내는 포트와 외부 세계 사이의 교환을 맞춤화합니다. 예를 들어 사용자가 GUI, 명령줄 인터페이스, 자동화된 데이터 원본 또는 테스트 스크립트를 통해 데이터를 제공할 수 있습니다. - Wikipedia

애플리케이션 어댑터는 다음과 같을 것입니다:

  • Web UI(Rails 컨트롤러, 뷰, JS 및 Vue 클라이언트)
  • REST API 엔드포인트
  • GraphQL 엔드포인트

이들은 사용자와의 상호 작용에 대한 책임을 집니다. 각 어댑터는 요청을 해석하고 매개변수를 구문 분석하며 애플리케이션 도메인에서 올바른 추상화를 호출한 후 결과를 사용자에게 제시해야 합니다.

표현 논리 및 인증은 어댑터 레이어에 특정합니다.

애플리케이션 어댑터 레이어는 실행할 수(플랫폼 코드를) 플랫폼 코드에 의존합니다: Rails 프레임워크, 어댑터를 지원하는 젬, 구성 및 유틸리티.

플랫폼 코드

플랫폼 코드의 경우 애플리케이션 도메인 또는 애플리케이션 어댑터가 작동하는 데 필요한 모든 클래스 및 모듈을 고려합니다.

오늘날 Rails의 lib/ 디렉터리에는 다른 곳에 있을 수 있는 여러 유형의 코드가 포함되어 있습니다. 대부분은 플랫폼 코드입니다:

  • REST API 엔드포인트는 애플리케이션 어댑터의 일부가 될 수 있습니다.
  • 도메인 코드(큰 도메인 코드 및 Gitlab::JiraImport와 같은 작은 도메인 코드)는 애플리케이션 도메인 내에 있어야 합니다.
  • 나머지는 모놀리스 내의 gems/ 디렉터리에 별도의 단일용도 젬으로 추출될 수 있습니다. 이는 Gitlab::ApplicationRateLimiter, Gitlab::Redis, Gitlab::Database와 같은 유틸리티뿐만 아니라 로깅, 오류 보고 및 메트릭, 비율 제한기, Banzai와 같은 일반 하위도메인을 포함할 수 있습니다.

ApplicationRecord 또는 ApplicationWorker와 같은 Rails 프레임워크를 확장하기 위한 기본 클래스 및 BaseService와 같은 GitLab 기본 클래스는 젬 확장으로 구현할 수 있습니다.

이는 Rails 프레임워크 코드 이외의 모든 코드가 gems/ 내에 존재한다는 것을 의미하며, 이는 해당 목적이 애플리케이션 코드를 제공하는 데 잘 드러나 있음을 보여줍니다.

최종적으로 gems/ 내의 모든 코드는 잠재적으로 별도의 리포지터리에 추출되거나 오픈소스로 공개될 수 있습니다. 플랫폼 코드를 gems/ 내에 배치하면 해당 목적이 애플리케이션 코드를 제공하는 데 있다는 것을 분명히 보여줍니다.

경계 강제

루비에는 주어진 모듈의 상수의 비공개 개념이 없습니다. 다른 프로그래밍 언어와 달리 흔히 쓰이는 상수가 모두 루비에서 공개되어 있습니다.

육각형 아키텍처로 이상적으로 구성된 코드베이스를 가지고 있더라도, 코드의 가장 큰 부분인 애플리케이션 도메인이 비모듈화된 big ball of mud가 될 수 있습니다.

경계를 강제하는 것은 구조를 장기간 유지하는 데 중요합니다. 큰 모듈화 노력 후에 우리는 천천히 다시 큰 ball of mulkd로 빠지는 것을 원하지 않습니다.

우리는 Packwerk를 사용하여 모듈 경계를 강제하는 방법을 탐구했습니다.

Packwerk는 점진적으로 코드베이스에 패키지를 도입하고 개인 정보 및 명시적 의존성을 실시하는 정적 분석기입니다. Packwerk는 Ruby 코드가 다른 패키지의 개인 구현 세부 정보를 사용하고 있는지 또는 명시적으로 선언되지 않은 패키지를 사용하고 있는지를 감지할 수 있습니다.

정적 분석기이므로 코드 실행에 영향을 주지 않으므로 Packwerk를 도입하는 것은 안전하며 점진적으로 할 수 있습니다.

Gusto와 같은 회사들은 Packwerk를 중심으로 Rails 모듈식 모놀리스로 이동하고자 하는 기관을 위한 개발 및 엔지니어링 도구디렉터리을 개발 및 유지 관리해 왔습니다.

EE 및 JH 확장

GitLab 코드베이스의 모듈화하는 독특한 도전 중 하나는 EE 확장(EE extensions)과 JH 확장(JiHu에서 관리함)의 존재입니다.

관련 도메인 코드(예: Ci::)를 동일한 bounded context 및 Packwerk 패키지에 이동함으로써, EE 확장도 이에 따라 이동해야 합니다.

상위 bounded contexts 역시 Packwerk 패키지와 일치하도록 구성되어야 하기 때문에, 특정 도메인과 관련된 모든 코드는 동일한 패키지 디렉터리 아래에 배치되어야 합니다. 이는 EE 확장을 포함합니다.

다음은 가능한 디렉터리 구조의 예시입니다:

domains
├── ci
│   ├── package.yml       # 패키지 정의.
│   ├── packwerk.yml      # 이 패키지에 대한 도구 구성.
│   ├── package_todo.yml  # 기존 위반이 있던 패키지.
│   ├── core              # 커뮤니티 에디션에서 항상 자동으로 로드되는 핵심 기능.
│   │   ├── app
│   │   │   ├── models/...
│   │   │   ├── services/...
│   │   │   └── lib/...   # 다른 클래스와 함께 도메인별 `lib`가 안에 이동됨.
│   │   └── spec
│   │       └── models/...
│   ├── ee                # 해당 bounded context에 특화된 EE 확장, 조건부 자동 로드.
│   │   ├── models/...
│   │   └── spec
│   │       └── models/...
│   └── public            # 다른 패키지에서 참조될 수 있도록 공개 상수가 여기에 놓임.
│       ├── core
│       │   ├── app
│       │   │   └── models/...
│       │   └── spec
│       │       └── models/...
│       └── ee
│           ├── app
│           │   └── models/...
│           └── spec
│               └── models/...
├── merge_requests/
├── repositories/
└── ...

도전과제

  • 이러한 변경 사항은 모듈화된 아키텍처의 이점을 이해하고 레거시 관행으로 빠지지 않는 개발 사고 방식의 전환을 요구합니다.
  • 애플리케이션 아키텍처를 변경하는 것은 어려운 작업입니다. 시간, 자원, 그리고 엔지니어로부터의 동의가 필요하지만 무엇보다도 중요한 것은 엔지니어로부터의 참여입니다.
  • 이는 중장기적인 엔지니어 팀이나 아키텍처 진화 계획을 수립하고, 다양한 엔지니어링 채널에서 토의를 유도하고 채택에 따른 도전을 해결하는 작업 그룹이 필요할 수 있습니다.
  • 우리는 표준 및 지침을 수립하고, 고립된 영역을 만들지 않도록 해야 합니다.
  • 새로운 코드가 위치할 지침을 명확히하고, lib/과 같은 쓰레기 드로어(Drawer) 폴더를 재생성하지 않아야 합니다.

기회

모듈식 모놀리식 아키텍처로의 전환은 앞으로 탐험할 많은 기회를 얻을 수 있습니다:

  • 모듈식 시스템에서 도메인 전문가 개념을 구체적으로 지니게 할 수 있습니다.
  • 정적 분석 도구 (예: Packwerk, RuboCop 등)를 사용하여 개발 및 CI에서 설계 위반 사항을 잡을 수 있으며, 최상의 관행이 준수되도록 할 수 있습니다.
  • 모듈 간의 의존성을 명시적으로 정의함으로써 변경된 부분만을 테스트함으로써 CI를 가속화할 수 있습니다.
  • 필요한 경우, 이러한 모듈식 아키텍처는 모듈을 별도의 서비스로 분해하는 데 도움이 될 수 있습니다.