본문으로 건너뛰기
2023. 8. 12
© WONKOOK LEE

코드의 유지보수성과 확장성을 결정하는 응집도와 결합도

응집도와 결합도

이 글에서 다루는 내용

이 글에서는 소프트웨어 설계에서 중요한 두 가지 개념, 응집도(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)높음 🟢모듈이 단일 책임을 가지며, 불필요한 기능이 없음. 가장 이상적인 형태.

  1. 우연적 응집 (Coincidental Cohesion)

    • 모듈 내부의 기능들이 아무런 연관 없이 묶여 있는 상태입니다.
    • 예제: 하나의 모듈 내에서 함수들이 서로 아무런 관련 없이 묶여 있는 경우. 예를 들어, utils.js 파일에 문자열 처리 함수, 날짜 포맷팅 함수, 배열 변환 함수 등 서로 관련 없는 기능이 함께 정의된 경우.
  2. 논리적 응집 (Logical Cohesion)

    • 비슷한 유형의 작업을 수행하지만, 서로 관련성이 없는 기능들이 한 모듈에 포함된 상태입니다.
    • 예제: utility.js 파일에 문자열 처리, 날짜 포맷팅, 숫자 연산 등의 함수가 섞여 있는 경우.
  3. 절차적 응집 (Procedural Cohesion)

    • 특정 절차를 따르는 여러 기능이 한 모듈에 포함된 상태입니다.
    • 예제: 하나의 함수에서 여러 단계의 처리를 연속적으로 수행하지만, 각 단계가 서로 독립적인 경우.
  4. 통신적 응집 (Communicational Cohesion)

    • 같은 데이터를 사용하거나, 같은 입력을 처리하는 기능들이 한 모듈에 포함된 상태입니다.
    • 예제: 동일한 데이터 객체를 읽고 수정하는 여러 메서드가 한 클래스에 포함된 경우.
  5. 순차적 응집 (Sequential Cohesion)

    • 한 기능의 출력이 다음 기능의 입력으로 사용되는 경우입니다.
    • 예제: API 데이터를 받아 변환하고, 변환된 데이터를 화면에 렌더링하는 과정이 하나의 모듈에서 처리되는 경우.
  6. 기능적 응집 (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)낮음 🟢모듈 간 필요한 데이터만 전달하며 불필요한 의존성이 최소화됨. 가장 이상적인 결합도.

  1. 내용 결합 (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>
);
}
  1. 공통 결합 (Common Coupling)

    • 여러 모듈이 전역 변수를 공유하는 경우입니다.
    • 예제: 여러 모듈이 동일한 전역 변수를 직접 읽거나 수정할 때 발생하는 결합도입니다. 예를 들어, window 객체의 속성을 여러 모듈에서 직접 참조하거나, Redux의 전역 상태를 불필요하게 여러 컴포넌트가 직접 수정하는 경우가 이에 해당합니다.
  2. 외부 결합 (External Coupling)

    • 두 모듈이 특정한 외부 리소스(파일, 데이터베이스, API 등)를 공유하는 경우입니다.
    • 예제: 여러 모듈이 같은 API 엔드포인트를 직접 호출하는 경우.
  3. 제어 결합 (Control Coupling)

    • 한 모듈이 다른 모듈의 실행 흐름을 직접 결정하는 경우입니다.
    • 예제: 함수가 매개변수로 플래그 값을 받아 분기문을 통해 다른 기능을 수행하는 경우.
  4. 스탬프 결합 (Stamp Coupling)

    • 모듈 간 데이터 구조를 공유하지만, 그중 일부 데이터만 사용하는 경우입니다.
    • 예제: 하나의 객체를 여러 모듈이 전달받지만, 일부 필드만 사용하는 경우.
  5. 자료 결합 (Data Coupling)

    • 모듈 간 필요한 데이터만 전달하며, 불필요한 의존성을 최소화한 상태입니다.
    • 예제: 함수가 필요한 인자만 전달받아 동작하는 경우.

이 중에서 내용 결합과 공통 결합은 피해야 하며, 자료 결합 수준으로 유지하는 것이 이상적입니다.
즉, 하나의 모듈이 다른 모듈의 내부 구조를 알 필요 없이 필요한 데이터만 주고받도록 설계하는 것이 바람직합니다.


3-3. 프론트엔드에서 결합도를 낮추는 방법

프론트엔드 개발에서는 다음과 같은 방법을 통해 결합도를 낮출 수 있습니다.

  • 전역 상태 관리 최소화

    • Redux, Context API 등 전역 상태를 사용할 때, 모든 컴포넌트가 필요 없이 공유하는 상태를 줄이는 것이 중요합니다.
    • 예제: 전역 상태 대신 컴포넌트 내부 상태(useState)나 props를 활용하여 데이터 흐름을 명확하게 유지하기.
  • 의존성 주입(Dependency Injection) 사용

    • 모듈이 특정 의존성을 직접 생성하지 않고, 외부에서 주입받도록 설계하면 결합도를 낮출 수 있습니다.
    • 예제: API 호출 로직을 컴포넌트 내부에서 처리하지 않고 별도의 서비스 모듈로 분리하기.
  • 컴포넌트 간 직접 참조 줄이기

    • React에서 부모-자식 관계가 복잡해질 경우, props 전달 대신 Context API나 상태 관리 라이브러리를 사용하여 계층 구조를 단순화할 수 있습니다.
    • 예제: prop drilling을 피하기 위해 상태 관리 라이브러리를 사용하거나, 컴포넌트를 더 작은 단위로 나누기.
  • 비즈니스 로직과 UI 로직 분리

    • UI 관련 코드와 데이터 처리 로직을 분리하여 유지보수를 용이하게 만듭니다.
    • 예제: API 요청을 useEffect 내부에서 처리하는 대신, 별도의 훅(useFetch)을 만들어 관리하기.

결합도를 낮추면 코드의 독립성이 강화되며, 변경이 필요한 경우에도 최소한의 수정만으로 기능을 확장할 수 있습니다.




4. 응집도와 결합도의 균형 잡기

4-1. 높은 응집도와 낮은 결합도를 조화롭게 유지하는 전략

응집도를 높이고 결합도를 낮추는 것은 좋은 소프트웨어 설계를 위한 핵심 원칙입니다.
하지만 이 두 가지 개념이 항상 독립적으로 적용되는 것은 아닙니다.
응집도를 높이려고 지나치게 많은 기능을 하나의 모듈에 포함하면 오히려 결합도가 높아질 수 있고,
반대로 결합도를 낮추려고 지나치게 분리하면 불필요한 복잡성이 증가할 수도 있습니다.

따라서, 응집도를 유지하면서도 불필요한 결합을 최소화하는 균형을 맞추는 것이 중요합니다.
이를 위해 다음과 같은 전략을 적용할 수 있습니다.

  • 모듈의 역할을 명확히 정의하기

    • 하나의 모듈이 단일 책임을 가지도록 설계하되, 다른 모듈과 지나치게 의존하지 않도록 합니다.
    • 예제: React 컴포넌트에서 UI 관련 코드와 비즈니스 로직을 분리하여 각 역할을 명확하게 구분.
  • 모듈 간 인터페이스를 명확히 하기

    • 서로 다른 모듈이 직접적으로 의존하지 않도록, 명확한 데이터 흐름을 유지하는 것이 중요합니다.
    • 예제: API 요청 모듈과 UI 컴포넌트 간 인터페이스를 정의하고, 데이터 처리 로직을 분리.
  • 데이터 흐름을 단방향으로 유지하기

    • 결합도를 줄이기 위해 데이터가 한 방향으로만 흐르도록 설계하는 것이 중요합니다.
    • 예제: React에서 상태 관리를 할 때, propscontext를 활용하여 데이터를 위에서 아래로 전달.
// ✅ 상태 관리 로직을 Context와 useReducer를 활용해 독립적으로 유지
const CounterContext = createContext<
{ state: number; dispatch: React.Dispatch<Action> } | undefined
>(undefined);

type Action = { type: "increment" } | { type: "decrement" };

function counterReducer(state: number, action: Action): number {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
return state;
}
}

function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, 0);

return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}

function useCounter() {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounter must be used within a CounterProvider");
}
return context;
}

function Counter() {
const { state, dispatch } = useCounter();

return (
<div>
<p>Count: {state}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}

function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}

이제 이러한 원칙을 실제 프론트엔드 아키텍처에서 어떻게 적용할 수 있는지 살펴보겠습니다.


4-2. 실제 아키텍처 설계에서의 적용

프론트엔드에서 응집도와 결합도를 조절하는 방법은 여러 가지가 있지만,
대표적으로 모듈화(Modularization), 의존성 주입(Dependency Injection), 이벤트 기반 구조(Event-Driven Architecture) 를 활용할 수 있습니다.


1) 모듈화 (Modularization)

모듈화는 응집도를 높이면서 결합도를 낮추는 가장 기본적인 방법입니다.
기능별로 모듈을 분리하면 유지보수가 용이해지고, 코드의 재사용성이 증가합니다.

  • UI 컴포넌트 분리:

    • React에서는 기능이 명확히 구분된 컴포넌트를 만들고, 한 컴포넌트가 너무 많은 역할을 하지 않도록 분리합니다.
    • 예제: 버튼, 카드, 리스트 등 재사용 가능한 UI 요소를 따로 모듈화.
  • 비즈니스 로직과 UI 로직 분리:

    • 데이터 처리 로직을 별도의 파일이나 훅(Hook)으로 분리하면, UI 코드와의 결합도를 낮출 수 있습니다.
    • 예제: API 호출을 담당하는 useFetch 훅을 따로 만들고, 이를 여러 컴포넌트에서 재사용.

2) 의존성 주입 (Dependency Injection)

의존성 주입은 한 모듈이 다른 모듈을 직접 참조하는 것이 아니라, 외부에서 필요한 모듈을 주입받아 사용하는 방식입니다.
이렇게 하면 특정 모듈이 내부적으로 다른 모듈에 강하게 의존하지 않도록 만들 수 있습니다.

  • API 요청 모듈을 주입하여 컴포넌트 독립성 유지

    • API 요청 로직을 컴포넌트 내부에서 직접 호출하는 것이 아니라, 주입받아 사용할 수 있도록 분리하면 유연성이 증가합니다.
  • 의존성을 함수나 props로 전달하여 결합도 낮추기

    • React에서는 props를 활용하여 특정 기능을 주입받아 사용할 수 있습니다.
    • 예제: 부모 컴포넌트에서 자식 컴포넌트로 핸들러 함수를 전달하여 직접적인 의존성을 줄임.

3) 이벤트 기반 구조 (Event-Driven Architecture)

이벤트 기반 아키텍처는 서로 다른 모듈이 직접 연결되지 않고, 특정 이벤트를 통해 통신하는 구조입니다.
이렇게 하면 모듈 간의 결합도를 낮추면서도, 기능이 유기적으로 동작할 수 있습니다.

  • 이벤트 버스(Event Bus) 사용

    • 컴포넌트 간 데이터 공유가 필요할 때, 직접 참조하는 대신 이벤트를 활용하여 처리할 수 있습니다.
    • 예제: EventEmitter를 사용하여 독립적인 모듈 간에 이벤트를 전달.
  • Pub-Sub 패턴 적용

    • 특정 모듈에서 발생한 이벤트를 여러 개의 다른 모듈이 구독(Subscribe)하여 사용할 수 있도록 하면 결합도를 줄일 수 있습니다.
    • 예제: Redux에서 dispatch를 통해 상태 변화를 중앙에서 관리하고, 필요한 컴포넌트만 해당 상태를 구독하도록 설정.

응집도와 결합도를 균형 있게 유지하는 것은 소프트웨어의 확장성과 유지보수성을 높이는 핵심 요소입니다.
이제까지 살펴본 원칙과 패턴을 적절히 적용하면, 효율적인 프론트엔드 아키텍처를 구축하는 데 도움이 될 것입니다.




5. 결론: 실무에서의 최적화 원칙

5-1. 코드의 복잡성을 줄이면서 유지보수성을 높이는 방법

프론트엔드 개발에서 응집도와 결합도를 적절히 관리하는 것은 단순한 이론적 개념을 넘어,
실제 프로젝트의 유지보수성, 확장성, 가독성을 결정하는 중요한 요소입니다.
하지만 실무에서는 완벽한 설계를 적용하기보다는, 코드의 복잡성을 줄이면서도 현실적인 개발 속도를 유지하는 것이 중요합니다.

이를 위해 다음과 같은 원칙을 고려할 수 있습니다.

  • 기능 단위로 모듈을 구성하여 응집도를 높이기

    • 컴포넌트나 유틸리티 함수는 하나의 명확한 역할을 수행하도록 설계합니다.
    • 예제: API 요청, 데이터 가공, UI 렌더링을 각각의 모듈로 분리하여 역할을 명확하게 구분.
  • 불필요한 의존성을 줄이고 결합도를 낮추기

    • 전역 상태를 최소화하고, 필요할 때만 propscontext를 활용하여 데이터 전달.
    • 예제: Redux, Context API 같은 전역 상태 관리 도구를 사용할 때, 모든 상태를 글로벌 컨텍스트로 관리하는 것이 아니라, 기능별 컨텍스트를 분리하여 불필요한 의존성을 줄이는 것이 중요합니다. 예를 들어, AuthContextThemeContext를 분리하여, 사용자 인증 관련 상태와 UI 테마 상태를 독립적으로 관리할 수 있습니다.
  • 코드 중복을 줄이고 재사용성을 높이기

    • 반복되는 로직을 별도의 유틸리티 함수나 커스텀 훅으로 분리하여 활용.
    • 예제: 여러 컴포넌트에서 사용하는 API 요청 로직을 useFetch 같은 훅으로 분리하여 중복을 최소화.
  • 단순한 해결책을 우선 적용하기

    • 불필요한 디자인 패턴을 적용하기보다는, 코드의 가독성과 유지보수성을 고려한 단순한 구조를 유지하는 것이 중요합니다.
    • 예제: 지나치게 복잡한 상태 관리 대신, React의 useState를 활용하여 필요할 때만 상태를 분리.

5-2. 응집도와 결합도를 고려한 현실적인 설계 방향

소프트웨어 설계에서 응집도와 결합도는 서로 밀접하게 연관되어 있으며,
단순히 "응집도는 높고, 결합도는 낮아야 한다"는 공식적인 원칙만으로 해결할 수 있는 문제가 아닙니다.
프로젝트의 규모, 팀의 개발 문화, 요구 사항의 변화 등을 고려하여 유연하게 적용하는 것이 중요합니다.

다음은 실무에서 현실적으로 적용할 수 있는 설계 방향입니다.

  • "작은 단위의 모듈화를 우선 적용"

    • 한 번에 너무 많은 기능을 분리하기보다, 핵심적인 기능부터 독립적인 모듈로 설계하는 것이 효과적입니다.
    • 예제: 먼저 API 호출과 UI를 분리한 후, 필요할 때 상태 관리나 데이터 가공 모듈을 추가하는 방식.
  • "필요한 곳에만 적절한 추상화 적용"

    • 과도한 추상화는 오히려 코드 복잡성을 증가시킬 수 있습니다.
    • 예제: 단순한 기능을 별도 클래스로 추상화하기보다, 함수나 훅을 활용하여 간단하게 해결.
  • "팀 내 코드 컨벤션과 일관성을 유지"

    • 개발자마다 설계 스타일이 다를 수 있으므로, 코드 컨벤션을 통해 일관된 모듈화 방식을 유지하는 것이 중요합니다.
    • 예제: 특정 기능은 hooks/ 폴더에서 관리하고, 상태 관리는 context/ 폴더에서 분리하는 방식으로 통일.

응집도를 높이고 결합도를 낮추는 것은 좋은 소프트웨어 설계를 위한 핵심 원칙이지만,
실무에서는 과도한 설계보다는 실용적인 접근이 필요합니다.

  1. 모듈을 기능 단위로 분리하여 응집도를 높이고, 역할을 명확하게 유지한다.
  2. 필요할 때만 의존성을 추가하여 결합도를 낮추고, 유지보수성을 고려한다.
  3. 단순한 구조를 우선적으로 적용하며, 팀 내 일관된 설계 원칙을 유지한다.

이러한 원칙을 적용하면 프론트엔드 프로젝트에서 유지보수성이 높고 확장 가능한 구조를 유지할 수 있으며,
변화하는 요구 사항에도 유연하게 대응할 수 있습니다.


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자