Reddit

Back to Coupling and Cohesion

결합도와 응집도로 돌아가기 — 소프트웨어 설계 원칙을 수치로 읽는 방법

SOLID, DRY, KISS 같은 설계 원칙을 따로 암기하기보다 결합도와 응집도를 관리하는 문제로 바라보자고 제안합니다. 실제 작업에서 변경에 관여한 컴포넌트와 관심사의 노력 분포를 기록해 결합도와 응집도를 계산하고, 같은 프로젝트의 변화 추적과 기술 부채 작업의 근거로 활용하는 방법을 설명합니다.

AI 요약

이 글은 SOLID, GRASP, KISS 같은 설계 원칙을 문구 그대로 적용하다가 불필요한 추상화와 복잡성을 만드는 문제에서 출발합니다. 저자는 여러 설계 원칙이 결국 시스템 각 수준에서 결합도와 응집도를 관리하는 방법으로 정리된다고 봅니다. 원칙의 이름을 외우거나 특정 패턴을 숭배하기보다, 어떤 변경이 얼마나 많은 컴포넌트와 관심사에 퍼지는지 살펴야 한다는 주장입니다.

결합도와 응집도의 뜻

결합도는 한 컴포넌트의 변경이 다른 컴포넌트의 변경을 요구하는 정도입니다. 응집도는 컴포넌트 안의 여러 부분이 하나의 목적에 얼마나 가깝게 맞물리는지를 나타냅니다. 예를 들어 정수 배열만 정렬하는 함수는 정수 타입에 결합되어 있습니다. 추상화한 Comparable 계약을 받도록 만들면 특정 원소 타입과의 결합은 줄어들지만 Comparable이라는 계약과는 여전히 연결됩니다. 함수가 배열 정렬만 담당하고 변환이나 업무 규칙 검증을 함께 처리하지 않는다면 응집도는 높습니다.

두 값은 절대적인 속성이 아닙니다. 어떤 변경을 기준으로 삼는지, 컴포넌트를 어디까지 나누는지, 어떤 관계를 문제로 보는지에 따라 결과가 달라집니다. 결합은 단방향일 수도 양방향일 수도 있습니다. 결합도와 응집도를 여러 유형으로 분류할 수 있지만, 실제 작업에서는 분류 체계를 외우기보다 변경 작업에 드러난 마찰을 측정하는 편이 유용하다고 설명합니다.

실제 작업에서 수치화하기

프로젝트에 결합도와 응집도 문제가 있다면 일정 기간 작업한 개발자가 정량 지표 없이도 문제를 감지하는 경우가 많습니다. 그래도 보고서에 수치를 제시하거나 리팩터링 작업을 정당화해야 한다면, 추상적인 코드 구조 대신 완료한 작업에서 통계를 모으는 방법을 쓸 수 있습니다.

결합도는 하나의 작업을 처리하면서 건드린 컴포넌트 수와 각 컴포넌트에 투입한 노력의 분포를 기록합니다. 응집도는 하나의 컴포넌트를 수정할 때 고려해야 했던 서로 다른 관심사의 수와 각 관심사에 투입한 노력의 분포를 기록합니다. 노력은 백분율, 시간, 일관된 상대 척도 가운데 상황에 맞는 단위를 사용하면 됩니다.

각 항목의 노력 비율을 p_i라고 하면 글에서 제시하는 결합도 계산은 1에서 각 p_i의 제곱 합을 뺀 값입니다. 응집도는 같은 분포에서 각 p_i의 제곱을 더한 값으로 계산합니다. 결합도는 0 이상 1 미만이며, 여러 컴포넌트에 노력이 고르게 퍼질수록 1에 가까워집니다. 응집도는 0 초과 1 이하이며, 하나의 관심사에 노력이 집중될수록 1에 가까워집니다. 이 계산은 Gini-Simpson index, Herfindahl-Hirschman index, Simpson index로 알려진 다양성과 집중도 측정 방식과 연결됩니다.

예를 들어 비밀번호 길이 규칙이 회원가입 폼과 비밀번호 변경 폼에 각각 구현되어 있다고 가정합니다. 규칙을 바꾸려면 회원가입 폼에 4, 비밀번호 변경 폼에 6만큼의 노력이 필요합니다. 노력 비율은 0.4와 0.6이므로 결합도는 1 - 0.4² - 0.6² = 0.48입니다. 각 폼에서는 비밀번호 길이 검증이라는 관심사 하나만 다루므로 두 폼의 응집도는 각각 1입니다.

여기서 결합도와 응집도의 공식이 보완적인 형태라고 해서 두 값이 서로 보완 관계에 있거나 합이 1이 되는 것은 아닙니다. 결합도는 하나의 변경에 참여한 컴포넌트 분포를 보고, 응집도는 변경된 각 컴포넌트 안의 관심사 분포를 보기 때문입니다. 두 지표는 기능, 프로젝트, 도메인, 모듈 등 원하는 범위로 집계할 수 있습니다. 단순 평균이 항상 적절하지는 않습니다. 작업 노력, 컴포넌트 크기, 다른 관련 요소를 가중치로 삼아야 할 때도 있습니다.

컴포넌트의 경계도 측정 목적에 맞춰 정합니다. 같은 파일에 있는 두 함수가 함께 바뀌는 일이 불편하지 않다면 별도 컴포넌트로 세지 않을 수 있습니다. 반대로 서로 다른 애플리케이션을 매번 함께 재배포해야 한다면 두 애플리케이션을 별도 컴포넌트로 보고 결합도를 계산합니다. 티켓 속성이나 스프레드시트에 작업별 값을 기록하면 프로젝트 안에서 결합도와 응집도가 시간에 따라 어떻게 움직이는지 볼 수 있습니다. 다른 프로젝트와 직접 비교하거나 코드 품질의 전체를 대표하는 값으로 쓰자는 제안은 아닙니다.

낮은 결합도와 높은 응집도

낮은 결합도와 높은 응집도를 지향하면 현재 작업에 필요한 대상에 집중하고 관련 없는 컴포넌트와 관심사에 분산되는 일을 줄일 수 있습니다. 저자는 이런 집중이 현재 변경뿐 아니라 이후 변경에도 영향을 줘 버그와 추가 작업을 줄이고 안정성과 유지보수성을 높인다고 설명합니다. 결합도와 응집도를 개선하는 목적은 숫자 자체가 아니라 코드 변경과 진화를 더 쉽게 만드는 데 있습니다.

설계 원칙을 결합도와 응집도로 해석하기

Single Responsibility Principle(SRP)은 클래스에 변경 이유를 하나만 두자는 원칙이므로 높은 응집도를 직접 표현합니다. Open/Closed Principle(OCP)은 기존 엔티티를 수정하는 대신 확장해 기능을 추가하므로 기존 기능과 새 기능의 결합을 줄이고 기존 응집도를 보존합니다.

Liskov Substitution Principle(LSP)은 기본 클래스 사용 코드가 파생 클래스를 특별 취급하지 않도록 요구합니다. 특정 서브클래스를 처리하는 분기가 생기면 사용 코드가 구현에 결합되기 때문입니다. Interface Segregation Principle(ISP)은 사용하지 않는 메서드에 의존하도록 강요하지 말자는 원칙입니다. 서로 무관한 책임을 담은 거대한 인터페이스를 피하면 클라이언트와 기능 사이의 불필요한 의존을 줄이고 인터페이스 응집도를 높일 수 있습니다.

Dependency Inversion Principle(DIP)은 구체 구현이 아니라 추상화에 의존하자는 원칙입니다. 구체 구현에 의존하면 추상화된 기능뿐 아니라 바뀔 수 있는 구현 세부사항까지 함께 의존하게 됩니다. DRY는 같은 지식이 여러 곳에 복제될 때 한 곳의 변경이 다른 모든 복제본의 변경을 요구하는 논리적 결합이 생긴다고 설명합니다.

Law of Demeter는 직접 의존하는 대상과만 상호작용하자는 원칙입니다. 의존 대상의 의존 대상까지 탐색하면 내부 구조와 추가 인터페이스에 결합됩니다. 동시에 자신의 관심사 바깥에 있는 관계 탐색까지 맡게 되어 응집도가 낮아질 수 있습니다. KISS는 복잡성을 동시에 기억해야 하는 정보의 양으로 본다면, 낮은 결합도와 높은 응집도가 그 양을 줄여 시스템을 단순하게 만든다고 해석합니다.

적용 범위와 한계

추상화를 늘리면 CPU와 메모리 비용이 생길 수 있습니다. 대부분의 애플리케이션에서는 무시할 정도지만 임베디드 시스템이나 고성능·저지연 시스템에서는 문제가 될 수 있으므로 실제 제약을 우선해야 합니다. 한 번 실행하고 폐기할 스크립트나 Proof of Concept(PoC)도 이후 변경 가능성이 낮다면 결합도와 응집도를 개선하는 추가 작업이 정당화되지 않을 수 있습니다.

따라서 원칙을 지키기 위해 불필요한 추상화를 도입해서는 안 됩니다. 앞으로 변경될 가능성과 실제 제약을 고려해 필요한 수준까지만 낮은 결합도와 높은 응집도를 추구해야 합니다. 글의 댓글에서는 이 측정값이 실제 리팩터링과 품질 작업을 설득하는 데 쓸모 있는지, 아니면 프로젝트를 이해하거나 비교하는 데 의미가 없는지 논쟁이 이어집니다.

Reddit 반응

  • @u/areklanga — 요약하면 결합도와 응집도를 실제로 적용하는 방법, 의미 있는 정량 지표, 그리고 소프트웨어 설계 원칙 뒤에 있는 공통 기반을 다룬 글입니다. 개인적으로 덧붙이면 이 글은 AI가 생성한 글이 아니므로 삭제하거나 기존 아이디어를 다시 말한 글로만 여기지 말아 달라고 합니다. SOLID뿐 아니라 DRY와 KISS까지 모두 결합도와 응집도에 관한 것이라는 견해를 설명하고, 결합도와 응집도를 정량화하는 새로운 아이디어를 자신의 경험을 바탕으로 제시합니다.
    • @u/Uberhipster — AI가 만든 글이 아니라는군요. 알겠습니다. SOLID, GRASP, KISS를 잘 따르면 해고될 수도 있지만 소프트웨어 공학에서는 바로 그렇게 해야 채용된다는 말이 정말 그렇다는 거죠.
    • @u/areklanga — 이 농담을 떠올리고 적절한 표현을 찾는 데만 20~30시간을 썼습니다. AI는 독창적인 농담을 전혀 만들 수 없습니다.
  • @u/TheKL — 간결하고 잘 쓴 글입니다. 신선해서 좋습니다.
    • @u/areklanga — 감사합니다.
  • @u/bwainfweeze — SRP를 버리고 Functional Core, Imperative Shell(FCIS)을 사용합니다. SRP의 문구만 지키고 취지는 놓치는 막다른 길과 패턴 숭배가 너무 많습니다. 반면 함수는 본래 단일 책임을 갖는 경향이 있습니다. 부수효과도 없으므로 테스트가 먼저 확장되며, 부수효과는 코드베이스 가장자리에 모입니다. Imperative Shell은 기능이 서로 나쁘게 상호작용하고 버그를 만드는 방식을 찾을 때 살펴봐야 하는 범위와, 이미 발견한 문제를 이해하는 데 필요한 탐색 깊이를 모두 줄입니다. 지금 알고 있는 방법 중 기존 기능 수에 대한 새 기능 비용을 로그 수준으로 만드는 유일한 방법입니다. SRP만으로는 n의 제곱근 정도에 가까워질 뿐입니다.
    • @u/Meleneth — 특정 방식으로 SRP를 구현하는 일을 위해 SRP를 버린 셈 아닌가요?
    • @u/bwainfweeze — ‘대체로 그렇다’와 ‘항상 그렇다’는 전혀 다릅니다. 이 분야에서는 뉘앙스가 중요합니다. 어떤 사람들은 그 차이가 중요하지 않다고 생각하는 것 같습니다. 그리고 제가 말한 내용에서 가장 흥미롭지 않은 부분인데, 어떤 함정을 잡았다고 생각하는지 모르겠습니다.
    • @u/areklanga — 감사합니다. Functional Core, Imperative Shell이 무엇인지 몰랐는데 찾아봤습니다. 타당한 접근처럼 보입니다. 그 원칙을 바탕으로 한 아키텍처와 프레임워크도 본 적이 있습니다. 그래도 제 글의 관점에서는 결합도와 응집도를 관리하는 또 하나의 방법입니다. Functional Core, Imperative Shell이든 다른 접근이든 프로젝트 안에서 낮은 결합도와 높은 응집도를 달성하게 해 준다면 좋은 방법입니다. 특정 원칙보다 낮은 결합도와 높은 응집도에 더 집중하자는 말입니다.
    • @u/FantaZmio — FCIS는 정말 좋은 방법입니다. 효과적으로 쓰기 위해 어떤 언어와 라이브러리를 사용하시나요? 저는 Scala를 좋아합니다. 평범한 Java 스타일의 명령형 코드를 작성할 수도 있지만, 라이브러리를 골라 FCIS를 한 단계 더 발전시킬 수도 있습니다. 순수 함수, Option·Either·NonEmptyList 같은 강력한 타입, 깔끔한 로직 흐름, Tagless Final 접근을 얻을 수 있습니다. 이건 빙산의 일각일 뿐입니다.
    • @u/chucker23n — Functional Core, Imperative Shell이라니 흥미롭습니다. 함수형 프로그래밍에서 ‘현실은 그렇게 작동하지 않는다’는 반론이 부딪히는 거친 부분을 줄일 수 있을 것 같습니다. 그런 이분법을 설정한다면 C#과 Swift 같은 다중 패러다임 언어가 이미 충분히 지원하는지 궁금합니다. 아니면 코어는 함수형 언어로, 셸은 명령형 언어로 작성하고 그 사이를 FFI나 IPC로 연결하는 편이 더 나은지도 궁금합니다.
  • @u/valarauca14 — 실제로 아주 좋은 글입니다.
  • @u/Ok_Spread_2062 — 읽을거리 감사합니다. 이 글은 확실히 북마크해 두겠습니다.
  • @u/possiblyquestionabl3 — 결합도가 높은 아키텍처나 설계의 부작용은 연쇄적으로 이어집니다. 구성 요소가 강하게 결합되어 있으면 잘 분리된 새 컴포넌트를 추가하기도 점점 어려워집니다. 리팩터링에 시간을 쓰지 않으면 자연스럽게 점점 더 강하게 결합된 부분이 늘어납니다.
  • @u/FantaZmio — 실제 공식으로 결합도와 응집도를 측정하고 숫자까지 얻을 수 있다는 생각은 전혀 못 했습니다. 이력서에 ‘프로젝트 전체 결합도를 0.8에서 0.43으로 낮췄다’고 쓸 수도 있겠네요. 사람들이 이런 말을 좋아합니다. 좋은 글 감사합니다.
  • @u/bwainfweeze — 수학 예시를 이해하지 못했습니다. 이미 존재하는 기능 수에 따라 모든 기능이 서로 상호작용하는 최악의 경우 결합도는 기능 수에 대해 팩토리얼로 늘어나는 것 아닌가요?
    • @u/areklanga — 의존성 수를 세는 방식이라면 맞습니다. 하지만 제가 제안하는 방식은 하나의 기능을 구현할 때 컴포넌트별 노력의 분포를 세는 방식입니다. 그러면 기능 구현에 관여하는 컴포넌트를 하나 더 추가할 때마다 결합도가 더 나빠질 수 있으므로 달성 가능한 상한이 없습니다. 이제 더 명확한가요? 원하시면 더 자세히 설명하겠습니다.
  • @u/smoke-bubble — 이 측정값은 의미가 없습니다. 숫자가 무엇을 말하든 처음 보는 프로젝트를 이해하는 데 아무것도 알려주지 않으며, 현재 프로젝트를 최적화하는 데도 도움이 되지 않습니다. 프로젝트 간 비교조차 할 수 없으니 계산할 가치가 없습니다.
    • @u/edtheshed — 최적화에는 도움이 됩니다. 낮은 결합도와 높은 응집도가 목표 그 자체는 아니지만, 코드베이스를 더 쉽게 변경하고 발전시키는 데 도움이 됩니다. 숫자가 꼭 필요하다는 주장에는 동의하지 않을 수 있습니다. 그래도 숫자가 좋아진다면 소프트웨어가 성장하고 있다는 신호가 될 수 있습니다. 변화하는 환경과 요구사항에 대응할 때 특히 그렇습니다.
    • @u/smoke-bubble — 그 숫자를 얼마나 자주 사용하시나요? 실제로는 한 번도 사용하지 않을 것이라고 확신합니다.
    • @u/edtheshed — 맞습니다. 제가 사용한다고 말한 적은 없습니다. 숫자가 좋아진다면 좋은 신호라고 이론적으로 말했을 뿐입니다. 저는 두 컴포넌트를 보고 결합도와 응집도가 높은지 낮은지 판단한 다음 리팩터링을 찾을 것입니다. 아마 당신도 그렇게 할 것 같습니다. 그래도 그 숫자가 무의미하다고 말하는 입장은 틀린 것 같습니다.
    • @u/smoke-bubble — 이 글이 측정값을 다루고 있고 그 측정이 엉터리라고 보기 때문에 하는 말입니다. 원칙을 적용하는 것과 그 값을 수치화하는 일은 다릅니다. 수치는 아무것도 알려주지 않으니 정량화하지 않습니다.
    • @u/edtheshed — 아무것도 알려주지 않는 것은 아닙니다. 코드베이스 품질의 한 측면을 정량화하는 데 쓸 수 있습니다. 예를 들어 기술 리드가 비즈니스 리드나 PO에게 품질 개선 시간을 요청하면서, 장기적으로 작업을 쉽게 만들거나 고객 경험을 개선한다고 설명할 때 다른 지표와 함께 이 수치를 근거로 제시할 수 있습니다. 순환 복잡도도 쓸모없거나 엉터리 지표라고 하시겠습니까?
    • @u/areklanga — 프로젝트를 이해하거나 최적화하거나 프로젝트 간 비교를 하려고 만든 지표가 아닙니다. 누군가 정량 지표를 요구할 때 사용할 수 있는 값입니다. 같은 프로젝트의 서로 다른 상태를 비교할 때 의미가 있습니다.
    • @u/smoke-bubble — 같은 프로젝트 안에서도 값이 좋아졌는지 나빠졌는지 알려주지 못합니다. 값의 변화가 정당화되거나 다른 수치와 설명 또는 상관관계로 연결되지 않기 때문입니다.
    • @u/areklanga — 왜 아무것도 알려주지 않는다고 보시나요? 결합도 값을 시간순으로 기록하는 가장 단순한 경우에도 프로젝트 결합도 수준의 추세를 볼 수 있습니다. 의존성 개수를 세는 전통적인 방식보다 이미 낫습니다.
    • @u/smoke-bubble — 그래도 증가가 어떤 이유로 필요한지 설명할 수 없고, 다른 개선 작업을 했기 때문에 값이 낮아졌다고 자랑할 수도 없습니다. 프로젝트 목표로 삼을 수도 없습니다. 따라서 신경 쓸 의미가 없습니다.
    • @u/areklanga — 결합도 증가를 꼭 설명해야 하는 이유가 있나요? 설명이 필요하더라도 그 설명에 이 지표가 필요한 것은 아닙니다. 제가 제안한 지표는 결합도 증가를 설명하기 위한 것이 아닙니다. 예를 들어 추상적인 ‘기술 부채’가 아니라 ‘현재 평균 결합도는 X이고, 이번 작업 범위를 마치면 최소 20% 낮아질 것으로 예상한다’고 말하며 작업을 정당화할 때 쓸 수 있습니다. 결합도 수준과 프로덕션 버그 수의 상관관계를 보여 결합도를 낮추면 버그 수를 줄일 수 있다고 설명할 수도 있습니다. 지난해 평균 결합도가 0.7이었고 올해는 0.4라면 충분히 자랑할 수 있습니다. 일부러 결합도가 낮게 나오는 작업만 골라 수치를 조작하는 상황을 말하는 것이라면, 팀과 프로젝트가 건강한 상태가 아닐 가능성이 큽니다.

원문: bastrich.tech / 번역·요약: Trawling