사전 모듈러 모놀리스
패턴

모듈러 모놀리스

gabury1고친 사람 github-actions[bot]

모듈러 모놀리스는 한 번에 배포하는 애플리케이션 안에서 코드를 기능별 모듈로 엄격하게 나눠 두는 설계 결정입니다. 주문 모듈은 결제 모듈이 열어 둔 창구로만 결제 기능을 씁니다. 배포는 단순하게 둔 채 코드가 뒤엉키는 것만 막으려는 선택입니다. 경계가 또렷해서 나중에 모듈 하나를 따로 도는 서비스로 떼어 내기도 쉽습니다.

쉽고 빠른 이해

모듈러 모놀리스는 프로그램 하나 안을 주문·결제·배송 같은 기능별 모듈로 나눕니다. 모듈끼리는 정해 둔 창구로만 서로를 부릅니다. 주문 코드가 결제 테이블을 직접 읽는 일이 없습니다.

한 덩어리 프로그램은 시간이 지나면 코드가 얽혀서 한 곳을 고치면 엉뚱한 곳이 깨집니다. 그렇다고 서비스로 쪼개면 네트워크 호출과 돌볼 서버가 늘어납니다. 그 사이에서 배포는 하나로 두고 얽힘만 막습니다.

  1. 업무 기능마다 모듈을 하나씩 둡니다
  2. 모듈마다 바깥에 여는 창구를 정하고 나머지는 숨깁니다
  3. 모듈마다 자기 테이블만 직접 만집니다

대가는 배포가 여전히 하나로 묶인다는 것입니다. 한 모듈을 고쳐도 전체를 다시 올립니다. 경계를 지키는 규칙도 검사하지 않으면 금방 무너집니다.

상세

이 절은 모듈러 모놀리스가 무엇을 하나로 두고 무엇을 나누는지, 그 경계를 어떤 규칙과 장치로 지키는지를 다룹니다. 이어서 대가와 서비스로 떼어 내는 길, 고를 때를 봅니다. 주문·결제·배송을 처리하는 쇼핑몰 하나를 줄곧 예로 씁니다.

여러 부서가 한 건물을 쓰는 회사를 떠올리면 가깝습니다. 건물이 하나라 문을 열고 닫는 시간은 모든 부서가 같습니다. 그래도 영업팀은 회계 자료가 필요할 때 회계팀 서랍을 직접 열지 않습니다. 회계팀 창구에 요청하고 건네받은 것만 씁니다.

배포 단위와 모듈 경계

시스템을 나누는 기준은 둘입니다. 하나는 배포 단위입니다. 다른 하나는 모듈 경계입니다. 이 패턴은 둘을 따로 정합니다.

배포는 고친 프로그램을 서버에 올려 사용자가 쓰게 하는 일입니다. 한 번에 함께 올라가는 묶음을 배포 단위라고 합니다. 배포 단위가 하나면 코드 한 줄을 고쳐도 전체를 다시 올립니다.

모듈은 코드를 기능마다 나눠 이름을 붙인 덩이입니다. 주문 모듈, 결제 모듈처럼 나눕니다. 모듈마다 따로 이해하고 따로 고치려고 나눕니다.

배포 단위가 하나인 애플리케이션을 모놀리스라고 부릅니다. 반대로 기능마다 따로 배포하는 서비스로 쪼갠 구조는 마이크로서비스라고 부릅니다. 둘은 배포 단위의 수로 갈립니다.

모놀리스 가운데 안쪽 경계까지 무너진 것도 있습니다. 어느 코드가 어느 테이블을 읽는지 아무도 모르는 상태입니다. 이런 덩어리를 큰 진흙 공이라고 부릅니다.

운영체제가 돌리는 프로그램 하나를 프로세스라고 합니다. 모놀리스는 프로세스 하나로 뜹니다. 마이크로서비스는 서비스마다 자기 프로세스로 뜹니다.

두 기준을 따로 놓고 보면 세 구조가 갈립니다.

구조 배포 단위 모듈 경계
큰 진흙 공 하나 무너져 있다
모듈러 모놀리스 하나 또렷하다
마이크로서비스 서비스마다 하나 프로세스가 가른다

모듈러 모놀리스는 가운데 줄입니다. 배포 단위는 큰 진흙 공과 같습니다. 경계는 마이크로서비스처럼 또렷합니다. 마이크로서비스와 다른 것은 경계를 지키는 수단입니다. 프로세스 경계 대신 코드 규칙으로 지킵니다.

모듈이 지키는 세 규칙

폴더를 나눴다고 경계가 생기지는 않습니다. 폴더만 나눈 코드는 여전히 서로의 안쪽을 부릅니다. 이 소절은 경계를 세우는 규칙 셋을 봅니다.

규칙 뜻 어기면
창구로만 부른다 모듈마다 바깥에 여는 창구를 정하고 나머지는 숨긴다 안쪽을 고칠 때마다 다른 모듈이 깨진다
자기 데이터만 만진다 이 모듈의 테이블은 이 모듈만 읽고 쓴다 칼럼 하나를 바꿀 때 어디가 깨질지 모른다
의존이 한 방향으로 흐른다 두 모듈이 서로를 부르지 않는다 한 모듈만 떼어 낼 수 없게 된다

첫째 규칙의 창구는 모듈이 바깥에 공개한 클래스와 메서드입니다. 흔히 모듈의 공개 인터페이스로 불리는 것이 이것입니다. 결제 모듈이라면 「결제한다」·「환불한다」 같은 기능만 엽니다. 금액을 계산하는 도우미 클래스나 테이블에 접근하는 코드는 모듈 안에 숨깁니다.

안쪽을 숨기면 결제 모듈은 창구만 유지한 채 안쪽을 마음대로 바꿀 수 있습니다. 쓰는 쪽이 모르는 코드는 바뀌어도 쓰는 쪽을 깨뜨리지 않기 때문입니다. 이렇게 안쪽을 숨기는 것을 캡슐화라고 합니다.

아래 그림은 주문 모듈이 결제 기능을 쓰는 두 길을 보입니다.

flowchart TD
    subgraph 배포단위["배포 단위 하나 · 프로세스 하나"]
        subgraph 주문["주문 모듈"]
            OI["주문 안쪽 코드"]
        end
        subgraph 결제["결제 모듈"]
            PA["결제 창구"] --> PI["결제 안쪽 코드"]
        end
        OI -->|"허용"| PA
        OI -.->|"금지"| PI
    end
    subgraph DB["데이터베이스 하나"]
        OT[("주문 테이블")]
        PT[("결제 테이블")]
    end
    OI --> OT
    PI --> PT

실선은 결제 창구를 거치는 길입니다. 점선은 창구를 건너뛰는 길입니다. 둘 다 같은 프로세스 안이라 점선도 함수 호출 한 줄이면 됩니다. 점선을 막는 것이 이 패턴이 하는 일입니다.

코드로 보면 두 길의 차이는 한 줄입니다.

Java
// 주문 모듈 안의 코드
paymentApi.pay(orderId, amount); // 허용
paymentRepository.save(payment); // 금지

첫 줄은 결제 창구를 부릅니다. 둘째 줄은 결제 모듈 안쪽의 저장 코드를 곧장 부릅니다. 둘째 줄 같은 호출이 늘수록 결제 모듈의 안쪽을 바꾸기 어려워집니다.

둘째 규칙은 같은 원칙을 데이터에 적용합니다. 모듈러 모놀리스도 대개 데이터베이스 하나를 씁니다. 그 안에서 테이블마다 주인 모듈을 정합니다. 주문 모듈이 결제 정보를 원하면 결제 테이블을 읽지 않고 결제 창구에 묻습니다.

데이터베이스 안에서 테이블을 묶는 이름 공간을 스키마라고 합니다. 모듈마다 스키마를 따로 두면 어느 테이블이 누구 것인지 이름만 보고 압니다.

두 테이블의 행을 짝지어 한 결과로 합치는 연산을 조인이라고 합니다. 둘째 규칙을 지키면 주문 테이블과 결제 테이블을 한 쿼리로 조인할 수 없습니다. 창구를 두 번 부르거나 필요한 값을 따로 복사해 둡니다.

셋째 규칙은 부르는 방향입니다. 주문이 결제를 부릅니다. 그런데 결제도 주문을 부르면 두 모듈은 늘 함께 있어야만 동작합니다. 이렇게 서로 기대는 고리를 순환 의존이라고 합니다.

고리가 생기면 한 모듈만 떼어 내거나 따로 시험할 수 없습니다. 그래서 방향을 한쪽으로 정합니다.

결제가 주문에 무언가를 알려야 할 때는 이벤트를 씁니다. 이벤트는 일어난 일을 알리기만 하는 메시지입니다. 결제 코드는 누가 그 알림을 받는지 모르므로 주문을 부르지 않습니다. 이벤트가 전달되는 모습은 아래 「모듈끼리 주고받는 두 방법」에서 봅니다.

규칙을 지키게 하는 장치

세 규칙은 모두 한 프로세스 안의 약속입니다. 네트워크가 막아 주지 않으므로 누군가 한 줄만 어겨도 경계에 구멍이 납니다. 그래서 규칙을 사람의 주의가 아니라 도구로 지킵니다. 흔히 쓰는 장치는 셋입니다.

장치 하는 일
접근 제한자 바깥에 보일 필요 없는 클래스와 메서드를 공개하지 않는다
하위 프로젝트 분리 업무 모듈마다 빌드 하위 프로젝트를 하나씩 둔다
아키텍처 테스트 금지한 방향의 호출이 코드에 있으면 테스트가 실패한다

접근 제한자는 언어가 기본으로 주는 장치입니다. 바깥에 보일 필요 없는 클래스를 공개하지 않으면 다른 모듈 코드는 그 클래스를 쓸 수 없습니다. 따로 도구를 들이지 않아도 됩니다.

하위 프로젝트 분리는 업무 모듈 하나를 빌드 도구의 하위 프로젝트 하나로 만드는 방법입니다. 빌드 도구에 따라 이 하위 프로젝트를 모듈이라고도 부릅니다. 업무 모듈 하나가 빌드 단위 하나와 짝을 이루는 셈입니다.

하위 프로젝트마다 빌드 설정에 자기가 쓸 다른 하위 프로젝트를 적습니다. 이 목록이 의존 선언입니다. 결제 프로젝트 설정에 주문을 적지 않으면, 결제 코드가 주문 코드를 부르는 줄은 컴파일부터 안 됩니다.

아키텍처 테스트는 코드끼리 누가 누구를 가져다 쓰는지 검사하는 테스트입니다. 테스트에는 「주문 모듈 코드는 결제 모듈 안쪽 패키지를 쓰지 않는다」 같은 규칙을 적습니다. 앞에서 본 금지 줄 paymentRepository.save(payment) 가 주문 코드에 있으면 이 테스트가 실패합니다. 평소 테스트와 함께 돌리므로 빌드할 때마다 걸러집니다.

셋 가운데 무엇을 쓰든 목적은 같습니다. 누군가 점선 화살표를 쓰는 순간 빌드나 테스트가 알아차리게 하는 것입니다.

이 결정의 대가

모듈러 모놀리스는 코드의 얽힘을 막습니다. 배포 단위가 하나라서 모든 모듈을 함께 배포해야 하는 제약은 남습니다. 얻는 것과 남는 것을 가르면 아래와 같습니다.

대가 생기는 까닭
한 모듈을 고쳐도 전체를 다시 배포한다 배포 단위가 하나다
한 모듈이 메모리를 다 쓰면 모든 모듈이 함께 멈춘다 프로세스가 하나다
바쁜 모듈만 골라 늘릴 수 없다 늘릴 때도 전체를 복사해 띄운다
조인 한 번이던 조회가 창구 호출 여럿이 된다 남의 테이블을 직접 읽지 않는다
창구와 검사 장치를 만들고 돌봐야 한다 경계를 지키는 것이 코드 규칙뿐이다

위의 세 줄은 배포 단위가 하나라서 치르는 대가입니다. 모놀리스와 같습니다. 이 셋이 커지면 모듈을 서비스로 떼어 낼 때가 온 것입니다.

아래 두 줄은 경계를 세워서 치르는 대가입니다. 큰 진흙 공이라면 안 치러도 되는 값입니다. 그 값으로 한 모듈을 고칠 때 다른 모듈이 안 깨지는 코드를 삽니다.

모듈끼리 주고받는 두 방법

모듈이 다른 모듈의 기능을 쓰는 방법은 둘입니다. 창구를 직접 부르거나, 일어난 일을 알리기만 합니다.

직접 부르기는 답이 바로 필요할 때 씁니다. 주문 모듈은 결제가 됐는지 알아야 주문을 확정하므로 결제 창구를 불러 결과를 받습니다. 같은 프로세스 안의 함수 호출이라 중간에 네트워크가 끼지 않습니다.

직접 부르면 얻는 것이 하나 더 있습니다. 여러 변경을 한 묶음으로 처리해 전부 반영하거나 전부 취소하는 장치를 트랜잭션이라고 합니다. 데이터베이스가 하나이므로 주문 저장과 결제 기록을 한 트랜잭션으로 묶을 수 있습니다. 서비스로 쪼개 데이터베이스까지 나누면 이 묶음이 끊깁니다.

이미 일어난 업무상 사건을 알리는 메시지를 도메인 이벤트라고 합니다. 주문 모듈은 「주문이 완료됐다」는 이벤트를 내보내기만 합니다. 배송 모듈이 그 이벤트를 받아 배송을 잡습니다.

주문 모듈이 내보낸 이벤트는 가운데의 중계 장치가 받습니다. 중계 장치는 그 이벤트를 받겠다고 미리 등록해 둔 모듈에 이벤트를 나눠 줍니다. 같은 프로세스 안에서 도는 이 장치가 이벤트 버스입니다.

sequenceDiagram
    participant 주문 as 주문 모듈
    participant 결제 as 결제 모듈
    participant 버스 as 이벤트 버스
    participant 배송 as 배송 모듈
    주문->>결제: 결제 창구 호출
    결제-->>주문: 결제 결과
    주문-)버스: 「주문 완료」 이벤트
    버스-)배송: 「주문 완료」 이벤트
    Note over 주문: 누가 이벤트를 받는지 모른다

결제는 결과를 받아야 다음으로 넘어가는 호출입니다. 배송 쪽은 이벤트 버스에 알림만 넘깁니다. 주문 모듈 코드에는 배송 모듈의 이름이 없습니다.

그래서 이벤트를 받는 모듈이 늘어도 주문 모듈 코드는 안 바뀝니다. 셋째 규칙에서 결제가 주문에 알릴 때 쓴다고 한 방법도 이것입니다. 결제는 이벤트를 내보내기만 합니다. 주문이 이벤트 버스를 거쳐 그 이벤트를 받습니다.

서비스로 떼어 내는 길

모듈러 모놀리스는 마지막 모양이 아니라 중간 단계로도 쓰입니다. 경계가 오래 흔들리지 않은 모듈은 그 경계를 따라 서비스로 떼어 낼 수 있습니다.

떼어 낼 때 바뀌는 것은 창구를 부르는 방법 하나입니다. 함수 호출이던 창구가 네트워크 너머의 API(Application Programming Interface, 응용 프로그램 인터페이스)가 됩니다. API 는 프로그램이 다른 프로그램을 부르는 약속입니다.

flowchart TD
    subgraph 떼기전["떼기 전 · 프로세스 하나"]
        A1["주문 모듈"] -->|"함수 호출"| B1["결제 창구"]
    end
    subgraph 뗀뒤["떼어 낸 뒤 · 프로세스 둘"]
        A2["주문 모듈"] -->|"네트워크 요청"| B2["결제 서비스의 API"]
    end
    떼기전 --> 뗀뒤

위아래 두 묶음에서 달라진 것은 화살표 하나입니다. 창구 뒤의 코드와 테이블은 이미 결제 모듈만 쓰고 있었으므로 함께 옮기면 됩니다. 네트워크 요청은 실패할 수 있으므로 호출하는 쪽은 실패를 다루는 코드를 새로 갖춥니다.

기능을 하나씩 새 서비스로 옮기고 옛 코드는 그만큼 지워 가는 방식을 스트랭글러 무화과 패턴이라고 부릅니다. 모듈러 모놀리스에서는 옮길 단위가 이미 모듈로 나뉘어 있습니다.

경계를 세우지 않은 채 떼면 서비스는 나뉘어도 늘 함께 배포해야 하는 구조가 됩니다. 이 구조를 분산 모놀리스라고 합니다. 함께 배포해야 하는 제약은 남습니다. 네트워크 비용만 더 냅니다. 모듈러 모놀리스를 먼저 거치는 것은 이 실패를 피하려는 순서입니다.

모듈 경계를 긋는 기준

화면 처리·업무 로직·데이터 접근처럼 기술 역할로 계층을 쌓는 구조를 레이어드라고 합니다. 모듈을 이 계층대로 자르면 기능 하나를 고칠 때 모든 모듈을 건드립니다. 그래서 모듈은 업무 기능을 따라 자릅니다. 계층은 각 모듈 안에서 나눕니다.

업무의 말과 규칙을 코드 구조에 옮겨 담는 설계 방법이 도메인 주도 설계입니다. 모듈 경계를 찾을 때 이 방법을 자주 씁니다.

도메인 주도 설계에서 한 낱말이 한 가지 뜻으로 통하는 범위를 경계 컨텍스트라고 합니다. 주문에서 말하는 「상품」과 배송에서 말하는 「상품」이 담는 값이 다르면 둘은 다른 경계 컨텍스트입니다. 이 범위가 모듈 하나의 후보가 됩니다.

경계를 잘 그었는지는 두 성질로 잽니다. 첫째는 모듈 안의 코드가 한 가지 일을 위해 모여 있는 정도인 응집도입니다. 주문 모듈 안에 주문을 받아 확정하는 코드만 모여 있으면 응집도가 높습니다. 응집도는 높을수록 경계가 또렷합니다.

둘째는 모듈끼리 서로에게 기대는 정도인 결합도입니다. 주문 모듈이 결제 창구의 메서드 하나만 부르면 결합도가 낮습니다. 결제 안쪽 클래스와 테이블까지 두루 쓰면 결합도가 높습니다. 결합도는 낮을수록 경계가 또렷합니다.

고를 때와 떼어 낼 때

모듈러 모놀리스로 둘지 서비스로 떼어 낼지는 팀과 경계의 상황이 정합니다. 흔한 상황을 표로 봅니다.

상황 흔한 선택
팀이 한둘이고 배포를 함께 해도 기다림이 짧다 모듈러 모놀리스로 둔다
업무 경계가 아직 자주 바뀐다 모듈러 모놀리스로 두고 경계를 옮겨 가며 다듬는다
여러 팀이 서로의 배포를 기다린다 경계가 굳은 모듈부터 서비스로 떼어 낸다
한 모듈만 부하가 유독 크다 그 모듈만 떼어 따로 늘린다

둘째 줄이 모듈러 모놀리스를 고르는 흔한 까닭입니다. 모듈 사이의 경계를 옮기는 일은 한 코드베이스 안에서 코드를 옮기는 일입니다. 서비스 사이의 경계를 옮기려면 API 와 데이터베이스까지 함께 옮겨야 합니다.

셋째와 넷째 줄은 앞의 대가 표 위쪽 세 줄이 커진 경우입니다. 그때도 모듈 경계가 이미 서 있으면 떼어 내는 일이 모듈 하나를 옮기는 일로 줄어듭니다.

관련 항목

모듈러 모놀리스와 맞세워지는 구성 방식

모놀리스 · 마이크로서비스 · 큰 진흙 공 · 분산 모놀리스 · 서비스 지향 아키텍처 · 서버리스

모듈러 모놀리스가 속하는 상위 분류

소프트웨어 아키텍처 · 아키텍처 양식 · 시스템 설계 · 애플리케이션

모듈 경계를 재는 성질

결합도 · 응집도 · 순환 의존 · 캡슐화 · 정보 은닉 · 관심사 분리

모듈 경계를 긋는 설계 방법

도메인 주도 설계 · 경계 컨텍스트 · 모듈화 · 컨웨이의 법칙 · 수직 슬라이스 아키텍처

모듈 안쪽을 나누는 설계 양식

레이어드 · 헥사고날 · 클린 아키텍처 · 포트와 어댑터 · 패키지

모듈 경계를 지키게 하는 장치

접근 제한자 · 아키텍처 테스트 · ArchUnit · Spring Modulith · Java 모듈 시스템 · 멀티 모듈 프로젝트

모듈끼리 주고받는 수단

API · 공개 인터페이스 · 도메인 이벤트 · 이벤트 버스 · 이벤트 주도 · 발행-구독 · 트랜잭션 · 결과적 일관성

모듈을 서비스로 떼어 내는 방법

스트랭글러 무화과 · 리팩터링 · 서비스 분해 · API 게이트웨이

배포 단위 하나에 묶이는 운영 작업

배포 · 빌드 · 프로세스 · 수평 확장 · 장애 격리 · 무중단 배포

다른 이름: modular monolith · 모듈식 모놀리스 · 모듈형 모놀리스 · modulith