Architecture & Design/Software Architecture
Foundations - 03. 결합과 응집
접착제로 붙인 모형은 뜯기 어렵다 — 결합과 응집
아이가 접착제로 붙인 플라스틱 모형을 생각해 보자. 날개가 떨어져 다시 붙이려면 접착제를 뜯어내야 하고, 그러다 몸통이 부서진다. 한 부분을 교체하려면 전체를 해체하는 수고가 든다. 반면 레고 블록으로 만든 비행기는 날개 블록을 빼고 다른 걸 끼울 수 있다. 한 블록을 바꿔도 나머지는 그대로다.
소프트웨어 모듈도 같다. 접착제로 붙인 것처럼 얽혀 있으면 한 부분을 바꿀 때 다른 부분이 부서진다. 레고처럼 분리돼 있으면 변경이 국소에 머문다. 전자를 결합도(coupling)가 높다고, 후자를 낮다고 부른다. 그리고 결합을 가늠하는 반대쪽 끝에 응집도(cohesion)가 있다 — 한 조립품 안의 블록들이 하나의 목적(예: 자동차 모형)으로 뭉쳐 있는 정도. 이 둘은 아키텍처를 지배하는 가장 근본적인 두 힘이며, 1960년대 Larry Constantine의 연구에서 비롯된 가장 오래된 통찰이다.
이 비유의 한계: 레고는 물리적 결합만 표현하지만, 소프트웨어 결합에는 "의미"나 "실행 순서" 같은 비물리적 얽힘도 있다. 그 부분은 뒤에 코네이선스로 다룬다.
결합 — 한 부분을 바꿀 때 다른 부분이 얼마나 흔들리는가
결합은 모듈·서비스·클래스 사이의 의존 정도다. A가 B에 강하게 결합돼 있으면, B를 바꿀 때 A도 바꿔야 한다. 결합이 강할수록 변경의 여파가 넓고, 변경이 위험해지며, 결국 변경 자체를 두려워하게 된다 — 그게 레거시의 정체다.
가장 약한 결합은 A가 B를 전혀 모르는 상태(무결합)이고, 가장 강한 결합은 A가 B의 내부 구현 디테일에 직접 의존하는 상태다. Myers와 Page-Jones은 결합을 단계로 나눴다:
| 결합 종류 | 강도 | 예 |
|---|---|---|
| 자료 결합(data) | 가장 약함 | A가 B에 인자로 값을 넘긴다 |
| 스탬프 결합(stamp) | 약함 | A가 전체 구조체를 넘기지만 B는 일부만 쓴다 |
| 제어 결합(control) | 중간 | A가 B에 "어떻게 동작하라"는 플래그를 넘긴다 |
| 외부 결합(external) | 중간 | A와 B가 공통 외부 포맷에 의존 |
| 공통 결합(common) | 강함 | A와 B가 전역 상태를 공유 |
| 내용 결합(content) | 가장 강함 | A가 B의 내부 데이터를 직접 건드린다 |
아래로 갈수록 "B가 바뀌면 A가 깨질 가능성"이 커진다. 로그 메시지의 타임스탬프 포맷을 ISO 8601에서 Unix epoch로 바꿨더니 결제 모듈이 로그 문자열을 파싱하다가 실패하고, 대시보드가 빈 화면을 뿌리며, 로그의 특정 칸 위치를 하드코딩한 알림 스크립트가 오후 내내 울린 적이 있다. 로그 코드는 결제·대시보드·알림을 단 한 번도 직접 호출하지 않았다. 그들은 로그의 텍스트 포맷이라는 공통 '프로토콜'에 묶여 있었고(외부 결합), 그 포맷이 바뀌자 일제히 깨졌다. 이 결합은 import 문으로는 보이지 않는다 — 그래서 위험하다.
// 공통 결합 + 내용 결합 — 전역 장바구니의 내부 리스트에 직접 손댐
class Checkout {
void process() {
Cart cart = GlobalState.cart; // 전역 상태 (공통 결합)
cart.items.add(findItem("sku-42")); // 내부 컬렉션 직접 조작 (내용 결합)
}
}
// 자료 결합 — 인터페이스로 추상화, 최소 정보만 전달
class Checkout {
private final Cart cart; // 생성자 주입, 전역 상태 없음
void process(AddItemCommand cmd) { // 값(명령 객체)만 넘김
cart.add(cmd); // cart가 자기 내부를 스스로 관리
}
}
두 번째 코드에서는 Cart의 내부를 items라는 List로 쓰든 Map으로 바꾸든 Checkout이 모른다. 구현 디테일이 결합을 통해 새어나가지 않는다. 이것이 "캡슐화가 결합을 낮춘다"는 말의 정확한 뜻이다.
응집 — 한 모듈의 요소들이 얼마나 같은 일에 집중하는가
결합이 모듈 사이의 관계라면, 응집은 모듈 안의 요소들이 얼마나 밀접하게 연관돼 있는가다. 한 조립품 안의 블록들이 "자동차"라는 하나의 목적으로 뭉쳐 있으면 응집이 높은 것이고, 자동차 바퀴와 집의 지붕이 한 조립품에 섞여 있으면 응집이 낮은 것이다. 응집이 높으면 모듈은 하나의 일에 집중하고 변경 이유가 하나다. 응집이 낮으면 관련 없는 기능들이 뒤섞여, 한 곳을 고치면 다른 일이 부서진다.
Page-Jones과 Myers가 잡은 응집 단계. 낮음(나쁨)에서 높음(좋음)으로:
| 응집 단계 | 설명 | 예 |
|---|---|---|
| 우연적(coincidental) | 아무 관련 없는 코드를 한 곳에 | 로깅·계산·파싱을 한데 묶은 "유틸리티" |
| 논리적(logical) | 비슷한 '종류'지만 서로 무관 | 입력·출력·에러 처리를 한 모듈에 |
| 시간적(temporal) | 같은 '시점'에 실행돼서 묶임 | startup()에 DB연결·캐시웜업·로깅초기화 전부 |
| 절차적(procedural) | 같은 '순서'로 실행돼서 묶임 | 단계 A→B→C가 한 함수에 |
| 통신적(communicational) | 같은 데이터를 다룸 | 같은 고객 객체를 조회·수정·삭제 |
| 순차적(sequential) | 한 출력이 다음 입력 | A의 결과가 B의 입력 |
| 기능적(functional) | 하나의 잘 정의된 일만 | 이상적 |
아래로 갈수록 좋다. "한 모듈은 하나의 일만(기능적 응집)"이 목표다. 낮은 응집의 전형이 god object다. Order 클래스가 결제·재고·배송·할인·영수증까지 떠안으면 변경 이유가 다섯 개다. 결제 규칙이 바뀌어도, 배송 정책이 바뀌어도, 할인 로직이 바뀌어도 수정된다. 변경 이유가 여럿이면, 그 중 누군가는 다른 맥락에서 깨뜨린다. 응집이 낮은 모듈은 결합도를 부른다 — 모든 걸 아는 객체에 모두가 의존하니까.
결합과 응집은 한 쌍이다
이 두 개념은 따로 놀지 않는다. 높은 응집이 느슨한 결합을 낳고, 느슨한 결합은 다시 높은 응집을 지원한다. 역도 성립한다 — 낮은 응집은 강한 결합을 부른다.
한 모듈이 하나의 일(높은 응집)에만 집중하면, 다른 모듈과 나눠 가질 책임이 적어진다. 모듈 경계가 일의 경계와 일치하면 변경은 한 모듈 안에 머문다. 반면 한 모듈이 여러 일(낮은 응집)을 뒤섞으면, 그 내부 상태를 여러 곳이 필요로 하고, 상태를 공유하느라 결합이 퍼진다. 그래서 아키텍처의 가장 오래된 구호는 "느슨한 결합·높은 응집"이다. 50년이 넘었지만 여전히 유효하다 — 마이크로서비스의 경계, DDD의 바운디드 컨텍스트, 헥사고날의 포트 분리가 모두 이 두 힘을 다루는 다른 이름이다.
코네이선스 — 더 정밀한 결합 측정
결합의 종류(자료·제어·공통)는 모듈 수준에서 유용하지만, 객체지향에선 더 정밀한 도구가 있다. Page-Jones가 제안하고 Jim Weirich가 정리한 코네이선스(connascence, "함께 태어남")다 — "A를 어떤 방식으로 바꾸면 B도 같이 바꿔야 한다면, A와 B는 그 방식에 대해 코네이선스하다"는 관점. 레고 비유가 담지 못했던 "의미"나 "실행 순서" 같은 비물리적 얽힘을 이 개념이 다룬다.
코네이선스는 결합을 정적·동적 두 축으로 더 잘게 쪼갠다. 정적 코네이선스는 이름·타입·의미·위치·알고리즘 수준이다(같은 이름을 쓴다, 같은 타입이다, status=1을 둘 다 "활성"으로 해석한다, 같은 인자 순서다). 동적 코네이선스는 실행 순서·시간·값·동기 수준이다(A가 B보다 먼저 실행돼야 한다, 정해진 시간 안에 끝나야 한다, 양쪽 타임아웃이 일치해야 한다, 동시에 일어나야 한다). 동적이 더 강하다.
코네이선스를 다루는 규칙은 단순하다. 가까운 곳(같은 모듈 안)에선 강한 코네이선스가 허용된다. 멀리 떨어질수록(모듈·서비스 간) 코네이선스는 약해야 한다. 같은 클래스 안의 두 메서드가 강하게 엮인 건 괜찮다 — 그건 응집이다. 하지만 서로 다른 서비스가 실행 순서나 값에 의존하면, 한쪽을 바꾸거나 장애가 나면 다른 쪽이 깨진다. 먼 거리의 강한 코네이선스가 아키텍처를 부서지게 만든다. 앞의 로그 사례도 "의미의 코네이선스" — 포맷 문자열의 의미에 대한 암묵 합의 — 가 먼 거리에 퍼져 있던 사례다.
결합을 줄이는 실천 — 안정된 추상에 의존을 옮긴다
결합을 줄이는 가장 강력한 수단은 추상화다. A가 B의 구체 클래스에 의존하면 강결합이지만, A가 인터페이스에 의존하고 B가 그것을 구현하면, A는 B의 구현 변경에 흔들리지 않는다.
// 강결합 — Checkout이 특정 결제 구현에 직접 의존
class Checkout {
private final StripeGateway gateway = new StripeGateway(); // 구체 클래스 고정
}
// 느슨한 결합 — 추상(인터페이스)에 의존, 구현은 주입
class Checkout {
private final PaymentGateway gateway; // 인터페이스
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
}
결합을 줄인다는 건 "아무 의존도 없다"가 아니다. 흔들리지 않는 것에 의존을 옮기는 일이다. 이 차이를 놓치면 "느슨한 결합"을 과잉 추구하다가 모든 걸 인터페이스로 감싸는 쓸데없는 간접층만 늘린다. 의존을 없애는 게 아니라, 안정된 추상에 의존을 옮겨 변경 여파를 좁히는 것이 본질이다.
설계 사례 — 결합도를 낮추는 단계적 리팩터링
한 서비스에서 Checkout이 결제(PaymentGateway), 재고(InventoryService), 알림(EmailService)을 직접 호출하고, 결제 구현체(StripeGateway)를 하드코딩한다고 하자. 결제를 다른 PG사로 바꾸려면 Checkout 코드를 열어야 한다 — 강결합이다.
flowchart LR
subgraph Before["Before (강결합)"]
CB["Checkout"] --> PG["StripeGateway<br/>(구체 클래스)"]
CB --> IS["InventoryService"]
CB --> ES["EmailService"]
end
subgraph After["After (느슨한 결합)"]
CA["Checkout"] --> PI["PaymentGateway<br/>(인터페이스)"]
CA --> IR["InventoryPort<br/>(인터페이스)"]
PI -. 구현 .-> SA["StripeGateway"]
PI -. 구현 .-> TA["TossGateway"]
end
Before에서 Checkout이 StripeGateway라는 구체 클래스를 직접 생성하고 호출한다. Stripe에서 Toss로 바꾸면 Checkout 코드를 고쳐야 한다.
// Before — 강결합: Checkout이 구체 클래스를 직접 생성
class Checkout {
private final StripeGateway gateway = new StripeGateway(); // 구체 클래스 고정
void process(Order order) {
gateway.charge(order.total()); // Stripe에 직접 의존
inventory.decrease(order);
email.send(order.customer(), "주문 완료");
}
}
After에서 Checkout은 PaymentGateway 인터페이스에 의존하고, 구현체는 외부에서 주입받는다. Stripe에서 Toss로 바꿀 때 Checkout은 손대지 않는다 — 주입하는 설정만 바꾼다.
// After — 느슨한 결합: 인터페이스에 의존, 구현은 주입
class Checkout {
private final PaymentGateway gateway; // 인터페이스
private final InventoryPort inventory; // 인터페이스
Checkout(PaymentGateway gateway, InventoryPort inventory) {
this.gateway = gateway; // StripeGateway든 TossGateway든 모름
this.inventory = inventory;
}
void process(Order order) {
gateway.charge(order.total());
inventory.decrease(order);
}
}
// 결제 교체 — Checkout 코드 수정 없이 주입만 교체
// 기존: new Checkout(new StripeGateway(), ...)
// 변경: new Checkout(new TossGateway(), ...)
이 리팩터링에서 주의할 점 — 인터페이스를 너무 많이 만들면 과잉이 된다. EmailService를 굳이 인터페이스로 빼지 않아도, 알림 교체 가능성이 낮다면 그대로 써도 된다. "결합을 줄인다"고 모든 의존에 인터페이스를 끼우면 간접층만 늘어난다 — 흔들릴 가능성이 높은 곳(결제 PG사, 외부 API)만 추상화하고, 안정적인 곳은 구체 클래스에 직접 의존해도 된다. 이 균형이 "느슨한 결합"과 "과잉 설계"를 가른다.
접착제를 녹이는 일
"이거 고치려면 저것도 고쳐야 해"라는 말을 들으면, 그건 분량의 문제가 아니라 결합의 문제다. 그때 물어야 할 질문은 "왜 이 둘이 묶여 있지"이고, 답은 대개 "모듈이 하나의 일에 집중하지 못해서(낮은 응집)" 또는 "구체 구현이 아니라 추상에 의존하지 않아서(강결합)"다. 아키텍처의 상당수는 이 두 질문에 답하는 일이다. 접착제로 붙인 모형을 뜯는 대신, 블록이 분리되도록 다시 설계하는 — 그것이 결합을 줄이는 일의 본질이다.
참고
- Page-Jones, Meilir —
(Yourdon Press, 1980) (결합·응집 분류 원전) — 접근 2026-07-15 - Myers, Glenford — <Composite/Structured Design> (Van Nostrand Reinhold, 1978)
- Page-Jones, Meilir —
(Addison-Wesley, 2000) (코네이선스) - Weirich, Jim — "Connascence" (강연, 코네이선스 현대적 정리) — 접근 2026-07-15
- Newman, Sam —
(2nd ed, O'Reilly, 2021), Ch.2
'Architecture & Design > Software Architecture' 카테고리의 다른 글
| Foundations - 06. Conway's Law와 팀 토폴로지 (0) | 2026.07.15 |
|---|---|
| Foundations - 05. 기술 부채와 의사결정 (0) | 2026.07.15 |
| Foundations - 04. 아키텍처 문서화 (0) | 2026.07.15 |
| Foundations - 02. 품질 속성 (1) | 2026.07.15 |
| Foundations - 01. 아키텍처란 무엇인가 (0) | 2026.07.15 |