코드의 유지보수성과 확장성을 결정하는 응집도와 결합도
이 글에서 다루는 내용
이 글에서는 소프트웨어 설계에서 중요한 두 가지 개념, 응집도(Cohesion)와 결합도(Coupling) 를 중심으로 코드의 유지보수성과 확장성을 높이는 방법을 다룹니다. 먼저, 유지보수성과 확장성이 중요한 이유를 설명하고, 응집도와 결합도의 개념을 정리한 후 각각의 장점과 단점을 분석합니다. 이후, 응집도를 높이는 방법과 결합도를 낮추는 전략을 구체적으로 살펴보며, 이를 프론트엔드 개발에서 어떻게 적용할 수 있는지 설명합니다. 마지막으로, 응집도와 결합도를 균형 있게 유지하는 전략을 정리하고, 실무에서 현실적으로 고려해야 할 요소들을 논의합니다.
1. 서론: 소프트웨어 구조를 결정하는 두 가지 원칙
1-1. 유지보수성과 확장성을 높이는 설계의 중요성
소프트웨어 개발에서 유지보수성과 확장성을 고려한 설계는 필수적인 요소입니다.
처음에는 단순한 기능만 구현하면 되지만, 시간이 지나면서 코드의 복잡성이 증가하고, 새로운 요구사항이 추가됨에 따라 기존 코드를 변경해야 하는 상황이 발생합니다. 이때 코드 구조가 체계적으로 설계되어 있다면, 기능을 추가하거나 수정하는 과정이 훨씬 수월해지며, 개발 속도와 코드 품질이 유지될 수 있습니다.
유지보수성이 높은 코드란 오류를 수정하거나 새로운 기능을 추가할 때, 코드 변경을 최소화하면서도 원하는 결과를 얻을 수 있는 코드를 의미합니다.
즉, 변경이 필요한 부분이 명확하고, 수정이 다른 부분에 영향을 주지 않도록 설계된 코드라고 할 수 있습니다.
확장성이 높은 코드는 새로운 요구사항이 추가되더라도 기존 코드의 수정 없이 기능을 확장할 수 있는 구조를 갖춘 코드입니다.
즉, 기존 기능을 해치지 않으면서도 새로운 기능을 유연하게 추가할 수 있도록 모듈화된 코드라고 볼 수 있습니다.
리액트를 사용한 프론트엔드 개발에서는 다음과 같은 경우에 유지보수성과 확장성이 중요한 역할을 합니다.
-
React 컴포넌트 설계
컴포넌트의 역할이 명확하게 분리되지 않으면, 하나의 변경이 전체 UI에 영향을 미칠 수 있습니다.
예를 들어, 하나의 컴포넌트에서 데이터 가져오기, 상태 관리, UI 렌더링까지 모두 처리한다면, 특정 기능을 변경할 때 다른 기능까지 수정해야 하는 상황이 발생할 수 있습니다. -
상태 관리(State Management)
전역 상태를 무분별하게 사용하면 의존성이 강해지고, 불필요한 렌더링이 발생하며, 유지보수가 어려워질 수 있습니다.
적절한 상태 관리 기법을 적용하면, 각 컴포넌트가 독립적으로 동작할 수 있어 유지보수성과 확장성이 향상됩니다. -
API 요청 처리
API 호출 로직이 여러 곳에 흩어져 있다면, 백엔드 API가 변경될 때 수정해야 할 코드가 많아집니다.
API 요청을 한 곳에서 관리하고, 필요한 부분에서만 해당 데이터를 활용하도록 하면 유지보수성을 높일 수 있습니다.
이러한 문제를 해결하기 위해서는 응집도(Cohesion)와 결합도(Coupling)를 이해하고, 이를 적절히 조절하는 것이 중요합니다.
응집도를 높이고 결합도를 낮추면 코드가 논리적으로 정리되어 유지보수성이 뛰어나며, 새로운 기능 추가도 유연해집니다.
1-2. 응집도와 결합도: 소프트웨어 구조를 결정하는 핵심 개념
소프트웨어 설계에서 응집도(Cohesion)와 결합도(Coupling)는 코드의 품질을 결정하는 핵심 요소입니다.
이 두 개념은 서로 밀접한 관계를 가지며, 적절한 균형을 유지하는 것이 좋은 설계의 핵심입니다.
응집도(Cohesion): 코드 내부의 논리적 연결성
응집도란 하나의 모듈이나 컴포넌트가 얼마나 관련된 기능을 포함하고 있는지를 나타내는 개념입니다.
즉, 모듈이 하나의 명확한 책임을 가지고 있는가? 를 평가하는 기준이 됩니다.
-
응집도가 높은 코드
- 하나의 컴포넌트 또는 모듈이 단일한 책임을 가짐
- 코드의 논리적 흐름이 일관되고, 가독성이 높음
- 특정 기능을 변경할 때, 다른 부분에 영향을 주지 않음
-
응집도가 낮은 코드
- 하나의 모듈이 여러 가지 역할을 담당함
- 변경이 필요할 때 관련 없는 코드까지 수정해야 할 가능성이 높음
- 코드의 목적이 불분명하고, 재사용성이 낮음
결합도(Coupling): 모듈 간의 의존성
결합도란 하나의 모듈이 다른 모듈과 얼마나 강하게 연결되어 있는지를 의미합니다.
즉, 특정 모듈을 수정할 때, 다른 모듈도 함께 수정해야 하는가? 를 평가하는 기준이 됩니다.
-
결합도가 높은 코드
- 하나의 모듈이 다른 모듈의 내부 구현에 직접적으로 의존함
- 특정 기능을 변경하면, 여러 곳에서 수정이 필요함
- 테스트와 디버깅이 어려워짐
-
결합도가 낮은 코드
- 모듈 간의 의존성이 최소화됨
- 하나의 모듈을 변경해도 다른 모듈에는 영향을 주지 않음
- 유지보수성과 확장성이 높아짐
응집도와 결합도의 균형이 중요한 이유
소프트웨어 설계에서 좋은 구조란 응집도를 높이면서 결합도를 낮추는 것입니다.
즉, 각 모듈이 독립적인 역할을 수행하면서도 필요한 부분에서는 유기적으로 동작할 수 있도록 설계하는 것이 바람직합니다.
하지만 현실적인 개발 환경에서는 응집도를 높이려고 지나치게 모듈을 세분화하면, 오히려 결합도가 높아질 수 있습니다.
반대로 결합도를 낮추려고 지나치게 분리하면, 코드가 지나치게 단순해지거나 재사용성이 떨어질 수 있습니다.
따라서 응집도와 결합도의 균형을 맞추는 것이 가장 중요합니다.
이를 위해 다음과 같은 원칙을 고려할 수 있습니다.
- 모듈이 하나의 명확한 책임을 가지도록 유지 (응집도 높이기)
- 필요한 데이터만 주고받도록 인터페이스를 명확하게 정의 (결합도 낮추기)
- 유연한 의존성 관리 방식 적용 (ex: 의존성 주입, 이벤트 기반 설계)
- 비즈니스 로직과 UI 로직을 분리하여 독립성 유지
응집도와 결합도를 적절히 조절하면, 코드의 유지보수성이 향상되며, 새로운 기능을 추가할 때도 기존 코드에 미치는 영향을 최소화할 수 있습니다.
2. 응집도의 개념과 단계
2-1. 응집도의 정의와 높은 응집도가 갖는 장점
응집도(Cohesion)란 하나의 모듈이나 컴포넌트 내부에서 기능들이 얼마나 밀접하게 연관되어 있는지를 나타내는 개념입니다.
응집도가 높을수록 모듈 내부의 요소들이 논리적으로 긴밀하게 연결되어 있으며, 하나의 명확한 역할을 수행합니다.
반대로 응집도가 낮으면 관련성이 적은 기능들이 하나의 모듈에 뒤섞이게 되어 유지보수가 어려워집니다.
높은 응집도를 유지하면 다음과 같은 장점이 있습니다.
- 가독성이 높아짐: 코드의 역할이 명확해지고, 모듈이 수행하는 기능을 쉽게 파악할 수 있습니다.
- 유지보수가 용이함: 특정 기능을 수정해야 할 때, 관련된 코드가 한 곳에 모여 있어 수정이 쉬워집니다.
- 코드 재사용성이 증가함: 기능이 독립적으로 설계되기 때문에 여러 곳에서 재사용할 수 있습니다.
- 테스트가 쉬워짐: 모듈이 하나의 역할에 집중할수록 테스트할 범위가 명확해지고, 단위 테스트가 용이해집니다.
응집도가 낮을 경우 하나의 모듈이 너무 많은 역할을 담당하게 되고, 기능 간의 의존성이 높아져 코드 수정이 어렵고, 예상치 못한 버그가 발생할 가능성이 커집니다. 따라서, 좋은 소프트웨어 설계를 위해서는 응집도를 높이는 것이 중요합니다.
2-2. 응집도의 단계
응집도는 단순히 높거나 낮은 개념이 아니라, 여러 단계로 구분됩니다. 일반적으로 응집도를 평가할 때 다음과 같은 단계로 구분할 수 있습니다.
단계 | 응집도 수준 | 특징 |
---|---|---|
우연적 응집 (Coincidental Cohesion) | 낮음 🔴 | 기능들이 서로 관련 없이 묶여 있음. 유지보수 어려움. |
논리적 응집 (Logical Cohesion) | 낮음 🔴 | 비슷한 유형의 기능이지만 서로 직접적인 연관이 없음. |
절차적 응집 (Procedural Cohesion) | 보통 🟡 | 특정 절차(순서)를 따르는 기능들이 함께 존재. |
통신적 응집 (Communicational Cohesion) | 보통 🟡 | 같은 데이터를 사용하거나 같은 입력을 처리하는 기능들이 포함됨. |
순차적 응집 (Sequential Cohesion) | 높음 🟢 | 한 기능의 출력이 다음 기능의 입력으로 사용됨. 자연스러운 흐름이 존재. |
기능적 응집 (Functional Cohesion) | 높음 🟢 | 모듈이 단일 책임을 가지며, 불필요한 기능이 없음. 가장 이상적인 형태. |
-
우연적 응집 (Coincidental Cohesion)
- 모듈 내부의 기능들이 아무런 연관 없이 묶여 있는 상태입니다.
- 예제: 하나의 모듈 내에서 함수들이 서로 아무런 관련 없이 묶여 있는 경우. 예를 들어, utils.js 파일에 문자열 처리 함수, 날짜 포맷팅 함수, 배열 변환 함수 등 서로 관련 없는 기능이 함께 정의된 경우.
-
논리적 응집 (Logical Cohesion)
- 비슷한 유형의 작업을 수행하지만, 서로 관련성이 없는 기능들이 한 모듈에 포함된 상태입니다.
- 예제:
utility.js
파일에 문자열 처리, 날짜 포맷팅, 숫자 연산 등의 함수가 섞여 있는 경우.
-
절차적 응집 (Procedural Cohesion)
- 특정 절차를 따르는 여러 기능이 한 모듈에 포함된 상태입니다.
- 예제: 하나의 함수에서 여러 단계의 처리를 연속적으로 수행하지만, 각 단계가 서로 독립적인 경우.
-
통신적 응집 (Communicational Cohesion)
- 같은 데이터를 사용하거나, 같은 입력을 처리하는 기능들이 한 모듈에 포함된 상태입니다.
- 예제: 동일한 데이터 객체를 읽고 수정하는 여러 메서드가 한 클래스에 포함된 경우.
-
순차적 응집 (Sequential Cohesion)
- 한 기능의 출력이 다음 기능의 입력으로 사용되는 경우입니다.
- 예제: API 데이터를 받아 변환하고, 변환된 데이터를 화면에 렌더링하는 과정이 하나의 모듈에서 처리되는 경우.
-
기능적 응집 (Functional Cohesion)
- 모듈이 하나의 명확한 기능만을 수행하며, 불필요한 요소가 포함되지 않은 상태입니다.
- 예제: React 컴포넌트가 한 가지 역할만 수행하며, 불필요한 상태나 로직이 포 함되지 않은 경우.
이 중에서 기능적 응집이 가장 이상적인 형태이며, 모듈이 하나의 명확한 책임을 수행하도록 설계하는 것이 중요합니다.
// ❌ Bad: 하나의 컴포넌트에서 너무 많은 역할을 담당
function Dashboard() {
const [user, setUser] = useState<User | null>(null);
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then(setUser);
fetch("/api/notifications")
.then((res) => res.json())
.then(setNotifications);
}, []);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}</p>
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
</div>
);
}
// ✅ Good: 데이터 처리 로직을 별도 훅으로 분리하여 응집도를 높임
function useUserData() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then(setUser);
}, []);
return user;
}
function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
fetch("/api/notifications")
.then((res) => res.json())
.then(setNotifications);
}, []);
return notifications;
}
function Dashboard() {
const user = useUserData();
const notifications = useNotifications();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}</p>
<NotificationList notifications={notifications} />
</div>
);
}
function NotificationList({
notifications,
}: {
notifications: Notification[];
}) {
return (
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
);
}
2-3. 프론트엔드에서 응집도를 높이는 방법
프론트엔드 개발에서는 응집도를 높이는 것이 특히 중요합니다.
다음과 같은 방법을 통해 코드의 응집도를 높일 수 있습니다.
-
컴포넌트 단위를 명확히 정의하기
- React에서는 한 컴포넌트가 여러 가지 역할을 수행하지 않도록 분리하는 것이 중요합니다.
- 예를 들어, 데이터 요청, 상태 관리, UI 렌더링을 하나의 컴포넌트에서 처리하기보다는, 이를 분리하여 응집도를 높일 수 있습니다.
-
하나의 모듈이 단일 책임을 가지도록 구성하기
Single Responsibility Principle(SRP)
을 적용하여 하나의 파일이나 함수가 한 가지 역할만 수행하도록 설계합니다.- 예를 들어, API 호출과 데이터 변환을 한 파일에서 처리하는 것이 아니라, API 요청 모듈과 데이터 가공 모듈을 분리하는 것이 좋습니다.
-
상태 관리 로직을 분리하기
- 전역 상태 관리를 사용할 때, 모든 상태를 한 곳에서 관리하는 것이 아니라, 관련된 상태끼리 그룹화하여 응집도를 높이는 것이 중요합니다.
- 예를 들어, Redux에서는
slice
단위로 상태를 관리하거나, React의useReducer
를 사용하여 관련된 상태를 그룹화할 수 있습니다.
-
재사용 가능한 유틸리티 함수와 헬퍼 함수 정의하기
- 프로젝트 내에서 여러 곳에서 사용되는 공통 로직을 별도의 유틸리티 파일로 분리하면 응집도를 높일 수 있습니다.
- 다만, 유틸리티 파일이 너무 많은 역할을 하게 되면 응집도가 낮아질 수 있으므로, 관련된 기능끼리 그룹화하여 관리하는 것이 중요합니다.
응집도를 높이면 코드가 모듈화되어 유지보수성과 확장성이 향상되며, 협업 시에도 코드의 역할이 명확해져 개발 효율이 증가합니다.
3. 결합도의 개념과 단계
3-1. 결합도의 정의와 낮은 결합도가 갖는 장점
결합도(Coupling)란 하나의 모듈이 다른 모듈과 얼마나 강하게 연결되어 있는지를 나타내는 개념입니다.
결합도가 높으면 모듈 간의 의존성이 커져 한 부분을 변경할 때 다른 부분도 함께 수정해야 하는 경우가 많아집니다.
반대로 결합도가 낮으면 모듈이 독립적으로 동작할 수 있어 유지보수가 쉬워지고 확장성이 높아집니 다.
낮은 결합도를 유지하면 다음과 같은 장점이 있습니다.
- 변경에 유연함: 한 모듈을 수정해도 다른 모듈에 미치는 영향이 최소화됩니다.
- 코드 재사용성이 높아짐: 모듈이 독립적이면 다양한 프로젝트나 기능에서 쉽게 재사용할 수 있습니다.
- 디버깅과 테스트가 쉬움: 특정 기능이 다른 요소에 의존하지 않기 때문에 단위 테스트가 용이합니다.
- 병렬 개발이 가능함: 팀 내에서 여러 개발자가 독립적인 모듈을 개발할 수 있어 생산성이 향상됩니다.
프론트엔드 개발에서는 컴포넌트 간의 의존성을 줄이거나, API 호출과 UI 로직을 분리하는 등의 방식으로 결합도를 낮출 수 있습니다.
이를 위해 결합도의 단계와 이를 낮추는 방법을 구체적으로 살펴보겠습니다.
3-2. 결합도의 단계
결합도는 강한 결합(좋지 않은 설계)에서 약한 결합(좋은 설계)으로 나누어 단계별로 구분할 수 있습니다.
단계 | 결합도 수준 | 특징 |
---|---|---|
내용 결합 (Content Coupling) | 높음 🔴 | 한 모듈이 다른 모듈의 내부 데이터를 직접 수정. 강한 의존성. |
공통 결합 (Common Coupling) | 높음 🔴 | 여러 모듈이 전역 변수를 공유하여 강하게 연결됨. 수정이 어렵고 예측 불가능한 오류 발생 가능. |
외부 결합 (External Coupling) | 보통 🟡 | 두 모 듈이 특정한 외부 리소스(파일, API 등)를 공유하는 상태. |
제어 결합 (Control Coupling) | 보통 🟡 | 한 모듈이 다른 모듈의 실행 흐름을 직접 결정. 플래그 값을 넘겨 제어하는 방식. |
스탬프 결합 (Stamp Coupling) | 낮음 🟢 | 모듈 간 데이터 구조를 공유하지만 일부만 사용. 데이터 의존성이 있지만 부분적으로 독립적. |
자료 결합 (Data Coupling) | 낮음 🟢 | 모듈 간 필요한 데이터만 전달하며 불필요한 의존성이 최소화됨. 가장 이상적인 결합도. |
-
내용 결합 (Content Coupling)
- 한 모듈이 직접 다른 모듈의 내부 데이터를 수정하거나, 내부 로직을 강하게 참조하는 경우입니다.
- 예제: A 모듈이 B 모듈의 변수를 직접 변경하는 경우.
// ❌ Bad: 한 컴포넌트가 직접 다른 컴포넌트의 내부 상태를 수정
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} setCount={setCount} />;
}
function Child({
count,
setCount,
}: {
count: number;
setCount: (c: number) => void;
}) {
useEffect(() => {
// 직접 부모의 상태를 변경하는 방식 (높은 결합도)
setCount(10);
}, []);
return <p>Count: {count}</p>;
}
// ✅ Good: 부모가 상태를 관리하고, 자식은 이벤트 핸들러만 호출
function Parent() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount((prev) => prev + 1);
return <Child count={count} onIncrement={handleIncrement} />;
}
function Child({
count,
onIncrement,
}: {
count: number;
onIncrement: () => void;
}) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onIncrement}>Increment</button>
</div>
);
}
-
공통 결합 (Common Coupling)
- 여러 모듈이 전역 변수를 공유하는 경우입니다.
- 예제: 여러 모듈이 동일한 전역 변수를 직접 읽거나 수정할 때 발생하는 결합도입니다. 예를 들어, window 객체의 속성을 여러 모듈에서 직접 참조하거나, Redux의 전역 상태를 불필요하게 여러 컴포넌트가 직접 수정하는 경우가 이에 해당합니다.
-
외부 결합 (External Coupling)
- 두 모듈이 특정한 외부 리소스(파일, 데이터베이스, API 등)를 공유하는 경우입니다.
- 예제: 여러 모듈이 같은 API 엔드포인트를 직접 호출하는 경우.
-
제어 결합 (Control Coupling)
- 한 모듈이 다른 모듈의 실행 흐름을 직접 결정하는 경우입니다.
- 예제: 함수가 매개변수로 플래그 값을 받아 분기문을 통해 다른 기능을 수행하는 경우.
-
스탬프 결합 (Stamp Coupling)
- 모듈 간 데이터 구조를 공유하지만, 그중 일부 데이터만 사용하는 경우입니다.
- 예제: 하나의 객체를 여러 모듈이 전달받지만, 일부 필드만 사용하는 경우.
-
자료 결합 (Data Coupling)
- 모듈 간 필요한 데이터만 전달하며, 불필요한 의존성을 최소화한 상태입니다.
- 예제: 함수가 필요한 인자만 전달받아 동작하는 경우.
이 중에서 내용 결합과 공통 결합은 피해야 하며, 자료 결합 수준으로 유지하는 것이 이상적입니다.
즉, 하나의 모듈이 다른 모듈의 내부 구조를 알 필요 없이 필요한 데이터만 주고받도록 설계하는 것이 바람직합니다.
3-3. 프론트엔드에서 결합도를 낮추는 방법
프론트엔드 개발에서는 다음과 같은 방법을 통해 결합도를 낮출 수 있습니다.
-
전역 상태 관리 최소화
- Redux, Context API 등 전역 상태를 사용할 때, 모든 컴포넌트가 필요 없이 공유하는 상태를 줄이는 것이 중요합니다.
- 예제: 전역 상태 대신 컴포넌트 내부 상태(
useState
)나props
를 활용하여 데이터 흐름을 명확하게 유지하기.
-
의존성 주입(Dependency Injection) 사용
- 모듈이 특정 의존성을 직접 생성하지 않고, 외부에서 주입받도록 설계하면 결합도를 낮출 수 있습니다.
- 예제: API 호출 로직을 컴포넌트 내부에서 처리하지 않고 별도의 서비스 모듈로 분리하기.
-
컴포넌트 간 직접 참조 줄이기
- React에서 부모-자식 관계가 복잡해질 경우,
props
전달 대신 Context API나 상태 관리 라이브러리를 사용하여 계층 구조를 단순화할 수 있습니다. - 예제:
prop drilling
을 피하기 위해 상태 관리 라이브러리를 사용하거나, 컴포넌트를 더 작은 단위로 나누기.
- React에서 부모-자식 관계가 복잡해질 경우,
-
비즈니스 로직과 UI 로직 분리
- UI 관련 코드와 데이터 처리 로직을 분리하여 유지보수를 용이하게 만듭니다.
- 예제: API 요청을
useEffect
내부에서 처리하는 대신, 별도의 훅(useFetch
)을 만들어 관리하기.
결합도를 낮추면 코드의 독립성이 강화되며, 변경이 필요한 경우에도 최소한의 수정만으로 기능을 확장할 수 있습니다.