모바일 개발에서의 결합도(Coupling) — 핵심 개념, 유형 및 줄이는 방법

저자: IT Sectr 게시일: 2026-05-13 읽는 시간: 9 분

결합도(Coupling)는 애플리케이션의 한 모듈이 다른 모듈에 얼마나 의존하는지를 보여주는 지표입니다. Wikipedia에 따르면, 낮은 결합도(low coupling)는 잘 설계된 시스템의 표시이며, 모듈이 이웃을 손상시키지 않고 변경될 수 있습니다. 모바일 애플리케이션을 설계할 때 결합도 관리는 아키텍트의 주요 작업 중 하나입니다.

핵심 요점

  • Coupling — 모듈 간 의존도: 높음 = 강한 결합, 낮음 = 느슨한 결합
  • Content coupling — 최악의 유형, 한 모듈이 다른 모듈의 내부 데이터를 수정함
  • Data coupling — 최상의 유형, 모듈이 매개변수를 통해 단순 데이터만 교환함
  • Dependency Injection — 모바일 개발에서 결합도를 낮추는 주요 도구
  • 인터페이스와 추상화 — 애플리케이션 계층 간 결합도를 낮추는 주요 메커니즘

결합도란

결합도(Coupling)는 한 모듈이나 클래스가 다른 모듈과 얼마나 강하게 연결되어 있는지를 결정하는 지표입니다. 한 모듈이 다른 모듈의 내부 구조에 대해 더 많이 알수록 결합도가 높아지고 시스템을 변경하기 어려워집니다. 잘 설계된 아키텍처에서는 결합도가 최소화되어야 합니다 — 모듈은 엄격하게 정의된 인터페이스를 통해서만 상호 작용합니다.

결합도에는 두 가지 측면이 있습니다: 구심적(들어오는 의존성 — 얼마나 많은 모듈이 이 모듈에 의존하는지)과 원심적(나가는 의존성 — 이 모듈이 얼마나 많은 모듈에 의존하는지)입니다. 이러한 지표를 분석하면 한 모듈의 변경이 다른 많은 모듈에 영향을 미치는 아키텍처의 핫스팟을 식별하는 데 도움이 됩니다. IntelliJ Dependency Analyzer 및 Xcode Graph와 같은 도구가 이러한 연결을 시각화합니다.

제로 결합도는 불가능하다는 점을 이해하는 것이 중요합니다 — 모듈은 어떤 방식으로든 상호 작용해야 하며, 그렇지 않으면 시스템이 아니라 격리된 프로그램의 집합에 불과합니다. 아키텍트의 임무는 결합도를 관리 가능하고 투명하게 만드는 것입니다. 이상적인 상태: 모듈은 인터페이스를 통해서만 상호 작용하고 단순한 데이터만 전달하며 서로의 내부 구조를 알지 못합니다. 이를 느슨한 결합(loose coupling)이라고 합니다.

결합도 유형 (약한 것에서 강한 것까지)

6가지 유형의 결합도가 최상에서 최악까지의 척도를 형성합니다. 이 척도를 이해하면 기존 코드를 평가하고 리팩토링 방향을 선택하는 데 도움이 됩니다. 대부분의 모바일 프로젝트에는 혼합된 결합도 유형이 있으며, 아키텍트의 임무는 강한 유형을 약한 유형으로 점진적으로 대체하는 것입니다.

Data coupling — 최상의 유형

Data coupling(데이터 결합) — 모듈이 메서드 매개변수를 통해 단순한 데이터만 교환합니다. 모듈 A가 모듈 B의 메서드를 호출하고, 프리미티브나 단순한 구조를 전달하며 결과를 받습니다. 모듈 A는 B가 내부적으로 어떻게 구현되는지 알지 못합니다. 이것이 가장 바람직한 결합도 유형입니다: 변경의 영향을 최소화합니다.

예: EmailValidator.isValid(email: String): Boolean. 소비자 클래스는 문자열을 전달하고 부울 값을 받으며, 검증기 내부의 정규식이나 검증 규칙에 대해 전혀 알지 못합니다. 검증 로직을 변경해도 소비자를 변경할 필요가 없습니다 — 결합도가 최소화됩니다. Data coupling은 애플리케이션의 모든 공개 인터페이스의 목표입니다.

Stamp coupling — 허용 가능하지만 이상적이지 않음

Stamp coupling(스탬프 결합) — 모듈이 복합 객체를 교환하지만 필드의 일부만 사용합니다. 모듈 A가 calculateDiscount 메서드에 User 객체를 전달하고, 해당 메서드는 user.status만 사용합니다. 문제: User 구조가 변경되면(필수 필드가 추가됨) calculateDiscount 모듈은 변경되지 않지만 User 객체를 생성하는 소비자는 변경됩니다.

실제로 스탬프 결합은 피할 수 없으며 전달된 객체가 표준 데이터 모델(Entity)인 경우 허용 가능합니다. 문제는 모듈이 단일 필드 하나 때문에 전체 객체를 받을 때 발생합니다. 이러한 경우 특정 값을 직접 전달하는 것이 더 좋습니다(data coupling). 해결책은 수신 측의 필드 사용을 분석하는 것입니다.

Control, External, Common 및 Content 결합

Control coupling(제어 결합) — 한 모듈이 다른 모듈에 동작을 제어하는 플래그를 전달합니다(calculate(useNewAlgorithm: Boolean)). 이것은 스탬프 결합보다 더 나쁩니다. 소비자 모듈이 호출된 모듈의 내부 동작 변형을 알아야 하기 때문입니다. 해결책: 메서드를 두 개로 분할 — calculateWithNewAlgorithm()과 calculateWithLegacyAlgorithm().

External coupling(외부 결합) — 모듈이 외부 프로토콜, 데이터 형식 또는 API에 의존합니다. 동일한 JSON을 파싱하거나 동일한 데이터베이스로 작업하는 모든 모듈은 외부 결합을 가집니다. 완전히 피할 수는 없지만 격리할 수 있습니다: 외부 형식과 내부 모델 사이에 매핑 계층을 만듭니다. Common coupling(공통 결합) — 모듈이 공통 전역 상태를 공유합니다. Content coupling(내용 결합) — 최악의 유형으로, 한 모듈이 다른 모듈의 내부 데이터를 직접 수정합니다.

결합 유형수준설명
Data최상매개변수를 통한 단순 데이터 전달
Stamp허용 가능부분 사용을 동반한 객체 전달
Control중간플래그를 통한 동작 제어
External높음외부 프로토콜/형식에 대한 의존
Common매우 높음전역 상태 공유
Content허용 불가모듈 내부 데이터 직접 수정

data(이상)에서 content(재앙)까지의 결합도 척도는 코드 리뷰를 위한 실용적인 도구입니다. 프로젝트에서 common이나 content 결합을 발견하면 — 그것들은 리팩토링의 최우선 대상입니다. Data와 stamp 결합은 허용 가능하며 모든 프로젝트에 존재하지만 그 양은 통제되어야 합니다.

모바일 개발에서 결합도가 중요한 이유

높은 결합도는 개발을 느린 프로세스로 만들어 모든 변경 시 수십 개의 잠재적으로 손상된 모듈을 확인해야 합니다. 이는 모바일 개발에서 특히 중요합니다: 플랫폼은 매년(Android API Level, iOS SDK), 라이브러리는 분기별, 비즈니스 요구사항은 지속적으로 업데이트됩니다. 느슨한 결합만이 지속적인 회귀 없이 이러한 변경의 흐름을 처리할 수 있는 방법입니다.

실제 예: 모든 화면이 직접 NetworkingManager와 DatabaseManager를 가져오는 모바일 앱. HTTP 클라이언트를 Retrofit에서 Ktor(Android) 또는 URLSession에서 Alamofire(iOS)로 교체할 때 개발자는 모든 화면을 수정해야 합니다. 낮은 결합도에서는 NetworkDataSource 인터페이스 뒤에 숨겨진 하나의 구현만 변경하면 됩니다 — 소비자는 교체를 인지하지 못합니다.

단위 테스트에 대한 결합도의 영향도 엄청납니다. 높은 결합도를 가진 클래스(생성자에서 직접 의존성 생성)는 격리된 상태로 테스트할 수 없습니다 — 데이터베이스, 네트워크 및 UI를 함께 끌고 옵니다. 이러한 클래스를 테스트하려면 에뮬레이터를 시작하고 통합 테스트를 기다려야 합니다. 낮은 결합도를 가진 클래스는 생성자 주입을 통해 의존성을 받아들이고 쉽게 모킹할 수 있습니다.

kotlin
// 높은 결합도 — 클래스가 자체적으로 의존성 생성
class ProfileViewModelHigh {
    private val api = RetrofitApi()
    private val db = RoomDatabase.getInstance()
    private val cache = MemoryCache()
}

// 낮은 결합도 — 의존성이 생성자를 통해 전달됨
class ProfileViewModelLow(
    private val api: ApiService,
    private val db: DatabaseService,
    private val cache: CacheService
)

첫 번째 경우 ProfileViewModelHigh는 특정 구현에 강하게 결합되어 있습니다 — Retrofit을 Ktor로 교체하려면 ViewModel 코드를 변경해야 합니다. 두 번째 경우 ProfileViewModelLow는 인터페이스에만 의존하며, 그 구현은 외부에서 제공됩니다. 두 번째 클래스의 테스트는 간단합니다: 모킹 구현을 전달하고 에뮬레이터 없이 로직을 검증합니다.

결합도 줄이기 패턴

의존성 역전 원칙(SOLID의 D)은 결합도 감소의 기초입니다. 이 원칙은 구체적인 구현이 아닌 추상화에 의존하도록 지시합니다. 클래스가 직접 RetrofitApi 객체를 생성하는 대신 ApiService 인터페이스를 받아야 합니다. 이렇게 하면 의존성이 특정 라이브러리에서 추상화 수준으로 이동하여 소비자를 변경하지 않고도 교체할 수 있습니다.

Observer 패턴(또는 반응형 버전 — StateFlow, Combine Publishers)은 데이터 소스와 구독자 간의 결합도를 줄입니다. 구독자는 데이터가 어디서 오는지 알지 못하며 — 단순히 변경에 반응합니다. 이는 발신자와 수신자를 분리합니다: 기존 구독자를 변경하지 않고 새 데이터 소스를 추가할 수 있습니다. EventBus와 SharedFlow도 동일한 원리로 작동합니다.

Bridge 패턴은 추상화와 구현을 분리하여 독립적으로 변경할 수 있게 합니다. 모바일 개발에서 Bridge는 예를 들어 플랫폼 종속 모듈에 사용됩니다: iOS(Kingfisher, Nuke)와 Android(Glide, Coil)용 서로 다른 구현을 가진 공통 ImageLoader 인터페이스. ImageLoader로 작업하는 코드는 선택한 라이브러리에 의존하지 않으며 구현만 변경하면 교체할 수 있습니다.

의존성 주입을 통한 결합도 관리

의존성 주입(DI)은 모바일 개발에서 결합도를 줄이는 가장 실용적인 도구입니다. 클래스가 자체적으로 의존성을 생성하는 대신 DI 컨테이너(Android용 Hilt, Koin, Dagger; iOS용 Swinject, Factory)가 외부에서 의존성을 제공합니다. 클래스는 생성자, 메서드 또는 프로퍼티 주입을 통해 의존성을 받으며 구체적인 구현을 알지 못합니다.

DI는 클래스의 의존성을 명시적으로 문서화합니다: 생성자를 보기만 해도 클래스가 어떤 모듈과 상호 작용하는지 이해할 수 있습니다. 생성자가 다른 계층에서 8개의 매개변수를 받는다면 — 이것은 과도한 결합도의 신호이며 리팩토링이 필요합니다. 좋은 관행은 클래스당 3~4개 이하의 의존성입니다. 그 이상은 단일 책임 원칙 위반과 과도한 결합도를 나타냅니다.

DI는 또한 테스팅을 간소화합니다: 각 테스트에 대해 모킹 의존성을 가진 클래스를 생성하며 실제 데이터베이스나 네트워크가 필요하지 않습니다. Flutter에서는 Provider, Riverpod 또는 GetIt을 통해 DI가 구현됩니다. 프레임워크에 관계없이 목표는 하나입니다: 의존성을 명시적이고 교체 가능하게 만들어 모듈 간 결합도를 줄이는 것입니다. 모바일 프로젝트에서 DI의 사용은 2020년대부터 사실상의 표준이 되었습니다.

swift
// DI 컨테이너가 의존성 그래프를 구축
protocol AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User
}

final class AuthService: AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User {
        // 구현
    }
}

// ViewModel은 특정 서비스를 알지 못함 — 프로토콜만
final class LoginViewModel {
    private let auth: AuthServiceProtocol

    init(auth: AuthServiceProtocol) {
        self.auth = auth
    }
}

// DI Container는 구체적인 유형이 생성되는 유일한 장소
final class DIContainer {
    lazy var authService: AuthServiceProtocol = AuthService()
    lazy var loginViewModel: LoginViewModel {
        LoginViewModel(auth: self.authService)
    }
}

여기서 LoginViewModel은 특정 AuthService가 아닌 AuthServiceProtocol 프로토콜에만 의존합니다. 구현을 교체할 때(예: Firebase Auth에서 사용자 정의 서버로 전환) DIContainer만 변경하면 됩니다. AuthServiceProtocol의 모든 소비자는 영향을 받지 않습니다 — 추상화와 DI를 통해 결합도가 최소화됩니다.

자주 묻는 질문

결합도와 응집도의 차이는 무엇인가요?

응집도는 모듈 내부의 일관성을 측정하는 반면, 결합도는 모듈 간의 외부 연결성을 측정합니다. 좋은 아키텍처는 높은 응집도와 낮은 결합도를 지향합니다. 이 두 지표는 반비례합니다: 응집도를 높이면 일반적으로 결합도가 낮아지고 그 반대도 마찬가지입니다.

프로덕션 코드에서 어떤 결합도 유형이 허용되나요?

Data와 stamp는 정상이며 모든 프로젝트에 존재합니다. Control 결합은 제한된 시나리오(예: strategy 패턴)에서 허용됩니다. External 결합은 외부 API 작업 시 불가피하지만 매핑 계층 뒤에 격리되어야 합니다. Common과 content 결합은 즉각적인 리팩토링이 필요한 아키텍처 문제의 신호입니다.

프로젝트에서 결합도를 어떻게 측정하나요?

정적 분석 도구: IntelliJ IDEA Dependency Matrix, Xcode Graph, Gradle Dependencies 리포트, SonarQube. 지표: 구심적 결합도(Ca), 원심적 결합도(Ce), 불안정성(Ce/(Ca+Ce)). 높은 불안정성(1에 가까움)은 모듈이 변경하기 쉽고 참조하는 것이 적다는 것을 의미합니다 — 이는 좋은 것입니다.

낮은 결합도가 해로울 수 있나요?

매우 낮은 결합도는 코드 탐색을 복잡하게 만드는 과도한 수의 추상화와 인터페이스를 의미할 수 있습니다. 모든 클래스에 대해 별도의 인터페이스가 생성되면 프로그래머는 파일 간 이동에 시간을 낭비합니다. 균형: 모듈의 외부 API에는 인터페이스를, 그러나 모든 내부 헬퍼 클래스에는 불필요합니다.

레거시 코드 작업 시 결합도를 어떻게 줄이나요?

Strangler Fig 기법을 사용합니다 — 직접 호출을 점진적으로 인터페이스로 대체합니다. 가장 자주 참조되는 클래스의 인터페이스 추출부터 시작합니다. 그런 다음 DI 컨테이너를 도입합니다. 격리된 코드를 특성화 테스트로 커버하여 리팩토링이 시스템 동작을 변경하지 않도록 확인합니다.

요약

  • Coupling — 모듈 간 의존도 지표: 느슨한 결합이 좋은 아키텍처의 목표
  • Data coupling — 최상의 유형, content coupling — 최악, 프로덕션 코드에서 허용 불가
  • 의존성 역전과 인터페이스 — 결합도 감소의 주요 메커니즘
  • 의존성 주입 — 의존성을 명시적이고 교체 가능하게 만드는 실용적 도구
  • 높은 결합도는 코드를 취약하게 함: 하나의 변경이 많은 모듈을 손상
  • 낮은 결합도는 테스트를 단순화: 각 모듈을 에뮬레이터 없이 독립적으로 모킹
  • 균형 유지 — 과도한 인터페이스는 코드를 복잡하게 함

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기