응집도
고친 사람 github-actions[bot]
응집도는 한 코드 덩이 안에 든 것들이 한 가지 일에 얼마나 모여 있는지를 잽니다. 함께 바뀌는 코드끼리 한곳에 모여 있을수록 응집도가 높습니다. 응집도가 높은 코드는 무엇 하나를 고칠 때 한곳만 열면 됩니다. 코드를 어떻게 나눌지 정할 때, 코드 덩이끼리 얼마나 서로 기대는지를 재는 결합도와 함께 따지는 잣대입니다.
쉽고 빠른 이해
응집도는 한 코드 덩이 안의 것들이 한 가지 일에 얼마나 모여 있는지를 잽니다. 결제 클래스 안에 결제 코드만 있으면 높습니다. 회원 가입과 메일 발송 코드가 섞여 있으면 낮습니다.
응집도가 낮으면 서로 상관없는 이유로 같은 파일을 자꾸 고치게 됩니다. 메일 문구 하나 바꾸려다 결제 코드를 건드릴 수 있습니다.
어떻게 높이나:
- 함께 바뀌는 코드끼리 한 덩이로 모읍니다
- 따로 바뀌는 코드는 다른 덩이로 떼어 냅니다
- 덩이마다 한 가지 일을 뜻하는 이름을 붙일 수 있는지 봅니다
대가도 있습니다. 너무 잘게 쪼개면 덩이끼리 서로를 부르는 일이 늘어 흐름을 따라가기 어려워집니다.
상세
이 절은 응집도가 무엇을 재는지, 낮으면 무엇이 곤란한지, 어떻게 가늠하고 높이는지를 봅니다. 쇼핑몰의 주문 코드를 줄곧 예로 씁니다. 앞쪽 소절은 클래스 하나를 봅니다. 뒤쪽 소절은 패키지와 서버로 넓혀 갑니다.
부엌 찬장의 한 칸을 떠올리면 가깝습니다. 라면 칸에는 냄비와 집게와 스프 봉지를 자르는 가위가 함께 들어 있습니다. 라면을 끓일 때는 그 칸 하나만 엽니다. 칸의 이름도 「라면 칸」 한 마디로 부를 수 있습니다.
먼저 재는 대상부터 정합니다. 프로그램을 나눠 이름 붙인 코드 덩이 하나를 모듈이라고 부릅니다. 클래스 하나일 수도 있고 패키지나 서버 하나일 수도 있습니다. 덩이마다 따로 이해하고 따로 고치려고 이렇게 나눕니다.
응집도는 한 모듈 안에 든 것들이 한 가지 일에 얼마나 모여 있는지입니다. 결제 모듈 안에 결제 승인과 취소와 환불 코드만 있으면 응집도가 높습니다. 같은 모듈에 회원 가입과 메일 발송 코드가 섞여 있으면 응집도가 낮습니다.
응집도는 대개 「높다」「낮다」로 견주어 말합니다. 쉽게 가늠하는 방법은 모듈이 하는 일을 한
문장으로 말해 보는 것입니다. 「주문을 받고, 메일도 보내고, 이미지도 줄인다」처럼 「그리고」가
붙으면 낮은 쪽입니다. 이름이 Utils·Helper·Manager 처럼 무엇이든 담을 수 있는 말이면
그것도 낮다는 신호로 봅니다.
한 클래스에 모인 남남
이 소절은 응집도가 낮을 때 무엇이 곤란한지를 클래스 하나로 봅니다. 주문을 처리한다는
OrderService 에 메서드 넷이 들어 있습니다.
class OrderService {
void placeOrder(Order o) { ... }
int totalPrice(Order o) { ... }
void sendPromoMail(User u) { ... }
byte[] resize(byte[] img) { ... }
}
넷 가운데 주문 일은 앞의 둘뿐입니다. 홍보 메일 보내기와 이미지 줄이기는 둘 곳을 못 찾아 여기 들어와 있습니다.
이 클래스는 서로 상관없는 이유로 바뀝니다. 가격 정책이 바뀌어도, 메일 문구가 바뀌어도, 이미지 크기 규칙이 바뀌어도 이 파일을 엽니다. 세 사람이 서로 다른 일로 한 파일을 고치면 서로의 수정이 부딪히는 병합 충돌이 잦아집니다.
읽는 사람도 곤란합니다. 주문 규칙을 보려고 연 파일에서 메일 코드까지 훑어야 합니다. 클래스 이름만 보고는 이미지 코드가 여기 있으리라고 짐작하기 어렵습니다.
가져다 쓰기도 무거워집니다. 다른 곳에서 이미지 줄이기만 쓰고 싶어도 주문 클래스 전체를 끌어와야 합니다. 이 클래스가 기대는 주문 저장소와 메일 서버까지 함께 따라옵니다.
고치는 방법은 따로 바뀌는 것들을 떼어 내는 것입니다. 주문 일 둘만 남기고 나머지를 각자의 클래스로 옮깁니다.
class OrderService {
void placeOrder(Order o) { ... }
int totalPrice(Order o) { ... }
}
class PromoMailer {
void sendPromoMail(User u) { ... }
}
class ImageResizer {
byte[] resize(byte[] img) { ... }
}
이제 클래스마다 하는 일을 한 문장으로 말할 수 있습니다. 메일 문구가 바뀌면 PromoMailer 만
엽니다. 클래스가 바뀌는 이유를 하나만 남기라는 설계 원칙을 단일 책임 원칙이라고 부릅니다.
응집도를 높이라는 말을 클래스 단위로 옮긴 원칙입니다.
응집의 단계
모든 묶음이 같은 무게는 아닙니다. 한 모듈 안의 것들이 무슨 까닭으로 함께 있느냐에 따라 흔히 일곱 단계로 나눕니다. 약한 쪽부터 표로 봅니다.
| 단계 | 함께 있는 까닭 | 예 |
|---|---|---|
| 우연적 응집 | 까닭이 없다. 둘 곳이 없어 모였다 | 아무 함수나 모아 둔 Utils 클래스 |
| 논리적 응집 | 하는 일의 종류가 비슷하다 | 파일·네트워크·키보드 입력을 인자 하나로 골라 처리하는 함수 |
| 시간적 응집 | 같은 때에 실행된다 | 서버가 뜰 때 설정 읽기·캐시 비우기·로그 열기를 몰아 하는 init() |
| 절차적 응집 | 정해진 순서대로 이어 실행된다 | 로그인 확인 뒤 장바구니를 불러오고 광고를 띄우는 함수 |
| 교환적 응집 | 같은 데이터를 다룬다 | 주문 한 건을 받아 저장도 하고 영수증도 찍는 함수 |
| 순차적 응집 | 앞 단계의 출력이 다음 단계의 입력이 된다 | 이미지를 읽어 줄인 결과를 받아 압축하는 함수 |
| 기능적 응집 | 모두가 한 가지 일을 이루려고 있다 | 주문 금액만 셈하는 함수 |
아래로 갈수록 함께 있는 까닭이 한 가지 일에 가까워집니다. 앞 소절의 OrderService 는 맨 위의
우연적 응집에 가깝습니다. 교환적 응집은 통신적 응집이라고도 부릅니다.
논리적 응집은 이름만 보면 그럴듯해서 헷갈립니다. 입력을 받는 함수가 인자로 「파일」「네트워크」 「키보드」를 받아 안에서 갈라 처리하면, 하는 일이 「입력 받기」 하나처럼 보입니다. 하지만 세 갈래는 서로 다른 코드입니다. 부르는 쪽은 매번 그중 하나만 씁니다.
시간적 응집은 서버를 띄우는 코드에서 흔히 생깁니다. init() 안의 설정 읽기와 캐시 비우기는
같은 때에 돌 뿐 서로 상관이 없습니다. 캐시 규칙이 바뀌어도 이 함수를 고칩니다. 설정 형식이
바뀌어도 이 함수를 고칩니다.
단계 사이의 경계가 늘 또렷하지는 않습니다. 실무에서는 단계 이름을 맞히는 데보다 「이 모듈이 바뀌는 이유가 몇 가지인가」를 묻는 데 이 표를 씁니다.
메서드와 필드로 가늠하기
응집도를 숫자로 재려는 시도도 있습니다. 이 소절은 클래스 하나를 두고, 메서드들이 필드를 얼마나 함께 쓰는지로 응집도를 가늠하는 방법을 봅니다.
아래 Checkout 클래스에는 필드 둘과 메서드 셋이 있습니다. 메서드마다 어느 필드를 쓰는지 주석으로
붙였습니다.
class Checkout {
List<Item> cart;
MailClient mail;
int total() { ... } // cart
void add(Item i) { ... } // cart
void notifyUser() { ... } // mail
}
total 과 add 는 cart 를 함께 씁니다. notifyUser 는 mail 만 쓰고 cart 에는 손대지
않습니다. 메서드와 필드를 선으로 이으면 서로 닿지 않는 두 무리, 곧 장바구니 무리와 메일 무리로
갈립니다.
flowchart TD
subgraph A["장바구니 무리"]
T["total"] --> C["cart"]
D["add"] --> C
end
subgraph B["메일 무리"]
N["notifyUser"] --> M["mail"]
end
A ~~~ B
선으로 이어지지 않는 무리가 둘이면 이 클래스는 두 가지 일을 하고 있다는 신호입니다. 무리마다 클래스를 하나씩 떼어 내면 응집도가 오릅니다. 여기서는 장바구니 클래스와 메일 클래스로 나뉩니다.
이 생각을 수치로 만든 지표가 LCOM(Lack of Cohesion of Methods, 메서드 응집 부족)입니다. 이름대로 응집이 모자란 정도를 재므로 값이 클수록 응집도가 낮습니다. 셈하는 방식이 여럿이라서 같은 클래스라도 방식마다 값이 다르게 나옵니다. 코드를 실행하지 않고 읽어서 검사하는 정적 분석 도구가 이 값을 계산해 보여 주기도 합니다.
패키지와 서버로 넓히기
응집도는 클래스에만 쓰는 말이 아닙니다. 모듈을 패키지나 서버로 잡아도 같은 물음을 던집니다. 이 소절은 백엔드 코드를 패키지로 나누는 두 방식을 견줍니다. 그다음 서버 단위로 한 번 더 넓힙니다.
첫째 방식은 계층별로 나누는 것입니다. 요청을 받는 코드는 controller, 업무 규칙은
service, 데이터베이스를 다루는 코드는 repository 패키지에 넣습니다.
둘째 방식은 기능별로 나누는 것입니다. order·member·product 패키지를
둡니다. 주문에 필요한 코드는 전부 order 안에 넣습니다.
두 방식을 주문 규칙 하나가 바뀌는 경우에 대 보면 이렇습니다.
| 계층별로 나눔 | 기능별로 나눔 | |
|---|---|---|
| 한 패키지에 든 것 | 여러 기능의 같은 층 코드 | 한 기능의 모든 층 코드 |
| 주문 규칙이 바뀌면 여는 패키지 | controller·service·repository 셋 |
order 하나 |
| 한눈에 보기 쉬운 것 | 층마다 지킬 규칙 | 한 기능의 흐름 |
계층별 패키지는 같은 종류의 코드를 모았다는 점에서 앞 표의 논리적 응집과 닮았습니다. 기능별 패키지는 함께 바뀌는 코드를 모았다는 점에서 기능적 응집 쪽에 섭니다. 어느 쪽을 고를지는 무엇이 더 자주 함께 바뀌느냐로 정합니다.
서버로 넓혀도 같습니다. 마이크로서비스처럼 기능을 여러 서버로 나누는 경우를 봅니다. 늘 함께 바뀌는 기능을 한 서버에 두면, 기능 하나를 고칠 때 서버 하나만 새로 배포합니다. 함께 바뀌는 기능이 두 서버로 갈라져 있으면 고칠 때마다 두 서버를 맞춰 배포해야 합니다.
결합도와 한 쌍인 까닭
응집도는 거의 언제나 결합도와 함께 불립니다. 이 소절은 둘이 어떻게 맞물리는지, 그리고 응집도를 높이려다 지나치는 경우를 봅니다.
결합도는 두 모듈이 서로에게 기대는 정도입니다. 한 모듈을 고칠 때 다른 모듈까지 따라 고쳐야 하는 정도로 잽니다. 응집도가 모듈 안을 본다면 결합도는 모듈 사이를 봅니다.
둘은 한 결정의 양면입니다. 함께 바뀌는 코드를 한 모듈에 모으면 응집도가 오릅니다. 그러면 무엇 하나를 고칠 때 모듈 경계를 넘을 일이 줄어 결합도가 내려갑니다. 설계 원칙으로는 흔히 「응집도는 높게, 결합도는 낮게」라는 한 줄로 부릅니다.
거꾸로 가는 경우도 있습니다. 응집도를 높이겠다고 모듈을 끝없이 잘게 쪼개면, 한 가지 일을 이루는 코드가 여러 모듈로 흩어집니다. 그 일을 하려고 모듈끼리 서로를 부르는 일이 늘어납니다. 모듈 하나하나는 작아도 모듈 사이의 결합도가 오릅니다.
그래서 나누는 기준은 크기가 아니라 「함께 바뀌는가」입니다. 늘 함께 바뀌는 코드는 한 모듈에 둡니다. 따로 바뀌는 코드는 떼어 놓습니다.
경계를 서둘러 긋지 않는 때도 있습니다. 한 번 돌리고 버릴 스크립트는 나눠 봐야 얻을 것이 적습니다. 요구가 아직 굳지 않은 초기 코드에서는 무엇이 함께 바뀌는지부터 모릅니다. 그럴 때는 한동안 한 덩이로 둡니다. 바뀌는 모양이 보인 뒤에 가릅니다. 동작은 두고 코드 구조만 고치는 리팩터링이 이때 하는 일입니다.
관련 항목
응집도와 한 쌍으로 모듈을 재는 척도
결합도 · LCOM · 팬인 · 팬아웃 · 순환 복잡도 · 의존성 그래프
응집도를 나누는 단계
우연적 응집 · 논리적 응집 · 시간적 응집 · 절차적 응집 · 교환적 응집 · 순차적 응집 · 기능적 응집
응집도를 높이는 설계 원칙
단일 책임 원칙 · 관심사 분리 · 정보 은닉 · 캡슐화 · SOLID · 공통 폐쇄 원칙 · 도메인 주도 설계 · 바운디드 컨텍스트
응집도를 높이는 리팩터링 기법
리팩터링 · 클래스 추출 · 메서드 추출 · 메서드 이동 · 모듈 분리
응집도가 낮을 때 드러나는 문제
신 객체 · 기능 편애 · 산탄총 수술 · 병합 충돌 · 파급 효과 · 유틸리티 클래스
응집도를 재는 단위가 되는 코드 덩이
모듈 · 클래스 · 패키지 · 계층 · 서비스 · 마이크로서비스 · 모듈러 모놀리스
응집도를 품질 기준으로 삼는 설계 분야
소프트웨어 아키텍처 · 소프트웨어 설계 · 객체 지향 · 구조적 설계 · 정적 분석 · 코드 품질
다른 이름: cohesion · 모듈 응집도