응집도, 결합도, 그리고 SOLID — 50년간 살아남은 소프트웨어 설계 원칙

· # AI 개념
소프트웨어 설계 SOLID 응집도 결합도 클린 아키텍처 리팩토링

1972년 12월, David Parnas는 “On the Criteria To Be Used in Decomposing Systems into Modules”라는 짧은 논문을 발표했다.1 이 논문은 모듈을 기능적 흐름이 아니라 정보 은닉(Information Hiding) 원칙에 따라 나누어야 한다고 주장했다. 2년 후인 1974년, Stevens, Myers, Constantine은 응집도(Cohesion)와 결합도(Coupling)라는 용어를 정의하며 구조적 설계의 기초를 다졌다.2 50년이 지난 지금, 이 개념들은 마이크로서비스 아키텍처에서 바운디드 컨텍스트를 정의하고, 함수형 프로그래밍에서 순수 함수를 설계하며, 클린 아키텍처의 의존성 규칙을 구현하는 데까지 여전히 핵심 역할을 하고 있다. 왜 이 원칙들은 이토록 오래 살아남았을까?

모든 것의 시작: 정보 은닉

David Parnas의 1972년 논문은 소프트웨어 설계 사상의 전환점이었다. 당시 주류였던 기능 분해 방식 대신, 그는 “변경 가능성이 높은 설계 결정을 다른 모듈로부터 숨기는” 방식을 제안했다. Parnas의 핵심 아이디어는 간단했다. 시스템을 분해할 때 다음 순서를 따라야 한다:

  1. 변경 가능성이 높은 설계 결정 목록을 작성한다
  2. 각 결정을 하나의 모듈이 담당하도록 한다
  3. 그 결정을 다른 모듈로부터 완전히 숨긴다 이는 관심사 분리(Separation of Concerns)의 구체적 실현이었다. Edsger Dijkstra가 1974년 “On the role of scientific thought”에서 언급한 이 원칙은 “동시에 한 가지 일에만 집중할 수 있도록 하는 것”을 목표로 했다.3

응집도: 모듈 내부의 결속력

Stevens, Myers, Constantine이 1974년 정의한 응집도는 “모듈 내 구성요소들이 얼마나 밀접하게 관련되어 있는가”를 측정한다.2 그들은 응집도를 7단계로 분류했으며, 이는 오늘날까지도 유효하다.

응집도 유형특징예시품질
우연적(Coincidental)논리적 관련성 없이 임의로 묶임유틸리티 클래스최악
논리적(Logical)유사한 기능들을 묶음InputManager (키보드+마우스+터치)나쁨
시간적(Temporal)같은 시점에 실행되는 기능들초기화 모듈보통
절차적(Procedural)순차적 실행 흐름에 따라 묶음데이터 처리 파이프라인보통
소통적(Communicational)같은 데이터를 조작하는 기능들Customer CRUD 클래스좋음
순차적(Sequential)한 기능의 출력이 다른 기능의 입력데이터 변환 체인좋음
기능적(Functional)하나의 명확한 작업 수행calculateTax(), sendEmail()최상

기능적 응집도가 최고인 이유는 단일 책임 원칙과 정확히 일치하기 때문이다. 변경 이유가 하나뿐이므로 유지보수성이 극대화된다.

# 우연적 응집도 (나쁜 예)
class Utils:
    def format_date(self, date): pass
    def calculate_tax(self, amount): pass
    def send_email(self, message): pass
    def validate_password(self, password): pass
# 기능적 응집도 (좋은 예)
class TaxCalculator:
    def calculate(self, amount, tax_rate):
        return amount * (1 + tax_rate)
class EmailService:
    def send(self, recipient, subject, body):
        # 이메일 전송 로직
        pass

결합도: 모듈 간의 의존성

결합도는 “모듈들이 얼마나 서로 의존하고 있는가”를 측정한다.4 Yourdon과 Constantine은 6가지 유형을 정의했다.

결합도 유형특징예시품질
내용(Content)한 모듈이 다른 모듈의 내부를 직접 조작전역 변수 직접 수정최악
공통(Common)전역 데이터를 공유Global state, 싱글톤 남용나쁨
제어(Control)제어 정보를 전달Flag 매개변수나쁨
스탬프(Stamp)데이터 구조 전체를 전달하나 일부만 사용객체 전체를 넘기고 한 필드만 사용보통
데이터(Data)필요한 데이터만 매개변수로 전달원시 타입 매개변수좋음
메시지(Message)매개변수 없는 메시지 전달이벤트 기반 통신최상

실무에서는 다음과 같은 정량적 메트릭으로도 결합도를 측정한다:

  • CBO(Coupling Between Objects): 클래스가 참조하는 다른 클래스 수
  • Ca(Afferent Coupling): 해당 모듈에 의존하는 모듈 수
  • Ce(Efferent Coupling): 해당 모듈이 의존하는 모듈 수
  • 불안정성(Instability): Ce/(Ca+Ce), 0(안정)–1(불안정)
# 스탬프 결합도 (나쁜 예)
def process_user(user_obj):
    # user_obj의 name만 사용하는데 전체 객체를 받음
    return f"Processing {user_obj.name}"
# 데이터 결합도 (좋은 예) 
def process_user(user_name: str):
    return f"Processing {user_name}"

Connascence: 결합도의 현대적 재해석

1996년, Meilir Page-Jones는 “What Every Programmer Should Know About Object-Oriented Design”에서 Connascence(공생성)라는 개념을 제시했다.5 이는 기존 결합도 개념을 객체지향 시대에 맞게 확장한 것이다. Connascence는 3가지 축으로 평가된다:

  1. 강도(Strength): 변경의 어려움 정도
  2. 지역성(Locality): 코드상 거리
  3. 정도(Degree): 영향받는 요소의 개수 Page-Jones는 Connascence를 정적동적으로 구분했다:

정적 Connascence (소스코드 분석 가능)

  • 이름(Name): 같은 이름 참조
  • 타입(Type): 같은 타입 사용
  • 의미(Meaning): 특정 값의 의미 공유
  • 위치(Position): 매개변수 순서 의존
  • 알고리즘(Algorithm): 같은 알고리즘 구현

동적 Connascence (런타임에만 확인 가능)

  • 실행(Execution): 실행 순서 의존
  • 타이밍(Timing): 시간 의존성
  • (Value): 관련 값들의 일치 필요
  • 정체성(Identity): 같은 객체 참조 필요 핵심 원칙: 거리가 멀수록 강도가 약해야 하고, 강도가 강할수록 지역성이 높아야 한다.
# Connascence of Position (나쁜 예)
create_user("John", "Doe", 25, "john@email.com")
# Connascence of Name (좋은 예) 
create_user(
    first_name="John",
    last_name="Doe", 
    age=25,
    email="john@email.com"
)

디미터 법칙: 결합도를 낮추는 실천 규칙

1987년 Northeastern University에서 Ian Holland이 제안한 디미터 법칙(Law of Demeter)은 “최소 지식 원칙”(Principle of Least Knowledge)으로도 불린다.6 핵심은 “친구하고만 대화하라”이다. 즉, 객체는 다음과만 상호작용해야 한다:

  1. 자기 자신의 메서드
  2. 매개변수로 받은 객체의 메서드
  3. 자신이 생성한 객체의 메서드
  4. 직접 구성 요소 객체의 메서드
# 디미터 법칙 위반 (나쁜 예)
class Order:
    def get_total(self):
        return self.customer.wallet.money.amount
# 디미터 법칙 준수 (좋은 예)
class Order:
    def get_total(self):
        return self.customer.get_payment_amount()

이 규칙을 따르면 체이닝 호출이 줄어들고, 중간 객체의 변경이 클라이언트에 미치는 영향이 최소화된다.

응집도×결합도 매트릭스

응집도와 결합도의 조합에 따른 시스템 품질을 매트릭스로 정리하면:

응집도↓결합도→낮은 결합도보통 결합도높은 결합도
높은 응집도🟢 이상적🟡 개선 필요🔴 리팩토링 필요
보통 응집도🟡 사용 가능🟡 평범함🔴 문제 있음
낮은 응집도🔴 분해 필요🔴 심각함🔴 재설계 필요

가장 좋은 조합은 높은 응집도 + 낮은 결합도이며, 이는 모든 설계 패턴과 아키텍처 원칙의 궁극적 목표이다.

SOLID 원칙과 응집도·결합도의 관계

Robert C. Martin이 2000년 “Design Principles and Design Patterns”에서 제시한 SOLID 원칙은 응집도와 결합도 개념의 현대적 발전형이다.7

SOLID 원칙정의응집도·결합도 기여
SRP클래스는 하나의 변경 이유만 가져야 함기능적 응집도 추구
OCP확장에 열리고 수정에 닫혀야 함추상화로 결합도 감소
LSP하위 클래스는 상위 클래스를 대체 가능해야 함인터페이스 결합도 유지
ISP클라이언트는 사용하지 않는 인터페이스에 의존하면 안 됨인터페이스 응집도 증가
DIP추상화에 의존하고 구체화에 의존하면 안 됨의존성 결합도 역전

이들 원칙은 각각 응집도를 높이거나 결합도를 낮추는 구체적 방법을 제시한다.

# SRP: 단일 책임 원칙 (기능적 응집도)
class TaxCalculator:
    def calculate_tax(self, amount: float, rate: float) -> float:
        return amount * rate
class TaxPersister:
    def save_tax_record(self, record: TaxRecord) -> None:
        # DB 저장 로직
        pass
# DIP: 의존성 역전 원칙 (낮은 결합도)
class PaymentProcessor:
    def __init__(self, payment_gateway: PaymentGateway):
        self._gateway = payment_gateway  # 추상화에 의존
    
    def process(self, amount: float) -> bool:
        return self._gateway.charge(amount)

실증 연구: 숫자로 보는 효과

응집도와 결합도가 실제로 소프트웨어 품질에 영향을 미치는지는 여러 실증 연구에서 검증되었다.

Basili, Briand & Melo (1996)

가장 중요한 초기 검증 연구다.8 Basili 등은 NASA의 8개 C++ 프로젝트에서 CK 메트릭을 수집하고 결함 발생과의 관계를 분석했다. 결과는 명확했다 — CBO, RFC, WMC, DIT, NOC 메트릭이 클래스의 결함 발생 가능성(fault-proneness)을 유의미하게 예측했다. 특히 CBO(결합도)가 높은 클래스일수록 결함이 발생할 확률이 뚜렷하게 증가했다.

Subramanyam & Krishnan (2003)

이 연구는 C++과 Java로 작성된 상용 소프트웨어를 대상으로 CK 메트릭과 결함의 관계를 분석했다.9 핵심 발견은 두 가지였다. 첫째, CBO 값이 증가하면 결함 수가 증가하는 유의미한 양의 관계가 존재했다. 둘째, 이 효과는 프로그래밍 언어에 따라 달랐다 — C++에서는 CBO 값의 증가에 따라 결함이 더 가파르게 늘었다.

Gyimóthy, Ferenc & Beszédes (2005)

오픈소스 프로젝트 Mozilla를 대상으로 한 대규모 연구에서 로지스틱 회귀 분석을 수행한 결과, CBO와 LOC, RFC가 결함 예측에 가장 효과적인 메트릭으로 확인되었다.10 반면 LCOM과 DIT는 상대적으로 예측력이 낮았다.

실무 임계값

메트릭권장 임계값근거
CBO9 이하 (단일 멤버 기준)Sahar 등(2010)11, Microsoft Visual Studio 공식 문서
WMC20–50 이하Chidamber & Kemerer(1994)12 권장
DIT5 이하Visual Studio .NET 공식 문서

다만 이 임계값들은 절대적 기준이 아니다. 프로젝트의 도메인, 언어, 프레임워크에 따라 적정값이 달라지며, 중요한 것은 추세를 관찰하는 것이다 — 특정 클래스의 CBO가 프로젝트 평균보다 현저히 높다면 리팩토링 대상으로 검토할 필요가 있다.

현대적 적용: MSA와 함수형 프로그래밍

마이크로서비스 아키텍처

마이크로서비스는 바운디드 컨텍스트(Bounded Context) 단위로 높은 응집도를 유지하면서, 서비스 간에는 API나 이벤트를 통해 낮은 결합도를 추구한다.

  • 도메인 응집도: 같은 비즈니스 도메인 로직을 한 서비스에 집중
  • 데이터 결합도: 서비스 간 데이터베이스 공유 금지
  • 인터페이스 결합도: REST API, GraphQL, 이벤트 스트림을 통한 통신
# 높은 응집도: 주문 관련 모든 기능이 한 서비스에
order-service:
  - create_order()
  - cancel_order()
  - calculate_total()
  - apply_discount()
# 낮은 결합도: 이벤트로만 통신
events:
  - OrderCreated → inventory-service
  - OrderCancelled → payment-service
  - PaymentCompleted → shipping-service

함수형 프로그래밍

함수형 패러다임에서는 순수 함수불변성을 통해 완벽한 기능적 응집도와 최소 결합도를 달성한다.

-- 완벽한 기능적 응집도: 세금 계산만 수행
calculateTax :: Float -> Float -> Float
calculateTax amount rate = amount * rate
-- 최소 결합도: 외부 상태 의존성 없음
processOrder :: Order -> Float -> Order
processOrder order taxRate = 
  order { orderTotal = baseAmount + calculateTax baseAmount taxRate }
  where baseAmount = sum (map itemPrice (orderItems order))

50년의 교훈

응집도와 결합도 개념이 반세기 동안 살아남은 이유는 이들이 인간의 인지적 한계를 반영하기 때문이다. Miller의 “마법의 숫자 7±2”(1956)13에서 보듯, 인간은 동시에 처리할 수 있는 정보량에 제한이 있다. 높은 응집도는 관련된 것들을 함께 묶어 인지 부담을 줄이고, 낮은 결합도는 독립적 사고를 가능하게 한다. 기술은 계속 변했지만 — 절차적 프로그래밍에서 객체지향으로, 모놀리스에서 마이크로서비스로, 명령형에서 함수형으로 — 인간이 복잡성을 다루는 방식은 변하지 않았다. 그래서 이 원칙들은 앞으로도 계속 유효할 것이다. 현재 우리가 추구하는 클린 아키텍처, 도메인 주도 설계, 반응형 시스템 모두 결국 “관련된 것은 가까이, 무관한 것은 멀리”라는 동일한 철학을 바탕으로 한다. 50년 전 Parnas와 Constantine이 제시한 이 간단한 아이디어는 소프트웨어가 존재하는 한 계속해서 우리와 함께할 것이다.

Footnotes

  1. Parnas, D. L. (1972). “On the Criteria To Be Used in Decomposing Systems into Modules.” Communications of the ACM, 15(12), 1053–1058.

  2. Stevens, W., Myers, G., & Constantine, L. (1974). “Structured Design.” IBM Systems Journal, 13(2), 115–139. 2

  3. Dijkstra, E. W. (1974). “On the role of scientific thought.” EWD447.

  4. Yourdon, E. & Constantine, L. (1979). Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. Prentice-Hall.

  5. Page-Jones, M. (1996). What Every Programmer Should Know About Object-Oriented Design. Dorset House.

  6. Holland, I. (1987). “The Law of Demeter.” Northeastern University, Demeter Project.

  7. Martin, R. C. (2000). “Design Principles and Design Patterns.” objectmentor.com.

  8. Basili, V., Briand, L., & Melo, W. (1996). “A Validation of Object-Oriented Design Metrics as Quality Indicators.” IEEE Transactions on Software Engineering, 22(10), 751–761.

  9. Subramanyam, R. & Krishnan, M. S. (2003). “Empirical Analysis of CK Metrics for Object-Oriented Design Complexity.” IEEE Transactions on Software Engineering, 29(4), 297–310.

  10. Gyimóthy, T., Ferenc, R., & Siket, I. (2005). “Empirical Validation of Object-Oriented Metrics on Open Source Software for Fault Prediction.” IEEE Transactions on Software Engineering, 31(10), 897–910.

  11. Sahar, H. et al. (2010). 임계값 연구. Microsoft Visual Studio 공식 문서 참조.

  12. Chidamber, S. R. & Kemerer, C. F. (1994). “A Metrics Suite for Object Oriented Design.” IEEE Transactions on Software Engineering, 20(6), 476–493.

  13. Miller, G. A. (1956). “The Magical Number Seven, Plus or Minus Two.” Psychological Review, 63(2), 81–97.

이 글이 도움됐다면 눌러주세요