결합도
고친 사람 github-actions[bot]
결합도는 코드 덩이끼리 서로에게 얼마나 기대는지를 잽니다. 한쪽을 고칠 때 다른 쪽까지 따라 고쳐야 하는 정도로 봅니다. 결합도가 낮은 코드는 한 덩이를 고치거나 갈아 끼워도 나머지가 멀쩡합니다. 한 프로그램 안의 클래스 사이에도, 네트워크로 이어진 서버 사이에도 이 말을 씁니다.
쉽고 빠른 이해
결합도는 코드 두 덩이가 서로에게 얼마나 기대는지를 잽니다. 주문 코드가 결제 코드 속의 상태 값을 바로 읽고 있으면, 결제 쪽이 그 값을 손볼 때 주문 쪽도 고쳐야 합니다.
결합도가 높으면 한 곳을 고칠 때 여러 곳이 같이 깨집니다. 한 덩이만 떼어 시험하기도 어렵습니다.
어떻게 낮추나:
- 상대의 속 대신 상대가 내놓은 기능만 부릅니다
- 넘기는 값을 꼭 필요한 것으로 줄입니다
- 서버끼리는 바로 부르지 않고 큐를 사이에 둡니다
대가도 있습니다. 거쳐 가는 단계가 늘어 코드의 흐름을 따라가기 어려워집니다. 큐를 두면 결과를 바로 받지 못합니다.
늘 함께 바뀌는 두 모듈은 떼지 말고 하나로 합칩니다.
상세
이 절은 결합도가 무엇을 재는지, 왜 낮추려 하는지, 어떻게 낮추는지를 봅니다. 주문 모듈과 결제 모듈 한 쌍을 줄곧 예로 씁니다. 앞쪽 소절은 한 프로그램 안의 코드를 보고, 뒤쪽 소절은 네트워크로 이어진 서버 둘로 옮겨 갑니다.
이인삼각 달리기를 떠올리면 가깝습니다. 두 사람의 발목이 끈으로 묶여 있어서 한 사람이 보폭을 바꾸면 다른 사람도 맞춰야 합니다. 한 사람이 넘어지면 둘 다 넘어집니다.
먼저 재는 대상부터 정합니다. 프로그램을 나눠 이름 붙인 코드 덩이 하나를 모듈이라고 부릅니다. 클래스 하나일 수도 있고 패키지나 서버 하나일 수도 있습니다. 모듈마다 따로 이해하고 따로 고치려고 이렇게 나눕니다.
결합도는 두 모듈이 서로에게 기대는 정도입니다. 한 모듈을 고칠 때 다른 모듈까지 따라 고쳐야 하는 정도로 잽니다. 주문 모듈이 결제 모듈 안쪽의 값을 바로 읽고 있으면, 결제 쪽이 그 값을 손볼 때 주문 쪽도 고쳐야 합니다. 이 두 모듈은 결합도가 높습니다.
결합도는 대개 「높다」「낮다」로 견주어 말합니다. 낮은 쪽을 느슨한 결합(loose coupling), 높은 쪽을 강한 결합(tight coupling)이라고 부릅니다. 한 모듈이 기대는 다른 모듈의 수를 세어 수치로 나타내기도 합니다.
고칠 때 번지는 파급
결합도가 높을 때 무엇이 곤란한지를 코드 한 조각으로 봅니다. 주문 모듈이 결제 상태를 확인하고 배송을 시작하는 Java 코드입니다.
// 결제 모듈 속 필드를 바로 읽는다
if (payment.status == 2) { // 2는 승인
ship(order);
}
주문 모듈은 결제 모듈이 상태를 숫자로 담는다는 것과 승인이 2라는 것까지 알고 있습니다. 결제 쪽이 상태를 숫자 대신 이름으로 담도록 바꾸면 이 줄은 컴파일되지 않습니다. 더 곤란한 쪽은 승인 번호만 3으로 바뀌는 경우입니다. 컴파일은 됩니다. 그런데 배송이 멈춥니다.
같은 필드를 정산 모듈과 알림 모듈도 읽고 있으면, 결제 쪽의 한 줄 수정이 세 모듈로 번집니다. 한 곳의 변경이 다른 곳으로 퍼지는 이 현상을 파급 효과라고 합니다. 결합도가 높을수록 파급 효과가 멀리 갑니다.
결제 모듈이 승인 여부를 묻는 메서드를 내놓고, 주문 모듈은 그것만 부르면 이야기가 달라집니다.
if (payment.isApproved()) { // 승인됐으면 true
ship(order);
}
이제 주문 모듈이 아는 것은 isApproved 라는 이름 하나입니다. 결제 쪽이 상태를 숫자로 담든
이름으로 담든 메서드 안쪽만 고치면 됩니다. 이렇게 속을 감추고 정해진 통로만 여는 일을
캡슐화라고 부릅니다.
그림으로 보면 수정이 번지는 폭이 드러납니다. 위는 세 모듈이 필드를 바로 읽을 때입니다. 아래는 메서드만 부를 때입니다.
flowchart TD
subgraph A["필드를 바로 읽을 때"]
P1["결제 모듈 · 상태 담는 법을 바꿈"] --> O1["주문 모듈 고침"]
P1 --> S1["정산 모듈 고침"]
P1 --> N1["알림 모듈 고침"]
end
subgraph B["메서드만 부를 때"]
P2["결제 모듈 · 상태 담는 법을 바꿈"] --> M2["결제 모듈 안 메서드만 고침"]
end
A ~~~ B
곤란한 것이 파급 효과만은 아닙니다. 결합도가 높은 모듈은 하나만 떼어 시험하는 단위 테스트가 어렵습니다. 주문 모듈 하나를 시험하려 해도 결제 모듈을 진짜로 띄워야 하기 때문입니다.
결합의 단계
모든 결합이 같은 무게는 아닙니다. 두 모듈이 무엇을 통해 이어져 있느냐에 따라 흔히 다섯 단계로 나눕니다. 강한 쪽부터 표로 봅니다.
| 단계 | 두 모듈이 나눠 갖는 것 | 예 |
|---|---|---|
| 내용 결합 | 상대의 안쪽 데이터나 코드 | 남의 객체 필드를 바로 읽고 쓴다 |
| 공통 결합 | 여러 모듈이 함께 고치는 전역 데이터 | 전역 변수 하나를 여러 모듈이 바꾼다 |
| 제어 결합 | 상대의 동작을 고르는 신호 | 넘긴 참·거짓 값 하나가 상대 안쪽의 갈림길을 정한다 |
| 스탬프 결합 | 필요한 것보다 큰 자료 | 우편번호만 필요한데 회원 객체 전체를 넘긴다 |
| 자료 결합 | 꼭 필요한 값만 | 우편번호 문자열 하나만 넘긴다 |
위로 갈수록 상대의 속을 깊이 압니다. 앞 소절의 payment.status 를 바로 읽는 코드가 맨 위의
내용 결합입니다. 공통 결합과 제어 결합 사이에 외부 결합을 한 단계 더 넣어 여섯으로 나누기도
합니다. 외부 결합은 바깥 파일 형식이나 장치 규약처럼 프로그램 밖에서 정한 것을 두 모듈이 함께
따르는 결합입니다.
이름만 보고 뜻이 안 잡히는 둘을 풀어 봅니다. 제어 결합은 넘기는 값이 데이터가 아니라 명령일
때입니다. print(doc, true) 에서 true 가 「요약본만 뽑아라」라는 뜻이면, 부르는 쪽은 상대
안쪽에 그런 갈림길이 있다는 것까지 압니다.
스탬프 결합은 필요한 것보다 큰 자료를 넘길 때입니다. 배송비를 셈하는 모듈에는 우편번호만 있으면 됩니다. 그런데 회원 객체 전체를 넘기면, 회원 객체의 모양이 바뀔 때 배송비 모듈도 영향을 받습니다. 우편번호 하나만 넘기도록 고치면 맨 아래 자료 결합으로 내려옵니다.
설계할 때는 두 모듈을 잇는 통로를 되도록 표의 아래쪽으로 끌어내립니다. 아래로 내려갈수록 상대가 바뀔 때 함께 깨지는 곳이 줄어듭니다.
서버 사이의 결합
결합도는 한 프로그램 안에서만 쓰는 말이 아닙니다. 네트워크로 이어진 서버 둘 사이에도 같은 물음을 던집니다. 이 소절은 주문 서버가 결제 서버를 부르는 경우를 봅니다.
주문 서버가 결제 서버를 바로 부르고 답을 기다리면, 두 서버는 같은 순간에 둘 다 살아 있어야 합니다. 결제 서버가 잠깐 내려가 있으면 주문도 실패합니다. 이렇게 두 쪽이 시간을 맞춰야 하는 묶임을 시간 결합이라고 부릅니다.
이 묶임은 코드를 아무리 잘 감춰도 남습니다. 주문 서버가 결제 서버의 안쪽을 하나도 모르더라도, 부르는 순간 상대가 답해야 한다는 조건은 바뀌지 않습니다.
두 서버 사이에 큐를 두면 이 묶임이 풀립니다. 큐는 넣은 순서대로 꺼내 가도록 메시지를 쌓아 두는 곳입니다. 주문 서버는 「이 주문을 결제해 달라」는 메시지를 큐에 넣고 바로 제 일로 돌아갑니다. 결제 서버는 준비되는 대로 꺼내 처리합니다.
sequenceDiagram
participant 주문 as 주문 서버
participant 큐
participant 결제 as 결제 서버
주문->>큐: 결제 요청 메시지를 넣는다
큐-->>주문: 받았다
Note over 결제: 이때 내려가 있어도 된다
결제->>큐: 다시 올라와 꺼낸다
큐-->>결제: 결제 요청 메시지
그림에서 주문 서버는 결제 서버와 한 번도 직접 말하지 않습니다. 결제 서버가 내려가 있는 동안에도 주문 서버는 주문을 계속 받습니다.
그래도 결합이 0이 되지는 않습니다. 두 서버는 메시지의 모양을 함께 알아야 합니다. 주문 서버가 메시지에 넣는 필드 이름을 바꾸면 결제 서버도 고쳐야 합니다. 큐가 끊는 것은 시간 결합입니다. 메시지 모양은 여전히 두 서버가 함께 알아야 합니다.
결합도를 낮추는 수단
결합도를 낮추는 방법은 대개 「상대에 대해 아는 것을 줄인다」 하나로 모입니다. 앞에서 본 캡슐화와 큐도 그 하나입니다. 이 소절은 흔히 쓰는 수단 셋을 더 보고 표로 모읍니다.
인터페이스는 무엇을 해 주는지만 적고 어떻게 하는지는 비워 둔 약속입니다. 주문 모듈이 「결제하기」라는 약속만 알면 뒤에서 어느 결제 회사의 코드가 도는지는 몰라도 됩니다. 결제 회사를 바꿀 때 주문 모듈은 손대지 않습니다.
의존성 주입은 모듈이 쓸 상대를 스스로 만들지 않고 바깥에서 건네받는 방식입니다. 주문 모듈 안에서 결제 객체를 직접 만들면 그 구체 클래스의 이름을 알아야 합니다. 바깥에서 건네받으면 인터페이스만 알면 됩니다.
이벤트 발행은 「주문이 들어왔다」는 사실만 알리고 끝내는 방식입니다. 그 소식을 듣고 결제를 하든 메일을 보내든 알리는 쪽은 모릅니다. 듣는 쪽이 하나 늘어도 알리는 쪽은 고치지 않습니다.
아래 표는 수단마다 부르는 쪽이 무엇을 몰라도 되게 해 주는지 견줍니다.
| 수단 | 부르는 쪽이 몰라도 되는 것 |
|---|---|
| 캡슐화 | 상대의 필드와 안쪽 생김새 |
| 인터페이스 | 상대가 어느 클래스인지 |
| 의존성 주입 | 상대를 누가 어떻게 만드는지 |
| 이벤트 발행 | 누가 이 소식을 받아 처리하는지 |
| 큐 | 상대가 지금 살아 있는지 |
표의 오른쪽 칸은 부르는 쪽이 더는 알지 않아도 되는 것입니다. 수단을 고를 때는 두 모듈 사이에서 무엇이 가장 자주 바뀌는지를 먼저 봅니다.
낮추는 대가
결합도를 낮추는 데는 대가가 따릅니다. 무엇을 치르는지, 그리고 언제 굳이 낮추지 않는지를 차례로 봅니다.
첫째 대가는 거쳐 가는 단계입니다. 인터페이스와 이벤트를 끼울 때마다 코드 한 줄이 어디로 가는지 따라가기 어려워집니다. 「이 일을 누가 처리하나」를 찾으려고 파일 여러 개를 건너야 합니다.
둘째 대가는 큐를 둘 때 생깁니다. 주문 서버는 결제가 끝났는지 바로 알 수 없습니다. 결과를 나중에 따로 받는 길을 새로 만들어야 합니다.
결합도를 0으로 만들 수는 없습니다. 함께 일하는 두 모듈은 적어도 상대에게 무엇을 부탁할지는 알아야 합니다. 목표는 결합을 없애는 것이 아니라 꼭 알아야 할 것만 남기는 것입니다.
그래서 굳이 떼지 않는 경우도 있습니다. 두 모듈이 늘 함께 바뀐다면 사이에 인터페이스를 세워도 고칠 때마다 둘 다 고칩니다. 떼어 놓은 대가만 치르고 얻는 것이 없습니다. 그럴 때는 두 모듈을 하나로 합칩니다.
응집도와 짝을 이루는 까닭
결합도는 거의 언제나 응집도와 함께 불립니다. 결합도가 모듈 사이를 본다면 응집도는 모듈 안을 봅니다.
응집도는 한 모듈 안에 든 것들이 한 가지 일에 얼마나 모여 있는지를 나타냅니다. 결제 모듈 안에 결제 코드만 있으면 응집도가 높습니다. 결제 모듈에 회원 가입과 메일 발송 코드가 섞여 있으면 응집도가 낮습니다.
둘은 한 결정의 양면입니다. 함께 바뀌는 코드를 한 모듈에 모으면 응집도가 오릅니다. 그러면 모듈 경계를 넘나드는 부탁이 줄어 결합도가 내려갑니다. 설계 원칙으로는 흔히 「응집도는 높게, 결합도는 낮게」라고 한 줄로 줄여 부릅니다.
관련 항목
결합도를 나누는 단계
내용 결합 · 공통 결합 · 외부 결합 · 제어 결합 · 스탬프 결합 · 자료 결합 · 메시지 결합
결합도를 서버 사이로 넓힌 묶임
시간 결합 · 위치 결합 · 스키마 · 계약 · 하위 호환성
결합도와 한 쌍으로 모듈을 재는 척도
응집도 · 팬인 · 팬아웃 · 의존성 그래프 · 순환 의존성
결합도를 낮추는 설계 원칙
캡슐화 · 정보 은닉 · 관심사 분리 · 단일 책임 원칙 · 의존성 역전 원칙 · 인터페이스 분리 원칙 · 디미터 법칙 · SOLID
결합도를 낮추는 코드 장치
인터페이스 · 추상화 · 의존성 주입 · 제어의 역전 · 이벤트 · 콜백 · 어댑터 패턴 · 퍼사드 패턴 · 옵서버 패턴
서버 사이의 결합을 끊는 통신 방식
큐 · 메시지 큐 · 메시지 브로커 · 발행-구독 · 비동기 처리 · 이벤트 기반 아키텍처 · 작업 큐 · Amazon SQS
결합도가 높을 때 드러나는 문제
파급 효과 · 전역 변수 · 전역 상태 · 신 객체 · 기능 편애 · 분산 모놀리스 · 연쇄 장애
결합도를 품질 기준으로 삼는 아키텍처
소프트웨어 아키텍처 · 모듈 · 모놀리스 · 모듈러 모놀리스 · 마이크로서비스 · 레이어드 · 헥사고날 · 클린 아키텍처
결합도의 높낮이를 가리키는 다른 이름
느슨한 결합 · 강한 결합 · 디커플링 · 의존성
결합도 때문에 어려워지는 작업
다른 이름: coupling · 커플링 · 모듈 결합도