한 줄 정의

모듈성은 서로 연관된 코드를 하나의 단위로 묶는 조직화 원칙이며, 아키텍트가 시스템의 구조적 건전성을 유지하기 위해 지속적으로 에너지를 투입해야 하는 핵심 영역입니다.

쉽게 말하면

소프트웨어 시스템은 물리학의 복잡계(complex system)와 비슷합니다. 복잡계는 엔트로피가 높아지는 쪽(즉, 무질서)으로 나아가려는 경향이 있고, 질서를 유지하려면 에너지를 투입해야 합니다.

시스템의 수많은 부품이 어떻게 연결되는지 신경 쓰지 않고 설계한다면, 결국 수많은 어려움을 야기합니다. 모듈성을 잘 유지하는 것은 이 책에서 암묵적(implicit) 아키텍처 특성 이라 부르는 것 중 하나입니다. 프로젝트 요구사항에 명시되어 있지 않더라도, 좋은 모듈성이 제공하는 질서와 일관성이 꼭 필요합니다.

왜 중요한가?

  • 해결하는 문제: 아키텍처를 분석하는 데 사용하는 도구들(지표, 적합성 함수, 시각화 등)은 대부분 모듈성 및 관련 개념들에 의존합니다. 모듈성은 아키텍트의 핵심 분석 도구의 기초입니다.
  • 이게 없다면: 스파게티 아키텍처, 분산 모놀리스, 분산 진흙덩방(Distributed Big Ball of Mud) 같은 복잡하고 유지보수하기 어려운 아키텍처 안티패턴이 만들어집니다.

핵심 내용

모듈성 대 세분도

모듈성세분도(granularity; 또는 입도) 는 같은 의미로 사용할 때가 많지만, 둘은 의미가 매우 다릅니다.

  • 모듈성 — 시스템을 더 작은 조각으로 나누는 것에 관한 것입니다.
  • 세분도 — 그런 조각들의 크기 에 관한 것입니다. 시스템(또는 서비스)의 특정 부분이 얼마나 커야 하느냐의 문제입니다.

세분도는 서비스나 컴포넌트가 서로 결합하게 만드는 요인으로 작용합니다. 이로부터 스파게티 아키텍처, 분산 모놀리스 같은 안티패턴이 발생합니다.

모듈성을 받아들이되 세분도를 조심하라. — 마크 리처즈

모듈성의 정의

메리엄-웹스터 사전은 모듈 을 “더 복잡한 구조를 구성하는 데 사용할 수 있는 표준화된 부품 또는 독립적인 단위”라고 정의합니다.

이 책에서는 모듈성 이라는 용어를 서로 연관된 코드를 논리적으로 묶는 활동을 설명하는 데 사용합니다.

  • 객체 지향이면 클래스 그룹이 논리적 코드 그룹일 것이고, 구조적 언어나 함수형 언어라면 함수 그룹이 논리적 그룹입니다.
  • 개발자들은 연관된 코드를 함께 그룹화하는 수단으로 모듈을 사용합니다. (자바의 package, .NET의 namespace 등)
모듈성과 물리적 분리는 다릅니다

이 책에서 모듈성 은 연관된 코드 묶음(클래스나 함수 등)을 통칭하는 일반적인 용어로, 물리적 분리를 함의하지는 않습니다. 단지 논리적인 분리의 의미할 뿐입니다.

예를 들어, 모놀리스 애플리케이션에서는 아주 많은 수의 클래스를 하나로 함께 묶어도 큰 문제가 되지 않고 오히려 편리할 수 있습니다. 하지만 나중에 아키텍처를 재구성할 때가 되면, 원래의 느슨한 분할이 조장한 결합도 때문에 모놀리스를 분해하기가 어려워집니다.

이것이 모듈성을 특정 플랫폼이 강제하거나 암시하는 물리적 분리 와는 별개의 개념으로 이야기하는 것이 유용한 이유입니다.

이름공간(namespace)의 역할

개발자들은 서로 다른 소프트웨어 자산들(컴포넌트, 클래스 등)을 완전히 한정된 이름(fully qualified name, FQN)을 이용해서 구분합니다.

패키징이 아키텍처에 중요한 영향을 미칩니다. 예를 들어, 다수의 패키지가 긴밀하게 결합되어 있으면, 관련 작업을 위해 그중 하나를 재사용하기가 더 어려워집니다.

모듈성 측정

모듈성이 매우 중요한 만큼, 이를 더 잘 이해할 수 있는 도구가 필요합니다. 여기서는 세 가지 핵심 개념인 응집, 결합, 그리고 동변성 에 집중합니다.

응집(Cohesion)

응집 은 하나의 모듈을 구성하는 요소들이 정말로 그 모듈 안에 들어 있어야 하는가에 관한 것입니다. 모듈을 구성하는 요소들이 서로 얼마나 관련이 있는지를 측정하는 지표입니다.

응집의 관점에서 이상적인 모듈은 필요한 모든 요소가 한데 패키징된 모듈입니다. 그런 모듈을 더 작은 모듈들로 나누는 것은 바람직하지 않습니다. 모듈을 유용하게 사용하려면, 호출을 통해서 모듈 구성요소들을 결합해야 하기 때문입니다.

응집력 있는 모듈을 나누려고 해 봤자 결합도가 증가하고 가독성이 떨어질 뿐이다. — 래리 콘스탄틴

응집 유형을 최선에서 최악의 순으로 나열하면 다음과 같습니다.

graph LR
    A[기능적 응집] --> B[순차적 응집]
    B --> C[통신적 응집]
    C --> D[절차적 응집]
    D --> E[시간적 응집]
    E --> F[논리적 응집]
    F --> G[우발적 응집]

    style A fill:#2e7d32,color:#fff
    style B fill:#388e3c,color:#fff
    style C fill:#689f38,color:#fff
    style D fill:#f9a825,color:#000
    style E fill:#ef6c00,color:#fff
    style F fill:#d84315,color:#fff
    style G fill:#b71c1c,color:#fff
유형설명응집 수준
기능적 응집 (functional)모듈의 모든 요소가 서로 관련되어 있으며, 모듈이 기능하는 데 필요한 모든 요소가 모듈에 들어 있습니다최고
순차적 응집 (sequential)두 모듈이 데이터를 매개로 해서 차례대로 상호작용합니다
통신적 응집 (communicational)두 모듈이 하나의 통신 체인을 형성해서 각자 정보를 처리하거나 어떤 출력에 기여합니다
절차적 응집 (procedural)두 모듈이 코드를 반드시 특정한 순서로 실행합니다
시간적 응집 (temporal)모듈들이 시간적 의존성에 기반해서 연관됩니다 (예: 시스템 초기화 시점에서 실행해야 할 것들)
논리적 응집 (logical)모듈 내의 데이터가 논리적으로는 관련이 있지만 기능적으로는 그렇지 않습니다 (예: StringUtils 패키지)
우발적 응집 (coincidental)모듈의 요소들이 별로 관련이 없고, 그저 같은 소스 파일에 모여 있을 뿐입니다최저

응집도는 결합도보다 덜 정확한 지표 입니다. 특정 모듈의 응집도는 특정 아키텍트의 재량에 따라 결정되는 경우가 많습니다.

예를 들어, 다음과 같은 Customer Maintenance(고객 관리) 모듈이 있다고 가정합니다.

  • add customer (고객 추가)
  • update customer (고객 갱신)
  • get customer (고객 조회)
  • notify customer (고객 알림)
  • get customer orders (고객 주문 조회)
  • cancel customer orders (고객 주문 취소)

마지막 두 항목(주문 조회, 주문 취소)이 이 고객 관리 모듈에 꼭 필요할까요? 개발자가 이 두 항목을 분리한다면 Customer Maintenance와 Order Maintenance(주문 관리) 두 개의 모듈이 됩니다. 어떤 것이 올바른 구조일까요? 답은 “상황에 따라 다릅니다” 입니다.

  • Order Maintenance 모듈의 연산이 주문 조회와 취소 둘뿐 이라면? → 모듈이 너무 작으므로 다시 통합하는 것이 합리적일 수 있습니다.
  • Customer Maintenance가 앞으로 훨씬 더 커질 것으로 예상된다면? → 해당 행동방식(behavior)을 다른 모듈로 추출하는 것을 검토해야 합니다.
  • Order Maintenance의 작동에 필요한 Customer 정보가 아주 많다면? → 분리해도 주문 처리 시 고객 데이터(이름, 주소, 결제 정보 등)를 계속 참조해야 하므로 두 모듈이 강하게 엮이게 됩니다. 분리의 이점 없이 결합도만 높아지므로 통합이 낫습니다.
  • Order 도메인이 환불, 배송 추적, 주문 이력 분석 등 자체 비즈니스 로직이 풍부해진다면? → 주문 관련 연산들이 그 자체로 응집력 있는 단위가 되므로 분리가 유리합니다.
응집 결여도 — LCOM 지표

치담버와 케메러의 객체 지향 지표 스위트에 포함된 LCOM(Lack of Cohesion in Methods; 메서드 응집 결여도) 은 모듈의 구조적 응집도를 측정하는 지표입니다.

기본적으로 LCOM 지표는 클래스 내의 비본질적인 결합 관계를 반영한 것입니다. 클래스 내에서 필드를 공유하지 않는 메서드 집합의 비율, 즉 “필드 공유를 통해 공유되지 않는 메서드 집합의 비율” 이라는 뜻입니다.

세 클래스를 통해 LCOM 지표를 직관적으로 이해할 수 있습니다. 여기서 팔각형은 필드, 사각형은 메서드이고, 연결선은 “이 메서드가 이 필드에 접근한다”는 뜻입니다.

클래스 X — LCOM 점수가 낮음 (응집이 좋음)

graph LR
    A{{A}} --- m1["m1()"]
    A --- m2["m2()"]
    A --- m3["m3()"]
    B{{B}} --- m1
    B --- m2
    B --- m3
    C{{C}} --- m1
    C --- m3

    style A fill:#ff9800,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#ff9800,color:#fff
    style m1 fill:#42a5f5,color:#fff
    style m2 fill:#42a5f5,color:#fff
    style m3 fill:#42a5f5,color:#fff

모든 메서드가 대부분의 필드에 접근합니다. 필드 공유가 많으므로 LCOM 점수가 낮고, 이 클래스는 응집이 좋습니다.

클래스 Y — LCOM 점수가 높음 (응집이 나쁨)

graph LR
    A{{A}} --- m1["m1()"]
    B{{B}} --- m2["m2()"]
    C{{C}} --- m3["m3()"]

    style A fill:#ff9800,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#ff9800,color:#fff
    style m1 fill:#42a5f5,color:#fff
    style m2 fill:#42a5f5,color:#fff
    style m3 fill:#42a5f5,color:#fff

각 메서드가 자기 필드 하나에만 접근합니다. 필드 공유가 전혀 없으므로 LCOM 점수가 매우 높고, 이 클래스는 사실상 세 개의 독립된 클래스입니다. 코드/메서드 쌍을 개별 클래스로 분리해도 시스템의 행동방식에는 영향을 미치지 않습니다.

클래스 Z — 혼합

graph LR
    A{{A}} --- m1["m1()"]
    B{{B}} --- m1
    B --- m2["m2()"]
    C{{C}} --- m3["m3()"]

    style A fill:#ff9800,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#ff9800,color:#fff
    style m1 fill:#42a5f5,color:#fff
    style m2 fill:#42a5f5,color:#fff
    style m3 fill:#42a5f5,color:#fff

A와 B는 m1()을 통해 연결되어 있지만, C와 m3()은 완전히 독립적입니다. 마지막 필드/메서드 조합(C와 m3())은 개별 클래스로 리팩터링해도 될 것입니다.

LCOM 지표는 코드베이스의 재구축이나 마이그레이션, 또는 이해를 위해 코드베이스를 분석할 때 도움이 됩니다.

결합도(Coupling)

코드베이스의 결합도를 분석하는 도구는 부분적으로 그래프 이론 에 기반합니다. 메서드 호출과 반환은 호출 그래프(call graph)를 형성하는데, 그래프 이론을 이용해서 호출 그래프를 수학적으로 분석할 수 있습니다.

에드워드 요든과 래리 콘스탄틴은 구심 결합도(afferent coupling)원심 결합도(efferent coupling) 지표를 비롯해 다수의 핵심 개념을 정의했습니다.

결합도 유형설명방향
구심 결합도 (afferent, Cᵃ)해당 코드 요소로 들어오는 연결들의 개수안으로
원심 결합도 (efferent, Cᵉ)해당 코드 요소에서 다른 코드 요소로 나가는 연결들의 개수밖으로

거의 모든 플랫폼에는 아키텍트가 코드의 이러한 결합 특성들을 분석하는 데 사용할 수 있는 도구들이 있습니다.

핵심 지표들 — 추상도, 불안정도, 주 시퀀스 거리

로버트 C. 마틴이 개발한 지표들로, 대부분의 객체 지향 언어에 평범위하게 적용됩니다.

추상도(Abstractness)

추상적인 요소(추상 클래스, 인터페이스 등)와 구체적인 요소(구현 코드)의 비(ratio)입니다.

A = Σmᵃ / (Σmᶜ + Σmᵃ)

  • mᵃ는 추상적 요소(인터페이스나 추상 클래스)이고 mᶜ는 구체적 요소(비추상 클래스 등)입니다.
  • 추상도가 0에 가까우면 → 구체적인 코드베이스 (예: 5,000줄짜리 main() 하나)
  • 추상도가 1에 가까우면 → 극도로 추상적인 코드베이스 (추상화 계층이 많아 연결 구조 파악이 어려움)

불안정도(Instability)

원심 결합도를 원심 결합도와 구심 결합도의 합으로 나눈 것입니다. 코드베이스의 변동성(volatility; 또는 취발성) 을 결정합니다.

I = Cᵉ / (Cᵉ + Cᵃ)

불안정도가 높은 코드베이스는 결합도가 높아서 변경 시 고장 나기 쉽습니다.

주 시퀀스로부터의 거리(Distance from the Main Sequence)

불안정도와 추상도에서 파생된 지표입니다.

D = |A + I − 1|

Transclude of Drawing-2026-03-03-22.02.03.excalidraw

  • 주 시퀀스(Main Sequence) — A + I = 1인 대각선. 추상성과 불안정성이 이상적으로 균형을 이루는 선입니다.
  • 무용 지역(Zone of Uselessness) — 대각선 위쪽. 추상도는 높은데 안정적입니다. 너무 추상적이기만 하고 아무도 사용하지 않는 코드입니다.
  • 고통 지역(Zone of Pain) — 대각선 아래쪽. 구체적인데 불안정합니다. 추상화가 부족해서 유지보수하기 어렵습니다.

D 값이 클수록 균형선에서 멀리 떨어져 있으므로, 무용 지역이나 고통 지역에 속할 가능성이 높습니다.

동변성(Connascence)

메일리어 페이지존스가 정의한 개념으로, 결합의 다양한 유형을 더욱 정확하게 설명하는 언어를 제공합니다.

두 컴포넌트가 동변적(connascent) 이라는 것은 한 컴포넌트가 변했을 때 시스템의 전체적인 정확성을 유지하려면 다른 컴포넌트도 변해야 한다는 뜻입니다.

정적 동변성 — 소스 코드 수준의 결합입니다.

유형설명예시
이름 동변성 (of name)여러 컴포넌트가 엔터티의 이름에 합의해야 합니다메서드 이름, 매개변수 이름
타입 동변성 (of type)여러 컴포넌트가 엔터티의 타입에 합의해야 합니다변수와 매개변수의 타입
의미 동변성 (of meaning)여러 컴포넌트가 특정 값의 의미에 합의해야 합니다TRUE = 1, FALSE = 0 같은 규약
위치 동변성 (of position)여러 컴포넌트가 값들의 순서에 합의해야 합니다함수 호출 시 매개변수 순서
알고리즘 동변성 (of algorithm)여러 컴포넌트가 특정 알고리즘에 합의해야 합니다서버-클라이언트 해싱 알고리즘

동적 동변성 — 실행 시점(runtime)에서의 호출들에 관한 것입니다.

유형설명예시
실행 동변성 (of execution)여러 컴포넌트의 실행 순서가 중요합니다이메일 속성 설정 후 전송
타이밍 동변성 (of timing)여러 컴포넌트의 실행 타이밍이 중요합니다경합 조건(race condition)
값 동변성 (of values)여러 값이 서로 의존하며, 반드시 함께 변경되어야 합니다사각형의 네 꼭짓점 좌표
신원 동변성 (of identity)여러 컴포넌트가 동일한 엔터티를 참조해야 합니다분산 대기열(Kafka, RabbitMQ)
graph RL
    subgraph dynamic["동적 동변성 (강함)"]
        F[실행]
        G[타이밍]
        H[값]
        I[신원]
    end
    subgraph static["정적 동변성 (약함)"]
        A[이름]
        B[타입]
        C[의미]
        D[위치]
        E[알고리즘]
    end
    dynamic -.->|리팩터링 방향| static

    style A fill:#81c784,color:#000
    style B fill:#66bb6a,color:#000
    style C fill:#fdd835,color:#000
    style D fill:#ffb74d,color:#000
    style E fill:#ff8a65,color:#000
    style F fill:#ef5350,color:#fff
    style G fill:#e53935,color:#fff
    style H fill:#c62828,color:#fff
    style I fill:#b71c1c,color:#fff

동변성의 속성들

동변성은 아키텍트와 개발자를 위한 분석의 틀(프레임워크)입니다. 세 가지 속성이 있습니다.

속성설명
세기(strength)시스템 동변성의 결합을 리팩터링하기가 얼마나 쉬운지를 나타냅니다. 아키텍트는 동적 동변성보다 정적 동변성을 선호해야 합니다. 정적 동변성은 간단한 소스 코드 분석으로 파악할 수 있고, 현대적인 도구들을 활용하면 정적 동변성을 수월하게 개선할 수 있기 때문입니다.
지역성(locality)동변적인 코드들 사이의 거리를 측정합니다. 같은 모듈 안에서의 동변성은 괜찮지만, 서로 다른 모듈이나 서비스에 걸친 동변성은 변경의 파급 범위가 넓어지고 조율 비용이 커지므로 더 큰 문제가 됩니다.
정도(degree)변경에 영향을 받는 클래스가 많을수록 동변성의 정도가 높은 것입니다. 동변성의 정도가 낮으면 코드베이스에 덜 해롭습니다.
페이지존스의 모듈성 개선 가이드라인

페이지존스는 동변성을 이용해서 시스템 모듈성을 개선하기 위한 세 가지 가이드라인을 제시했습니다.

  1. 시스템을 캡슐화된 요소들로 분해해서 전체적인 동변성을 최소화한다.
  2. 캡슐화의 경계에 걸쳐 있는 동변성을 찾아서 최소화한다.
  3. 캡슐화 경계 안의 동변성을 최대화한다.

즉 모듈 내부에서는 강한 동변성이 괜찮지만, 모듈 사이에 걸친 동변성은 최대한 줄여야 한다는 뜻입니다.

짐 웨이리치의 동변성 규칙
  • 정도의 규칙(Rule of Degree): 강한 유형의 동변성을 더 약한 유형의 동변성으로 변환하라.
  • 지역성의 규칙(Rule of Locality): 소프트웨어 요소 사이의 거리가 멀수록 더 약한 유형의 동변성을 적용하라.
동변성과 도메인 주도 설계(DDD)

페이지존스의 책이 처음 출간되었을 때 아키텍트들은 이 지역성의 중요성을 거의 인식하지 못했습니다. 현대적인 어법으로 말하자면 이것은 아키텍트들이 구현 내부 사항(결합도가 높은)의 범위를 가능한 한 줄게 제한해야 한다는 뜻으로, DDD의 ‘경계 컨텍스트(bounded context)’ 개념에서도 구현의 결합을 제한하는 것이 바람직합니다.

동변성이 아키텍트에게 유용한 이유

동변성을 배우는 것이 유익한 이유는 설계 패턴을 배우는 것이 유익한 이유와 같습니다. 다양한 유형의 결합을 좀 더 정확하게 설명할 수 있는 언어로 작용합니다.

예를 들어, 코드 검토(code review)를 수행할 때 아키텍트가 개발자에게 “메서드 선언 중간에 마법의 문자열 리터럴을 추가하지 말고, 상수로 추출하세요”라고 지시하는 대신 “의미 동변성이 이름 동변성으로 변하도록 코드를 리팩터링하세요” 라고 말할 수 있습니다.

모듈에서 컴포넌트로

이 책은 서로 연관된 코드의 묶음을 모듈 이라고 부릅니다. 그런데 대부분의 아키텍트들이 모듈을 부르는 이름은 컴포넌트 입니다.

소프트웨어 아키텍처의 핵심 구축 요소인 컴포넌트 의 개념은(그리고 그에 대응되는 논리적 또는 물리적 분리에 대한 분석은) 컴퓨터 과학의 초기 시절부터 존재해 왔지만, 개발자와 아키텍트들은 여전히 좋은 결과물을 만들어 내는 데 어려움을 겪고 있습니다.

정리

모듈성 vs 세분도

기준모듈성 (Modularity)세분도 (Granularity)
관심사시스템을 조각으로 나누는 것 자체조각의 크기
핵심 질문”어떤 코드를 함께 묶을 것인가?""서비스/컴포넌트가 얼마나 커야 하는가?”
위험느슨한 분할 → 결합도 증가과도한 분할 → 분산 모놀리스

정적 동변성 vs 동적 동변성

기준정적 동변성동적 동변성
분석 시점소스 코드 수준실행 시점(runtime)
발견 용이성코드 분석 도구로 쉽게 발견발견하기 어려움
리팩터링비교적 수월함어려움
아키텍트 선호선호함가능하면 정적 동변성으로 변환

내 생각

  • 모듈성은 암묵적 아키텍처 특성 이라는 점이 핵심입니다. 요구사항에 “모듈성을 확보해주세요”라고 쓰여 있는 프로젝트는 없지만, 이것이 없으면 시스템은 반드시 엔트로피에 의해 무너집니다. 아키텍트가 지속적으로 에너지를 투입해야 하는 영역입니다.

  • LCOM 지표 는 레거시 코드를 분석하거나 마이크로서비스로 마이그레이션할 때 유용합니다. LCOM 점수가 높은 클래스는 분리 후보로 검토할 수 있습니다.

  • 동변성 개념은 코드 리뷰에서 즉시 활용할 수 있습니다. “매직 넘버를 상수로 바꾸세요”라는 피드백을 “의미 동변성을 이름 동변성으로 리팩터링하세요”라고 표현하면, 팀 내에서 결합에 대한 공통 어휘가 생깁니다.

  • 짐 웨이리치의 정도의 규칙지역성의 규칙 은 직관적이면서도 강력한 가이드라인입니다. 특히 마이크로서비스 아키텍처에서 서비스 간 결합을 평가할 때, “서비스 사이의 거리가 멀수록 더 약한 유형의 동변성을 적용하라”는 규칙은 매우 실용적입니다.

관련 개념

출처

  • 소프트웨어 아키텍처 The Basics, Ch03 모듈성