응집도는 하나의 모듈이나 클래스 내의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타내는 메트릭입니다. Wikipedia에 따르면, 높은 응집도는 잘 설계된 모듈의 특징으로, 모든 메서드와 필드가 하나의 작업을 위해 동작합니다. 응집도는 코드 유지보수성에 직접적인 영향을 미치며 결합도(coupling) — 모듈 간의 상호 연결성 — 와 대비됩니다.
핵심 요점
응집도는 하나의 클래스나 모듈 내의 메서드, 필드, 속성이 논리적으로 얼마나 연결되어 있는지를 평가하는 메트릭입니다. 응집도가 높은 모듈은 하나의 작업을 수행하고 이를 완료하는 데 필요한 요소만 포함합니다. 응집도가 낮은 모듈은 한 번에 여러 가지를 시도하며 — 메서드 간 의미적 연관성이 약합니다.
객체지향 프로그래밍의 맥락에서 응집도는 단일 책임 원칙(S)과 밀접하게 관련됩니다. 클래스가 하나의 명확한 책임을 가지면 응집도는 일반적으로 높습니다. 클래스가 UI, 비즈니스 로직, 네트워킹을 동시에 처리하면 — 응집도가 낮으며, 이러한 클래스는 더 좁은 책임을 가진 여러 개의 별도 클래스로 분할해야 합니다.
응집도를 이해하면 개발자가 리팩토링 결정을 내리는 데 도움이 됩니다. 클래스에서 해당 클래스의 어떤 필드도 사용하지 않는 메서드를 보게 되면, 이는 낮은 응집도의 신호입니다. 이러한 메서드는 클래스 내에 잘못 배치되었거나 클래스 설계가 잘못된 것입니다. 높은 응집도를 추구하는 것은 코드의 모든 수준에서 아키텍처를 개선하려는 지속적인 노력입니다.
소프트웨어 공학에서는 응집도의 7가지 수준을 최악에서 최고까지 구분합니다. 이 척도를 이해하면 모듈의 품질을 객관적으로 평가하고 리팩토링 방향을 결정할 수 있습니다. 수준이 높을수록 코드의 유지보수성과 이해도가 향상됩니다.
우연적 — 최악의 수준으로, 모듈 내 요소들이 논리적 연결 없이 무작위로 그룹화됩니다. 예: 날짜 형식 지정, 이메일 전송, 할인 계산 메서드를 가진 Utilities 클래스. 이러한 클래스는 모든 메서드를 읽지 않고는 이해할 수 없으며, 하나의 메서드를 변경하면 단지 함께 있다는 이유로 다른 메서드가 깨질 수 있습니다.
논리적 — 요소들이 논리적으로 관련되어 있지만 근본적으로 다른 작업을 수행합니다. parseJSON, parseXML, parseCSV 메서드를 가진 클래스는 “파싱”이라는 주제로 논리적으로 연결되어 있지만, 각 메서드는 근본적으로 다른 작업을 합니다. 문제: 새 형식(YAML)이 추가되면 클래스가 커지고 인터페이스가 비대해집니다.
시간적 — 요소들이 실행 시간별로 그룹화됩니다. 데이터베이스를 설정하고, 구성을 로드하고, 분석을 초기화하는 AppInitializer 클래스 — 이 모든 것은 앱 시작 시 발생하지만 작업 자체는 관련이 없습니다. 책임 영역별로 별도의 Initializer로 분할하는 것이 좋습니다.
절차적 — 요소들이 실행 순서로 결합될 때 발생합니다. “주문 처리” 모듈에는 validateCart, processPayment, sendConfirmation 메서드가 포함되며 — 각 메서드는 이전 메서드 이후에 엄격히 호출됩니다. 이는 우연적 또는 논리적 응집도보다 낫지만 여전히 이상적이지 않습니다: 각 단계를 별도 모듈로 추출할 수 있습니다.
통신적 — 요소들이 동일한 데이터로 작업합니다. getUser, updateUser, deleteUser 메서드를 가진 UserService 클래스는 공통 User 엔터티로 결합됩니다. 이는 절차적 응집도보다 훨씬 우수하며 클래스에 명확한 도메인이 있습니다. 모바일 프로젝트의 대부분 Repository 클래스는 통신적 응집도를 가집니다.
기능적 — 가장 높은 수준으로, 모듈의 모든 요소가 단일 작업 수행에 참여합니다. 길이, 문자 존재 여부, 비밀번호 복잡성을 확인하는 단일 validate 메서드를 가진 PasswordValidator 클래스는 기능적 응집도의 예입니다. 이러한 클래스가 변경되는 경우는 비밀번호 검증 규칙이 변경된 경우뿐입니다.
기능적 응집도 달성은 아키텍처 리팩토링의 주요 목표입니다. 각 클래스는 변경 이유가 정확히 하나여야 합니다. 모바일 개발에서 기능적 응집도는 별도의 Use Cases, 사용자 정의 View, 포맷터, 검증기를 추출하여 달성됩니다. 이러한 각 클래스는 명확한 책임 영역을 가진 완전한 구성 요소입니다.
응집도와 결합도는 같은 품질의 양면입니다. 모듈 내 응집도가 높을수록 모듈 간 결합도는 낮아지는 경향이 있습니다. 잘 설계된 시스템은 높은 내부 응집도와 느슨한 외부 결합도를 동시에 추구합니다. 이 원칙은 1970년대부터 소프트웨어 공학에서 기본으로 인정받아 왔습니다.
응집도-결합도 관계는 균형으로 생각할 수 있습니다. 개발자가 여러 작업을 하나의 클래스에 결합하여 응집도를 희생하면, 이웃 모듈은 더 많은 의존성을 갖게 됩니다 — 다양한 목적으로 이 과부하된 클래스에 접근해야 하므로 결합도가 증가합니다. 반대로, 작고 응집도 높은 클래스로 분할하면 모듈 간 상호작용 지점이 줄어듭니다.
실제로 이는 다음을 의미합니다: 기능적 응집도를 가진 새 클래스를 추출하면 동시에 다른 모듈이 구현 세부 사항을 알 필요가 없어집니다. 예를 들어, EncryptionManager를 기능적 응집도를 가진 별도 클래스로 추출하면 다른 모듈은 암호화 알고리즘의 세부 사항을 이해할 필요 없이 간단한 encrypt/decrypt 인터페이스를 얻습니다.
// 낮은 응집도 — 클래스가 한 번에 모든 것을 수행
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// 높은 응집도 — 각 클래스가 하나의 작업을 해결
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
예제는 차이점을 보여줍니다: UserManager는 논리적 응집도를 가집니다 — 모든 메서드가 사용자에 관한 것이지만 각각 근본적으로 다른 작업을 합니다. 리팩토링 후 각 클래스는 기능적 응집도를 가지며, 다른 모듈이 필요한 클래스에만 의존하므로 결합도가 감소합니다.
LCOM(메서드 응집도 부족)은 클래스 응집도를 측정하는 가장 잘 알려진 메트릭입니다. LCOM은 공통 필드를 공유하지 않는 메서드 쌍의 수를 계산합니다. 값 0은 이상적인 응집도(모든 메서드가 동일한 필드로 작업)를 의미하고, 높은 값은 낮은 응집도를 나타냅니다. LCOM4(개선된 버전)는 다른 메서드를 통한 전이적 연결을 고려합니다.
Android 개발에서 Detekt의 TooManyFunctions 규칙을 통해 응집도 메트릭을 얻을 수 있습니다. 다른 필드 그룹을 사용하는 수십 개의 메서드를 가진 클래스는 낮은 응집도를 가질 가능성이 높습니다. iOS에서 SwiftLint는 file_length와 function_body_length 규칙을 가지고 있습니다 — 간접적 지표: 긴 파일과 메서드는 종종 낮은 응집도를 나타냅니다.
수동 평가 방법: “이 클래스는 하나의 이유로 변경됩니까, 아니면 여러 이유로 변경됩니까?” 질문합니다. 둘 이상의 독립적인 이유를 말할 수 있다면 — 클래스의 응집도가 낮은 것입니다. 두 번째 테스트: “이 클래스를 두 개의 독립적인 클래스로 분할할 수 있습니까?” 그렇다면 — 실행하세요. 코드 리뷰에서 정기적으로 응집도를 확인하면 갓 클래스를 방지하고 기술 부채를 줄일 수 있습니다.
첫 번째 단계 — 단일 책임 원칙을 적용합니다. 각 클래스는 하나의 명확한 책임을 가져야 합니다. 클래스에 주요 작업과 관련 없는 메서드가 있으면 별도 클래스로 추출합니다. IDE의 Extract Class 또는 Extract Delegate 기법이 이 과정을 자동화합니다. 추출 후 원래 클래스가 더 집중되었는지 확인합니다.
두 번째 단계 — Facade 패턴을 사용하여 인터페이스를 단순화합니다. 클래스가 20개의 메서드를 제공하지만 클라이언트가 3-4개만 사용하는 경우, 클래스의 응집도가 낮을 수 있습니다 — 너무 다양한 기능을 제공합니다. 메서드를 주제별로 그룹화하고, 각 그룹에 대해 별도 클래스를 추출한 후 원래 클래스를 퍼사드로 만들거나 제거합니다.
세 번째 단계 — 필드 그룹에 주목합니다. 클래스에 메서드의 하위 집합에서만 사용되는 필드가 있다면 — 이는 낮은 응집도의 지표입니다. 필드 그룹별로 클래스를 분할합니다. 예를 들어, 클래스에 userRepository, networkClient, analyticsTracker 필드가 있지만 첫 번째 메서드 그룹은 userRepository만 사용하고 두 번째는 networkClient를 사용하는 경우 — 이는 두 개의 다른 클래스입니다.
네 번째 단계 — 임의의 static 메서드를 가진 “utility” 클래스 생성을 피하는 것입니다. Utils나 Helpers 클래스에 있는 모든 static 메서드는 전문 클래스로 추출할 후보입니다. FormatUtils.dateToString은 DateFormatter로, ValidationUtils.isValidEmail은 EmailValidator로 이동하는 것이 좋습니다. 이렇게 하면 각 클래스의 응집도가 높아지고 코드가 자체 문서화됩니다.
자주 묻는 질문
거의 항상 그렇습니다. 기능적 응집도는 코드를 명확하고 예측 가능하게 만듭니다. 그러나 극단으로 치닫으면 과도한 분할이 발생할 수 있습니다 — 모든 작업에 대해 별도 클래스를 만들어 아키텍처가 지나치게 복잡해집니다. 균형은 기능당 몇 개의 클래스를 두고 각각 기능적 응집도를 가지는 것입니다.
응집도는 단일 모듈이나 클래스 내의 내부 일관성 메트릭입니다. 모듈성은 애플리케이션을 물리적 모듈로 분할하는 아키텍처 원칙입니다. 높은 응집도는 개별 클래스와 전체 모듈 모두를 설계할 때의 목표입니다.
Android용 Detekt와 iOS용 Xcode Analyzer는 메서드나 필드 수가 의심스러울 정도로 많은 클래스를 강조 표시합니다. IntelliJ IDEA와 AppCode에는 의존성 시각화 기능이 있어 — 연결 그래프를 보고 낮은 응집도를 가진 클래스를 발견할 수 있습니다. SonarQube는 LCOM 메트릭을 자동으로 계산합니다.
네. connect, disconnect, isConnected 메서드를 가진 인터페이스는 높은 응집도를 가집니다 — 모든 메서드가 연결 관리와 관련됩니다. connect, parseData, renderUI 메서드를 가진 인터페이스는 낮은 응집도를 가집니다. 인터페이스 분리 원칙(SOLID)은 높은 응집도를 가진 좁게 초점 맞춰진 인터페이스를 만들 것을 요구합니다.
세 가지 질문을 합니다: 클래스의 목적을 한 문장으로 설명할 수 있나요? 모든 메서드가 이 목적을 지원하나요? 일부 메서드에서 사용되지 않는 필드가 있나요? 어떤 질문에 대한 답변이 “아니오”라면 — 응집도가 낮은 것이며 클래스를 분할해야 합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.