이미지 출처: https://en.wikipedia.org/wiki/Coupling_%28computer_programming%29
잘 작성된 소프트웨어는 높은 응집도와 낮은 결합도를 가진다. 응집도가 높고 결합도가 낮다는 것은, 소프트웨어의 각 모듈들이 유사한 작업을 하는 코드들로 구성되어 있고 각 모듈들간의 의존성이 적다는 것을 의미한다.
결합도는 의미적으로 명확하다. 의미하는 바를 짐작하는게 그리 어려운 일은 아니다. 다만 응집도는 조금 모호할 수 있다. 실제로도 응집도와 결합도의 차이점은 종종 모호하기도 하다. 이는 이 두 용어 뒤에있는 아이디어가 실제로 비슷하기 때문이다. 그럼에도 불구하고 이 둘은 차이가 있다.
결합도는 프로그램 모듈간 의존성의 정도를 나타낸다. 모듈간의 연결 강도를 의미하고, 모듈들이 서로 얼마나 상호의존적인지를 측정하는 척도다.
높은 결합도는 프로그램의 심각한 단점이다. 모듈의 로직을 이해하기 어렵고 수정하기 어렵게 만든다. 독립적인 테스트와 재사용이 어렵기 때문이다.
낮은 결합도(Low coupling) 반대로 잘 구조화되고 잘 설계된 시스템의 특징이며, 높은 응집도(high cohesion)와 결합될 때 전반적으로 좋은 가독성과 유지보수성을 제공한다.
응집도는 연관된 코드들이 얼마나 잘 모여있는가를 의미한다. 즉, 모듈 내부 요소들 간의 연관성 정도를 나타내는 척도이며, 이는 특정 프로그램 모듈이 수행하는 작업들이 서로 얼마나, 그리고 어떤 방식으로 연관되어 있는지를 나타낸다.
높은 응집도(High cohesion)를 가진 객체(서브시스템)는 책임들이 서로 잘 조화를 이루며, 과도한 양의 작업을 수행하지 않는 것으로 간주된다.
낮은 응집도(low cohesion)를 가진 클래스는 서로 연관성이 없는 많은 이질적인 기능이나 책임들을 수행한다.
응집도와 결합도에는 이를 형성하는 성격에 따라 유형이 분류될 수 있다.
출처: https://www.javatpoint.com/software-engineering-coupling-and-cohesion
결합도의 유형은 다음과 같은 것들이 있다:
결합도 유형 | 설명 | 영향 |
---|---|---|
내용 결합도 | 한 모듈이 다른 모듈의 내부 구현에 의존하거나 이를 변경함 (예: 다른 모듈의 로컬 데이터를 사용) | 두 번째 모듈의 동작이 변경되면 첫 번째 모듈을 다시 작성해야 함 |
공통 결합도 | 두 모듈이 공통 데이터를 사용함 (예: 전역 변수) | 공유 자원이 변경되면 이를 사용하는 모든 모듈의 변경이 필요함 |
외부 결합도 | 두 모듈이 외부에서 강제된 데이터 형식, 통신 프로토콜 등을 사용 | 일반적으로 외부 요소(도구, 장치 등)로 인해 발생 |
제어 결합도 | 한 모듈이 다른 모듈의 동작을 제어함 | 무엇을 어떻게 해야 하는지에 대한 정보가 전달됨 |
스탬프 결합도 | 모듈들이 동일한 데이터 구조를 사용하지만 각각 일부분만 사용 | 구조 변경 시 변경된 부분을 사용하지 않는 모듈까지 수정이 필요할 수 있음 |
데이터 결합도 | 모듈들이 매개변수 등을 통해 데이터를 공유 | 작은 기본 단위의 데이터만 모듈 간에 공유됨 |
메시지 결합도 | 모듈들이 오직 매개변수나 메시지 전달을 통해서만 통신 | 상태가 분산되어 있음 |
무결합 | 모듈들 간에 전혀 상호작용이 없음 | 모듈 간의 독립성이 높아짐 |
출처: https://botpenguin.com/glossary/cohesion-and-coupling
응집도의 유형은 다음과 같은 것들이 있다:
응집도 유형 | 설명 | 영향 |
---|---|---|
우연적 응집도 | 모듈의 부분들이 아무런 의미 없이 무작위로 그룹화됨 | 모듈이라는 것 자체 외에는 이들을 묶는 것이 없음 |
논리적 응집도 | 모듈의 부분들이 논리적으로 같은 문제와 관련됨 | 부분들의 성격은 서로 다를 수 있음 |
시간적 응집도 | 모듈의 부분들이 프로그램에서 보통 같은 시간에, 인접해서 사용됨 | 관련된 작업들이 시간적으로 묶여 있음 |
절차적 응집도 | 모듈의 부분들이 항상 특정한 순서로 사용됨 | 순서에 의존하여 작업이 수행됨 |
통신적 응집도 | 모듈의 부분들이 같은 데이터를 다룸 | 데이터 중심으로 작업이 이루어짐 |
순차적 응집도 | 한 모듈 부분의 출력이 다른 부분의 입력 데이터가 됨 | 작업의 결과가 다음 작업의 입력으로 사용됨 |
기능적 응집도 | 모듈의 부분들이 모듈이 담당하는 하나의 명확한 작업을 해결하는데 집중됨 | 모듈이 명확한 목적을 가지고 있으며, 작업이 잘 정의되어 있음 |
응집도를 높이고 결합도를 낮춘다는 것은 코드베이스의 관련된 부분은 한곳에 모아두고, 관련되지 않은 부분들을 분리하는 것을 의미한다.
이 가이드라인은 언뜻 보면 굉장히 간단해 보인다.하지만, 실제로는 코드베이스의 어떤 부분들이 서로 연관되어 있는지 이해하기 위해 소프트웨어의 도메인 모델을 깊게 파악해야 한다.
순환 복잡도와 같은 측정 가능한 지표들과는 달리, 코드가 얼마나 높은 응집도와 낮은 결합도를 가졌는지는 측정할 수 없다. 도메인 모델의 속성인 코드의 의미에 크게 의존한다.
높은 응집도, 낮은 결합도를 유지하기 위한 가이드라인과 크게 관련된 원칙이 있다: 관심사의 분리다. 이 둘이 제안하는 모범 사례들은 상당히 유사하다. 관심사 분리 원칙에 대해 더 자세히 알아보려면 이 글을 확인하자.
높은 응집도와 낮은 결합도를 모두 가진 코드 외에도, 스펙트럼의 다른 부분에 해당하는 세 가지 유형이 있다. 다음은 모든 4가지 유형이다:
높은 응집도와 낮은 결합도를 모두 가진 코드다.
예시:
// 이상적인 코드 예제
class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
this.items.push({ product, quantity });
}
getTotal() {
return this.items.reduce((total, item) => total + item.product.price * item.quantity, 0);
}
}
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
이처럼 연관된 코드들이 모여 하나의 모듈을 형성하고, 이 모듈들 사이의 결합도가 낮다.
높은 응집도와 높은 결합도를 가진 코드를 의미한다. 안티패턴이며, 하나의 모듈이 많은 책임을 담당하는 경우를 의미한다.
예시:
// 신의 객체 예제
class CommerceSystem {
constructor() {
this.products = [];
this.cart = [];
}
addProduct(name, price) {
this.products.push({ name, price });
}
addToCart(productName, quantity) {
const product = this.products.find(p => p.name === productName);
if (product) {
this.cart.push({ product, quantity });
}
}
checkout() {
return this.cart.reduce((total, item) => total + item.product.price * item.quantity, 0);
}
}
위 코드는 CommerceSystem
이라는 클래스가 Product
, Cart
에 대한 모든 로직들을 수행하고 있다.
낮은 응집도와 높은 결합도를 가진다. 서로 다른 클래스나 모듈 간의 경계가 잘못 선택되었을 때 발생한다.
예시:
// 경계가 잘못 선택된 경우
class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
this.items.push({ product, quantity });
}
// 이는 Product에 존재해야 한다
getProductName(productId) {
const product = this.items.find(item => item.product.id === productId);
return product ? product.product.name : null;
}
}
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
// 이는 Cart에 존재해야 한다.
calculateTotal(cart) {
return cart.items.reduce((total, item) => total + item.product.price * item.quantity, 0);
}
}
신의 객체(God Object)와는 다르게, 이러한 종류의 코드는 바운더리를 가지고 있다. 문제는 경계들이 부적절하게 선택되었고, 종종 도메인의 실제 의미를 반영하지 못한다는 것이다. 이러한 코드는 보통 단일 책임 원칙을 위반한다.
주로 프로그래머가 코드베이스를 과도하게 분리하려다가 초점을 잃어버릴때 발생한다. 결합도가 낮고, 응집도가 낮다.
예시:
// 파괴적 디커플링
class Cart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
}
class Item {
constructor(product, quantity) {
this.product = product;
this.quantity = quantity;
}
}
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
// 분리될 필요가 없는 코드
function calculateTotal(cart) {
return cart.items.reduce((total, item) => total + item.product.price * item.quantity, 0);
}
calculateTotal
은 Cart
클래스에 메서드 형태로 존재해야 응집도가 높다. 너무 과도하게 분리한 나머지 응집도가 낮아져 버렸다.
종종, 개발자가 낮은 결합도, 높은 응집도 지침을 구현하려고 할 때 지침의 결합도 측면에만 너무 많은 노력을 기울이고 다른 한 측면은 완전히 잊어버린다. 이는 코드가 실제로는 분리되어 있지만 동시에 명확한 초점이 없는 상황으로 이어진다. 너무 많이 분리되어 있어서 의미를 파악하기 어렵거나 심지어 불가능해진다.
응집도와 결합도 간의 관계를 이해하는 것이 중요하다. 코드의 일관성을 해치지 않고 코드 페이스를 완전히 분리하는 것은 불가능하다. 마찬가지로, 불필요한 결합을 만들지 않고 완전히 응집력 있는 코드를 만드는 것은 불가능하다.
이 둘 사이의 균형을 잘 잡는게 높은(하지만 완전하지 않은) 응집도와 느슨한(하지만 완전히 분리되지는 않은) 결합도를 가진 코드베이스를 만드는 핵심이다.
뿐만 아니라 다양한 레벨에서 응집도와 결합도에대해 생각할 수 있어야 한다.하나의 예시는 프로젝트 내의 폴더 구조일 것이다:
얼핏 보기에 이 프로젝트는 잘 정리되어 있다: entities
, factories
등을 위한 별도의 폴더들이 있다. 하지만 응집도가 부족하다.
위 프로젝트 구조는 경계가 잘못 선택되었다. 그 결과 응집도가 낮아졌다. 프로젝트의 내부는 느슨하게 결합되어 있지만, 경계가 의미를 반영하지 못한다.
높은 응집도(그리고 낮은 결합도)를 가진 버전은 다음과 같을 것이다:
사내 신규 프로젝트에 FSD의 구조를 차용하기로 결정했다. 최근에 FSD를 깊게 공부했고, FSD 라는 아키텍쳐가 왜 기능 단위의 분할을 하려고 하는지를 조금은 이해했다고 생각한다.
FSD를 추천하냐?고 하면 잘 모르겠다. 쉽게 도입할 수 있는 아키텍쳐는 아니라고 생각한다. 그래도 프로젝트 폴더 또는 모듈 레벨에서의 응집도와 결합도에 대해 고민해본적이 없다면 한 번쯤 공부해 보기를 권하고 싶다.
https://ubiklab.net/posts/low-coupling-high-cohesion/?source=post_page-----d36369fb1be9--------------------------------
https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/
https://enterprisecraftsmanship.com/posts/separation-of-concerns-in-orm/
제목을 보니 정보처리기사 공부할 때가 생각나네요 ㅋㅋㅋ 항해 때 FSD를 맛보았을 때 제가 내린 결론은 함께 하는 (혹은 개발자 본인 스스로) 확실하게 정해둔 본인만의 기준이 생기면 파일을 나누는게 쉽겠다. 였어요.
좋은 글 잘 읽었습니다 :)
최근에 고민하던 부분인데 간결하고 깔끔하게 잘 작성해 주셔서 너무 좋습니다. (회사 동료들한테 공유해버렸습니다.)
작성해주신 모듈화와 응집도 결합도 개념은 정말 다방면에서 생각해보게 되는 것 같아요.
(구조도 그렇지만, 모듈 내 코드 면에서도 고민하게 되더라고요.)
특히 응집도에 관해 짚어 주신 게 너무 와닿습니다.
결합도야 최소화 하더라도, 어떻게 하면 응집도를 높일 수 있을까라는 고민을 갖고 있는데
파괴적 디커플링을 경계하고, 무리하지 않는 선에서(완전하지 않은) 응집력을 갖게끔 하는 연습을 해봐야겠어요.
좋은 글 감사합니다.
🔥 응집도와 결합도의 개념을 이렇게 명확하게 정리해주다니... 정말 감탄밖에 안 나옵니다! 🚀 특히 코드 예제와 개념 비교가 너무 깔끔해서 읽으면서 머릿속에서 정리가 착착 됐어요. 🎯 FSD를 고민하는 과정도 공감됐고, 실제로 적용하면서 생기는 고민들까지 잘 풀어주셔서 너무 유익했습니다! 🙌 감사합니다! 👏👏
좋은 글이네요. 이번에 진행하는 프로젝트에 위글을 참고해서 리펙토링 해봐야겠어요. 저번에 대충 빠르게 모듈 만들고보니 종속성때문에 디버깅이 너무 까다롭더라고요..
요즘 폴더 구조에 고민이 많았는데, 응집도를 높여서 도메인 별로 폴더 구조를 만드는 방법도 아주 좋네요!
응집도와 결함도 내용도 잘 읽었습니다 :)
응집도와 결합도에 대한 명확한 설명과 예제 코드 잘봤습니다, 네 가지 코드 유형 분류와 폴더 구조에서의 응집도 문제까지 확인했습니다. FSD 구조가 쉽게 도입하기 어렵다는 의견도 공감됩니다. 결국 균형이 중요하다는 결론이 와닿았습니다. 개발 시 응집도와 결합도의 균형점을 찾는 데 도움이 될 것 같습니다! 잘읽었습니다 :)
저도 최근에 FSD를 사용해보면서 구조에 대해 매번 고민하게 됐는데 좋은 글 잘 보고 갑니다. 너무 잘 써져있어서 3번이나 읽었습니다 🔥 🔥 🔥