새벽의 기록

[iOS] 결합도와 응집도에 관해 + CoordinatorView에 대한 스스로의 판단 한 스푼 본문

[iOS]

[iOS] 결합도와 응집도에 관해 + CoordinatorView에 대한 스스로의 판단 한 스푼

OneTen 2025. 11. 26. 00:34

좋은 소프트웨어일수록 모듈의 독립성이 높다는 말을 많이 들어왔다.

하지만 왜?? 독립성이 높으면 왜 좋지?? 추상적으로는 알고 있으나 설명해보라고 하면 명확히 얘기할 수가 없었다.

그래서 이번에 모듈의 독립성과 결합도, 응집도에 대해 심층 분석해본 후 내 개인적인 생각들에 대해 남기려고 한다.


모듈?

복잡하고 큰 문제일수록, 일반적으로 작은 부분으로 쪼개어서 하나씩 풀어나가기 마련이다.

이 때 문제를 작은 부분으로 쪼개나가는 것을 모듈화라고 한다.

소프트웨어에서는 각 기능별로 나누어진 소스 단위를 뜻한다. 독립적으로 컴파일 가능한 프로그램 혹은 하나의 함수나 클래스도 모듈이 된다.

 

좋은 모듈화를 구현하기 위해서는 목적에 맞는 기능만으로 구성을 해야하는데, 모듈은 주어진 기능만을 독립적으로 수행하기 때문이다. 따라서 목적에 맞추어 재사용성이 높고 코드의 이해/수정이 용이하게 만들어야 한다.

 

예를 들어 특정 모듈을 수정하더라도 다른 모듈에 끼치는 영향이 적게 설계하고, 오류가 발생하더라도 모듈 단위로 나뉘어져 있기 때문에 최소한의 수정만으로 해결할 수 있게도 만들어야 한다.

 

이러한 모듈의 독립성은 모듈의 결합도(Coupling)와 응집도(Cohesion)를 기준으로 평가한다.

결합도는 모듈과 모듈 간의 의존 정도를 의미하고, 응집도는 한 모듈 내의 구성요소들 간의 연관 정도를 의미한다.

결합도와 응집도의 정도에 따라 독립성이 평가되는데, 응집도는 강할수록, 결합도는 약할 수록 좋은 모듈로 평가 된다.


결합도?

결합도는 모듈간의 의존 정도 또는 연관된 관계의 의존 정도를 의미한다고 보면 된다. (결합도를 의존도라고 부르기도 함)

 

예를 들면 결합도가 높은 객체는 다른 객체와 연관 관계가 끈끈하여, 하나의 객체의 구조를 변경하게 된다면 그에 연관된 객체들도 싹 변경해야 할수도 있고, 사용 코드도 변경해야 할 수도 있어서 유지보수 측면에서 매우 마이너스적인 요소로 작용된다.

 

비유하자면 자동차 하나에는 여러개의 모듈들(핸들, 바퀴, 엔진, 배터리 등)이 들어있을 것이다. 그리고 이렇게 하나의 프로그램(자동차) 안에서 각 모듈들이 서로 관련되어 의존하고 있는 정도가 결합도다.

 

핸들과 바퀴 모듈간의 관계를 예로 들자면, 이 둘은 각각의 동작이 상호작용을 통해 자동차가 굴러가기 때문에 어느정도의 결합도가 생길 수 밖에 없다. 하지만 그렇다고 해서 바퀴를 교체하는데 핸들까지 교체해야 된다면 이건 과도하게 결합도가 높은 상태로, 자동차 설계부터가 잘못 되었다고 말할 수 있다.

 

즉, 어떤 객체가 다른 객체에 대해 너무 자세히 알고 있다면 두 모듈은 높은 결합도를 지니게 되고, 반대로 서로 꼭 필요한 지식만 가지고 있다면 두 모듈은 낮은 결합도를 가진다고 말할 수 있다.

 

결합도를 낮추면 각각의 확장에는 유리하고, 서로에게 영향은 주지 않으니(변경에는 닫혀있으니) 객체지향 5원칙 중 OCP(개방폐쇄원칙)을 구현하는 데 도움이 된다.

 

 

결합도의 단계에는 6단계가 있다.

 

내공부제스자 (내 공부 제 스스로 자알해요)

→ 이 쪽으로 갈 수록 결합도가 낮음, 즉 바람직한 방향

  • 내용 결합도: 한 모듈이 다른 모듈의 내부 기능 및 내부 자료를 직접 참조하거나 수정할 때의 결합도
  • 공유 결합도: 공유되는 공통 데이터 영역을 여러 모듈이 사용할 때의 결합도, 전역변수가 키워드
  • 외부 결합도: 어떤 모듈에서 선언한 데이터를 외부의 다른 모듈에서 참조할 때의 결합도
  • 제어 결합도: 어떤 모듈이 다른 모듈 내부의 논리적인 흐름을 제어하기 위해 신호를 전달하는 결합도
  • 스탬프 결합도: 모듈간의 인터페이스로 배열이나 레코드 등의 자료구조가 전달될 때의 결합도
  • 자료 결합도: 모듈간의 인터페이스가 자료 요소로만 구성될 때의 결합도

응집도?

응집도는 모듈의 내부 요소들이 서로 관련되어 있는 정도를 의미한다.

 

한 모듈이 하나의 기능(책임)을 갖고있는 것은 응집도가 높은 것이고, 한 모듈이 여러 기능을 갖고 있는 것은 응집도가 낮은 것이다. 응집도가 높은 모듈은 하나의 모듈 안에 함수나 데이터와 같은 구성 요소들이 하나의 기능을 구현하기 위해 필요한 것들만 배치되어 있고 긴밀하게 협력한다. 반대로 응집도가 낮은 모듈은 모듈 내부에 서로 관련 없는 함수나 데이터들이 존재하거나 관련성이 적은 여러 기능들이 서로 다른 목적을 추구하며 산재해 있다.

 

예를들어 쇼핑몰 프로젝트에서 주문 처리를 담당하는 클래스에서 회원의 정보를 업데이트하는 메서드가 있다면, 이것은 응집도가 낮은 것이다. 회원 정보 업데이트는 회원만 담당하는 클래스에서 따로 분리하여 처리하는것이 옳다.

 

이처럼 응집도가 높은 모듈은 기능을 수정할 때 관련 내용이 하나의 모듈에 모여있으므로 코드를 이해하기도 쉽고, 수정한 후 관련 없는 다른 모듈에겐 영향을 주지 않아 코드의 유지보수에 유리하다.

 

즉, 응집도가 높을 수록 독립성이 높은 모듈이며 좋은 소프트웨어는 높은 응집도(high cohesion)을 유지해야 한다.

 

응집도를 높일수록 하나의 책임만 지니는 방향으로 구현되기 때문에 객체지향 5원칙 중 SRP(단일책임원칙)을 만족시키는 데 도움이 된다.

 

 

응집도의 단계에는 7단계가 있다.

 

우논시절교순기(우리 논 시절 교문 앞 순대 기막혔는데)

→ 이 쪽으로 갈 수록 응집도가 높음, 즉 바람직한 방향

  • 우연적 응집도: 모듈 내부의 구성요소들이 서로 관련 없는 요소로만 구성된 경우의 응집도
  • 논리적 응집도: 유사한 성격을 갖거나 특정 형태로 분류되는 처리 요소들로 모듈이 형성되는 경우 응집도
  • 시간적 응집도: 특정 시간에 처리되는 기능들 응집도
  • 절차적 응집도: 모듈이 다수의 관련 기능을 가질 때 구성요소들이 순차적으로 수행할 경우 응집도
  • 교환적 응집도: 동일한 입력과 출력을 사용해 서로 다른 기능을 수행하는 요소들 응집도
  • 순차적 응집도: 하나의 활동으로부터 나온 출력데이터를 다음 활동의 데이터로 사용할 경우 응집도
  • 기능적 응집도: 모듈 내부의 모든 기능 요소들이 단일 문제와 연관되어 수행될 경우의 응집도

그럼 무조건 응집도는 높고 결합도는 낮게?

지금까지의 내용만 보면 무조건 결합도가 낮을수록, 응집도가 높을수록 좋아 보이는데 진짜 그럴까?라는 의문이 들었다.

 

정규화와 반정규화의 관계처럼 과하게 이상을 추구하다가는 오히려 불편한 경우가 있지는 않을까?

그래서 각각의 극단을 추구할 시에 예상되는 부작용에는 무엇이 있을 지 생각해보았다.

 

 

응집도(Cohesion)를 억지로 높이려고 할 경우 예상되는 부작용

 

응집도를 높인다는 것은 "하나의 모듈이 하나의 역할만 하도록 쪼갠다"는 뜻이다. 하지만 너무 잘게 쪼개면 다음과 같은 문제가 발생할 수 있을 것 같다.

 

1. 파편화(Fragmentation)와 문맥 상실

  • 클래스나 함수가 너무 작아져서, 코드를 읽을 때 "도대체 전체 로직은 어디에 있지?"라며 파일 여러 개를 왔다 갔다 해야 하는 경우가 생길 것이다.
  • 결국 로직의 흐름을 한 눈에 파악하기 힘들어지고 가독성에 부정적인 영향을 줄 것으로 사료된다.

 

2. 라이프사이클 관리의 복잡성

  • 지나치게 쪼개진 모듈들은 서로 데이터를 주고받아야 하므로, 객체를 생성하고 주입하고 관리하는 코드가 실제 구현과 관련된 로직보다 더 많아질 수도 있을 것 같다.

결국 내가 생각했을 때 응집도의 핵심은 작게 쪼개는 것이 아니라, 같이 변경되는 것들을 모아두는 것(같은 목적을 지닌 것)이라고 생각한다. 논리적으로 밀접한데도 억지로 분리하면 오히려 응집도가 깨진 상태가 될 것이다.

ex) 로그인 모듈 → 아이디 검증 모듈, 비밀번호 검증모듈, 이메일 검증 모듈…


 

결합도(Coupling)를 억지로 낮추려고 할 경우 예상되는 부작용

 

결합도를 낮춘다는 것은 결국 "모듈 간의 의존성을 끊거나 느슨하게 만드는 것(인터페이스 등 사용)"을 의미한다.

하지만 너무 낮추게 되면 다음과 같은 문제가 발생할 수 있을 것 같다.

 

1. 코드 찾기의 어려움

  • A 클래스에서 B 클래스의 기능을 쓸 때, 직접 호출하지 않고 Interface, Delegate, Protocol 등의 추상층을 거치게 한다.
  • 이 경우 코드를 분석할 때 'Command + 클릭'으로 이동하면, 실제 구현층이 아닌 텅 빈 추상층으로 이동될 수 있다. 즉 자세한 내용을 알고자 할 때 구현체를 찾는 데 리소스가 들 수 있다

2. 보일러플레이트 코드

  • 계층 간 결합을 끊기 위해, 내용은 똑같거나 비슷한데 이름만 다른 모델들(DTO, Entity, Presentation Model등)을 계속 만들고, 이를 변환(Mapping)하는 코드를 작성해야 한다.
  • 즉, 단순한 데이터 전달을 위해 작성해야 할 코드가 늘어나 생산성이 저하될 수 있을 것 같다.

결국 “이 코드가 얼마나 자주, 어떠한 이유로 변경되는가?” 를 중점적으로 봐야할 것 같은데, 프로젝트 상황에 맞춰서 팀원들과 어느 정도까지 단계를 높일 건지 상의하는 과정이 필요하다고 생각한다.


그렇다면 CoordinatorView 구조의 결합도와 응집도는 각각 몇 단계일까??

잠깐 코디네이터뷰가 뭔디요

 

결합도 (Coupling): 스탬프 결합도 (Stamp Coupling)

 

많은 MVVM-C예제에서는 ViewModel이 Coordinator를 소유하고 있는 구조를 채택하는 경우를 확인할 수 있다.(viewModel.coordinator.push(...))

이 경우 ViewModel과 화면 이동은 서로를 강하게 의존하게 된다. 따라서 ViewModel이 Coordinator의 함수를 직접 사용하며 흐름을 제어하는 것은 제어 결합도(Control Coupling)에 가깝다.

 

하지만 코디네이터뷰 구조의 경우 ViewModel은 오직 ViewRoute라는 Enum만 변경할 뿐, Coordinator가 전혀 존재하지 않으며 이 변경이 화면 이동을 유발하는지조차 알지 못한다.

ViewModel이 홀로 Enum 상태를 변경하고, 코디네이터뷰가 이를 감지하는 구조이므로 ViewModel은 화면 이동을 직접 제어하지 않는다. 따라서 제어의 주체가 분리되었기에 결합도를 제어 → 스탬프 단계로 낮추는 데 성공했다고 생각한다.

 

View 역시 Coordinator를 전혀 모르며 오직 ViewModel만 알고 ViewModel의 메서드만 호출한다. 실제 Coordinator 객체는 CoordinatorView라는 상위 객체가 관리하므로, 하위 View는 내비게이션 스택의 존재를 몰라도 된다.

 

물론 CoordinatorView 자체는 View, ViewModel, Coordinator를 모두 알아야 하므로 결합도가 높다.

이 부분이 여전히 고민이긴 하지만, 일단 이 뷰는 의존성 주입과 연결을 담당하는 계층의 역할로서 높은 결합도를 지닐 수 밖에 없다고 판단했다.

 

이 결과 ViewModel을 다른 곳에서 재사용하거나, 내비게이션 로직 없이 테스트하기가 매우 쉬워진다.

 


응집도 (Cohesion): 교환적 응집도 (Communicational Cohesion)

 

동일한 입력 데이터: CoordinatorView 내의 onChange 블록에 있는 코드들(case .recommend, case .detail ...)은 서로 다른 화면으로 이동시키는 기능들이지만, 모두 ViewRoute라는 동일한 입력 데이터(상태) 에 반응하여 실행된다.

동일한 출력 대상: 또한, 이들은 모두 Coordinator라는 동일한 객체를 사용하여 출력을 수행한다.

 

즉, "화면의 내비게이션 상태를 처리한다"는 목적하에 동일한 자원을 참조하는 작업들이 한곳에 모여 있으므로 교환적(통신적) 응집도에 해당한다고 판단했다.

 

또한 책임과 관련된 관점에서 바라보면, 각 객체가 최대한 단일 책임 원칙(SRP)을 추구하고 있다고 생각한다.

 

ViewModel: 오직 "데이터 처리"와 "상태 관리"에만 집중한다. "어떤 화면으로 이동할지"에 대한 로직이 전혀 없기에, 비즈니스 로직의 순도가 높아졌다.

Coordinator: 화면 전환의 매커니즘(path)와 관련된 코드만 집중한다.

CoordinatorView: ViewModel의 상태 변화를 감지하여 Coordinator의 행동으로 변환한다. 이전에는 View나 ViewModel 어딘가에 흩어져 있던 onChange, sheet, navigationDestination 같은 '연결 코드'들이 이곳에 하나로 응집되었다. 덕분에 내비게이션 흐름을 파악하려면 이 파일 하나만 보면 된다.

 

 

제미나이가 요약해준 내용

 

결합도: 기존의 ViewModel이 내비게이션 로직을 직접 제어하던 제어 결합도(Control Coupling) 구조를 탈피했습니다. 대신, ViewModel은 순수 데이터(State)만 제공하고 CoordinatorView가 이를 참조하는 스탬프 결합도(Stamp Coupling) 방식으로 개선하여, ViewModel의 독립성을 확보하고 테스트 용이성을 극대화했습니다.

 

응집도: UI 컴포넌트 곳곳에 산재되어 있던 화면 전환 로직을 CoordinatorView라는 하나의 계층으로 모았습니다. 이를 통해 동일한 상태(Route)를 참조하는 로직들이 한곳에서 관리되는 교환적 응집도(Communicational Cohesion)를 달성하여, 내비게이션 흐름의 가독성과 유지보수성을 크게 높였습니다.

 

이를 통해 ViewModel의 단위 테스트(Unit Test) 작성 시 Coordinator를 Mocking할 필요조차 없어졌으며, 순수한 비즈니스 로직 객체로서의 높은 응집도를 달성했습니다.

 

 

 


참고

https://inpa.tistory.com/entry/OOP-💠-객체의-결합도-응집도-의미와-단계-이해하기-쉽게-정리#

Comments