[태그:] 클린코드

  • 보이지 않는 재앙: 결합도(Coupling)가 당신의 프로젝트를 서서히 망가뜨리는 방법

    보이지 않는 재앙: 결합도(Coupling)가 당신의 프로젝트를 서서히 망가뜨리는 방법

    성공적인 소프트웨어 프로젝트는 견고한 건축물과 같습니다. 각각의 벽돌(모듈)이 제 역할을 충실히 하면서도 서로에게 불필요한 부담을 주지 않아야 전체 구조가 안정적으로 유지됩니다. 지난 ‘응집도’에 대한 글에서 우리는 벽돌 자체가 얼마나 단단하고 잘 만들어졌는지에 대해 이야기했습니다. 이제 우리는 그 벽돌들이 서로 어떻게 연결되어 있는지, 즉 ‘결합도(Coupling)’에 대해 이야기할 차례입니다. 결합도는 모듈과 모듈 사이의 상호 의존성 정도를 나타내는 척도로, 코드의 유연성, 확장성, 그리고 유지보수성을 결정하는 가장 중요한 요소 중 하나입니다.

    결합도가 높은 시스템은 마치 모든 가구가 바닥에 강력 접착제로 붙어있는 방과 같습니다. 의자 하나를 옮기려고 해도 바닥 전체를 뜯어내야 하는 대공사가 필요합니다. 이와 같이, 코드의 결합도가 높으면 간단한 기능 변경 하나가 예기치 않은 수많은 다른 모듈의 수정을 요구하며, 이는 개발 일정 지연, 예측 불가능한 버그 발생, 그리고 개발자의 번아웃을 초래하는 ‘보이지 않는 재앙’이 됩니다. 이 글에서는 결합도의 정확한 의미와 그 수준을 나누는 6가지 레벨을 구체적인 예시와 함께 깊이 있게 탐구할 것입니다. 또한, 이것이 현대적인 API 설계나 의존성 주입 패턴과 어떻게 연결되는지, 그리고 왜 제품 관리자와 UX/UI 디자이너조차 이 개념을 이해해야 하는지를 명확히 설명해 드릴 것입니다.

    목차

    1. 결합도란 무엇인가?: 시스템 유연성의 척도
    2. 결합도의 6가지 레벨: 단단한 악연부터 건강한 관계까지
    3. 현대 소프트웨어 개발에서 결합도 관리하기
    4. 결론: 유연한 시스템을 향한 여정

    결합도란 무엇인가?: 시스템 유연성의 척도

    결합도의 정의와 중요성

    결합도(Coupling)는 하나의 모듈이 변경될 때 다른 모듈이 함께 변경되어야 하는 정도를 측정하는 지표입니다. 즉, 모듈 간의 의존성이 얼마나 강한지를 나타냅니다. 이상적인 소프트웨어는 각 모듈이 독립적으로 작동하여, 하나의 모듈을 수정하거나 교체하더라도 다른 부분에 미치는 영향(Side Effect)이 거의 없어야 합니다. 이러한 상태를 ‘느슨한 결합(Loose Coupling)’ 또는 ‘약한 결합’이라고 부릅니다. 반대로, 여러 모듈이 서로의 내부 구조나 데이터에 깊숙이 관여하여 떼려야 뗄 수 없는 관계가 된 상태를 ‘강한 결합(Tight Coupling)’ 또는 ‘높은 결합’이라고 합니다.

    결합도가 낮은 시스템은 여러 가지 중요한 이점을 제공합니다. 첫째, 유지보수가 매우 쉬워집니다. 특정 기능의 요구사항이 변경되었을 때, 해당 모듈만 집중적으로 수정하면 되므로 작업 범위가 명확해지고 버그 발생 가능성이 줄어듭니다. 둘째, 테스트가 용이합니다. 각 모듈을 독립적으로 테스트할 수 있기 때문에, 문제의 원인을 신속하게 파악하고 격리할 수 있습니다. 셋째, 재사용성이 향상됩니다. 다른 모듈에 대한 의존성이 적은 모듈은 다른 프로젝트나 시스템에서도 쉽게 가져다 쓸 수 있는 부품이 됩니다. 넷째, 팀 단위의 병렬 개발이 가능해집니다. 각 팀이 맡은 모듈이 다른 팀의 작업에 큰 영향을 주지 않으므로, 대규모 프로젝트에서 개발 생산성을 극대화할 수 있습니다.

    응집도(Cohesion)와의 관계: 좋은 설계의 두 기둥

    결합도는 지난 글에서 다룬 응집도(Cohesion)와 함께 소프트웨어 설계 품질을 평가하는 핵심적인 두 축을 이룹니다. 이 둘의 관계는 ‘모듈 내부는 단단하게, 모듈 외부는 유연하게’라는 한 문장으로 요약할 수 있습니다. 즉, 좋은 설계의 목표는 ‘높은 응집도와 낮은 결합도(High Cohesion, Low Coupling)’를 동시에 달성하는 것입니다.

    이 관계를 오케스트라에 비유해 봅시다. 바이올린 파트는 오직 바이올린 연주에만 고도로 집중하고 연습합니다(높은 응집도). 트럼펫 파트 역시 트럼펫 연주에만 몰두합니다(높은 응집도). 이 두 파트는 서로의 악기 연주법이나 내부 연습 과정에 대해 전혀 알 필요가 없습니다. 그들이 소통하는 유일한 방법은 지휘자의 지휘와 악보라는 명확하고 잘 정의된 인터페이스를 통해서입니다(낮은 결합도). 만약 바이올린 연주자가 트럼펫 연주자의 연주법에 사사건건 간섭하거나, 악보 없이 서로의 눈치만 보며 연주한다면(높은 결합도), 그 오케스트라는 아름다운 하모니를 만들어낼 수 없을 것입니다. 소프트웨어 모듈도 마찬가지입니다. 각자의 책임에만 충실하도록 응집도를 높이고, 모듈 간의 소통은 최소한의 표준화된 방법으로만 이루어지도록 결합도를 낮추는 것이 견고하고 아름다운 시스템을 만드는 비결입니다.


    결합도의 6가지 레벨: 단단한 악연부터 건강한 관계까지

    결합도는 그 강도에 따라 여러 수준으로 분류됩니다. 일반적으로 6가지 레벨로 나누며, 가장 강한 결합(최악)부터 가장 느슨한 결합(최상) 순으로 살펴볼 것입니다. 내 코드가 어느 수준에 해당하는지 파악하고 개선 방향을 찾는 것은 매우 중요합니다.

    결합도 수준 (영문명)설명좋은가?
    1. 내용 결합도 (Content Coupling)한 모듈이 다른 모듈의 내부 데이터나 코드를 직접 수정.매우 나쁨
    2. 공통 결합도 (Common Coupling)여러 모듈이 하나의 공통된 전역 변수나 데이터를 공유하고 수정.나쁨
    3. 외부 결합도 (External Coupling)여러 모듈이 외부의 특정 파일 포맷이나 통신 프로토콜을 공유.좋지 않음
    4. 제어 결합도 (Control Coupling)한 모듈이 다른 모듈의 동작을 제어하는 제어 신호(플래그)를 전달.보통
    5. 스탬프 결합도 (Stamp Coupling)모듈 간에 필요한 데이터만 전달하는 것이 아니라, 구조체나 객체 전체를 전달.양호
    6. 자료 결합도 (Data Coupling)모듈 간에 필요한 최소한의 데이터(매개변수)만으로 통신.매우 좋음

    1. 내용 결합도 (Content Coupling)

    가장 최악의 결합도 수준으로, 한 모듈이 다른 모듈의 내부로 직접 침투하여 그 모듈의 지역 데이터를 수정하거나, 코드의 특정 부분으로 직접 분기하는 경우를 말합니다. 이는 다른 모듈의 주권을 완전히 무시하는 행위이며, 객체지향 프로그래밍에서는 private으로 선언된 멤버 변수를 외부에서 강제로 바꾸는 것과 같습니다.

    • 예시: 모듈 A가 모듈 B 내부에 선언된 count라는 변수의 값을 B.count = 10; 과 같이 직접 수정하는 코드입니다. 이렇게 되면 모듈 B는 자신의 상태를 스스로 제어할 수 없게 되며, count 값이 예상치 못하게 변경되어 심각한 버그를 유발합니다. 모듈 B를 수정하면 모듈 A까지 반드시 함께 검토해야 하므로 유지보수가 극도로 어려워집니다.

    2. 공통 결합도 (Common Coupling)

    여러 모듈이 하나의 공통된 데이터 영역, 예를 들어 전역 변수(Global Variable)를 공유하고, 이를 통해 서로 통신하며 데이터를 변경하는 구조입니다. 전역 변수는 프로그램 어디서든 접근하고 수정할 수 있기 때문에 매우 편리해 보이지만, 치명적인 단점을 가집니다.

    • 예시: currentUser라는 전역 객체를 두고, ‘로그인 모듈’이 이 객체에 사용자 정보를 채워 넣고, ‘게시판 모듈’과 ‘알림 모듈’이 이 객체를 참조하여 사용자 이름을 화면에 표시한다고 가정해 봅시다. 만약 ‘프로필 수정 모듈’이 currentUser의 이름을 변경했는데, ‘알림 모듈’이 변경 사실을 인지하지 못하고 이전 이름으로 알림을 보낸다면 데이터 불일치 문제가 발생합니다. 어떤 모듈이 전역 변수를 언제, 어떻게 바꾸었는지 추적하기가 매우 어려워 시스템 전체가 불안정해집니다.

    3. 외부 결합도 (External Coupling)

    두 개 이상의 모듈이 외부의 특정 파일 포맷, 통신 프로토콜, 또는 데이터베이스 스키마와 같은 외부 요소에 함께 의존하는 경우입니다. 공통 결합도와 유사하지만, 공유 대상이 프로그램 내부의 데이터가 아닌 외부의 요소라는 차이가 있습니다.

    • 예시: ‘주문 생성 모듈’과 ‘재고 관리 모듈’이 모두 데이터베이스의 ‘Products’라는 테이블 구조에 직접 의존하고 있다고 생각해 봅시다. 만약 관리자가 ‘Products’ 테이블에 가격(price) 컬럼의 데이터 타입을 정수(int)에서 실수(float)로 변경한다면, 이 테이블을 직접 참조하는 ‘주문 생성 모듈’과 ‘재고 관리 모듈’ 두 곳 모두에서 에러가 발생하며 코드를 수정해야 합니다. 외부의 변경 하나가 시스템의 여러 부분에 파급 효과를 일으키는 것입니다.

    4. 제어 결합도 (Control Coupling)

    한 모듈이 다른 모듈로 제어 플래그(Control Flag)와 같은 값을 전달하여, 전달받은 모듈의 동작 방식을 결정하는 구조입니다. 즉, 호출하는 모듈이 호출되는 모듈의 내부 로직을 알고 있다는 것을 전제로 합니다.

    • 예시: processData(data, sortOption)이라는 함수가 있고, sortOption 값에 따라 ‘이름순 정렬’ 또는 ‘날짜순 정렬’을 수행한다고 가정해 봅시다. processData를 호출하는 모듈은 sortOption에 어떤 값을 넣어야 하는지, 그리고 그 값에 따라 processData가 어떻게 동작할지를 미리 알고 있어야 합니다. 이는 두 모듈 간의 논리적인 의존성을 만들어냅니다. 만약 processData에 ‘가격순 정렬’ 기능이 추가된다면, 이 함수를 호출하는 모든 모듈의 코드를 검토하고 수정해야 할 수도 있습니다.

    5. 스탬프 결합도 (Stamp Coupling)

    두 모듈이 데이터를 주고받을 때, 필요한 개별 데이터가 아닌 데이터 구조(자료 구조, 객체 등) 전체를 전달하는 경우입니다. 마치 편지를 보낼 때 필요한 내용 한 줄만 보내면 되는데, 집문서 전체를 복사해서 보내는 것과 같습니다.

    • 예시: 학생의 이름만 필요한 printStudentName() 함수에 student 객체 전체(이름, 학번, 주소, 성적 등 모든 정보 포함)를 매개변수로 전달하는 경우입니다. printStudentName() 함수는 이름 외의 다른 데이터는 전혀 사용하지 않음에도 불구하고, student 객체의 구조가 변경될 때마다(예: ‘전공’ 필드 추가) 영향을 받을 잠재적 가능성이 생깁니다. 또한, 불필요하게 많은 데이터를 주고받는 것은 비효율적일 수 있습니다.

    6. 자료 결합도 (Data Coupling)

    가장 이상적이고 바람직한 결합도 수준입니다. 모듈 간의 데이터 교환이 오직 필요한 최소한의 데이터, 즉 매개변수를 통해서만 이루어지는 경우입니다. 호출된 모듈은 전달받은 데이터로 자신의 작업을 수행하고 결과를 반환할 뿐, 호출한 모듈이나 시스템의 다른 부분에 대해 전혀 알 필요가 없습니다.

    • 예시: calculateArea(width, height) 함수는 가로 길이와 세로 길이만 인자로 받아 넓이를 계산하여 반환합니다. 이 함수는 width와 height가 어디서 왔는지, 이 함수를 누가 호출했는지 전혀 신경 쓰지 않습니다. 오직 자신의 기능에만 충실합니다. 이러한 자료 결합도는 모듈의 독립성을 최대로 보장하며, 재사용성과 테스트 용이성을 극대화합니다. 우리가 작성하는 대부분의 함수는 바로 이 자료 결합도를 목표로 설계되어야 합니다.

    현대 소프트웨어 개발에서 결합도 관리하기

    결합도를 낮추는 것은 단순히 코드를 깔끔하게 만드는 것을 넘어, 변화에 빠르게 대응하고 안정적으로 서비스를 운영하기 위한 현대 소프트웨어 개발의 핵심 전략입니다. 특히 복잡한 시스템을 다루는 제품 관리자나 사용자 경험의 일관성을 책임지는 UX/UI 디자이너에게도 결합도에 대한 이해는 필수적입니다.

    API와 느슨한 결합

    오늘날 마이크로서비스 아키텍처(MSA)의 핵심은 서비스 간의 느슨한 결합을 유지하는 것입니다. 이를 가능하게 하는 가장 중요한 도구가 바로 잘 정의된 API(Application Programming Interface)입니다. 각 서비스는 자신의 기능을 API라는 표준화된 창구를 통해서만 외부에 공개합니다. 다른 서비스는 그 서비스의 내부 구현(어떤 프로그래밍 언어를 썼는지, 어떤 데이터베이스를 사용하는지 등)을 전혀 몰라도, 약속된 API 명세에 따라 요청을 보내고 응답을 받기만 하면 됩니다.

    예를 들어, ‘결제 서비스’는 POST /payments라는 API를 제공하여 결제를 처리합니다. ‘주문 서비스’는 이 API를 호출할 때 필요한 최소한의 정보(주문 금액, 사용자 ID 등)만 전달하면 됩니다(자료 결합도). 만약 ‘결제 서비스’ 내부에서 사용하는 PG(Payment Gateway)사가 변경되거나 결제 로직이 복잡하게 바뀌더라도, API 명세만 그대로 유지된다면 ‘주문 서비스’는 아무런 코드를 변경할 필요가 없습니다. 이처럼 API는 서비스 간의 강력한 방화벽 역할을 하여, 변경의 파급 효과를 차단하고 각 서비스의 독립적인 발전을 가능하게 합니다.

    의존성 주입(Dependency Injection)과 제어의 역전(IoC)

    애플리케이션 내부의 클래스나 컴포넌트 간의 결합도를 낮추기 위해 널리 사용되는 중요한 디자인 패턴으로 ‘의존성 주입(DI)’과 ‘제어의 역전(IoC)’이 있습니다. 과거에는 객체 A가 객체 B의 기능을 필요로 할 때, A 내부에서 직접 B b = new B(); 와 같이 B 객체를 생성했습니다. 이는 A가 B라는 구체적인 클래스에 직접 의존하는 강한 결합을 만듭니다.

    의존성 주입은 이러한 의존 관계를 외부에서 결정하고 주입해주는 방식입니다. 객체 A는 더 이상 B를 직접 생성하지 않고, 외부의 조립기(Assembler)나 프레임워크(예: Spring)가 생성된 B 객체를 A에 전달(주입)해 줍니다. 이를 통해 A는 B라는 구체적인 구현이 아닌, B가 구현한 추상적인 인터페이스에만 의존하게 되어 결합도가 크게 낮아집니다. 객체가 자신의 의존성을 직접 관리하는 것이 아니라 외부로부터 제어받는다고 해서 ‘제어의 역전(Inversion of Control)’이라고 부릅니다. 이는 코드의 유연성과 확장성을 높이고, 단위 테스트 시 실제 객체 대신 가짜 객체(Mock Object)를 쉽게 주입할 수 있게 하여 테스트 효율을 극대화합니다.

    PM, UX/UI 관점에서의 결합도

    기술적인 개념처럼 보이는 결합도는 제품 개발의 속도와 방향성에 직접적인 영향을 미칩니다. 제품 관리자(PM)가 ‘상품 상세 페이지에 새로운 추천 상품 로직을 추가해주세요’라는 간단한 요구사항을 제시했다고 가정해 봅시다. 만약 프론트엔드 코드와 백엔드 코드가 강하게 결합되어 있다면, 이 작은 UI 변경을 위해 백엔드의 데이터 조회 방식, API 구조, 심지어 데이터베이스 스키마까지 변경해야 할 수도 있습니다. 이는 예상보다 훨씬 큰 개발 공수로 이어져 다른 중요한 기능의 개발 일정을 지연시킵니다.

    반면, 각 부분이 느슨하게 결합되어 있다면, 백엔드팀은 기존 API를 유지한 채 새로운 추천 로직을 개발하고, 프론트엔드팀은 해당 API를 호출하여 화면에 표시하기만 하면 됩니다. 이처럼 낮은 결합도는 기능 개발을 독립적으로 진행할 수 있게 하여 제품의 시장 출시 시간(Time-to-Market)을 단축시킵니다. UX/UI 디자이너 역시 마찬가지입니다. 디자인 시스템을 구축할 때 컴포넌트 간의 결합도를 고려하여 설계하면, 특정 컴포넌트의 디자인 변경이 다른 컴포넌트에 미치는 영향을 최소화하여 전체 UI의 일관성을 유지하기 쉬워집니다.


    결론: 유연한 시스템을 향한 여정

    결합도는 소프트웨어의 건강 상태를 진단하는 청진기와 같습니다. 결합도를 세심하게 관리하는 것은 당장의 기능 구현보다 훨씬 더 중요한 장기적인 투자입니다. 낮은 결합도는 변화의 충격을 흡수하는 유연한 구조를 만들어, 예측 불가능한 비즈니스 요구사항과 급변하는 기술 환경 속에서 우리의 소프트웨어가 살아남고 지속적으로 발전할 수 있는 힘을 제공합니다.

    우리의 목표는 명확합니다. 모듈 간의 의존성을 최소화하고, 불가피한 의존성은 가장 이상적인 ‘자료 결합도’ 수준으로 유지하기 위해 노력해야 합니다. 이를 위해 명확한 인터페이스를 설계하고, 전역 변수 사용을 지양하며, 의존성 주입과 같은 검증된 디자인 패턴을 적극적으로 활용해야 합니다.

    하지만 기억해야 할 점은, ‘제로 결합도’는 현실적으로 불가능하며 바람직하지도 않다는 것입니다. 모든 모듈이 완벽히 고립되어 있다면 시스템은 아무런 일도 할 수 없습니다. 중요한 것은 결합 그 자체가 아니라 ‘결합을 어떻게 관리할 것인가’입니다. 각 모듈이 꼭 필요한 최소한의 약속(인터페이스)을 통해 소통하도록 설계하고, 그 약속이 깨졌을 때의 파급 효과를 최소화하는 것이 핵심입니다.

    결합도 관리는 한 번에 끝나는 작업이 아니라, 프로젝트 생명주기 전체에 걸쳐 계속되는 여정입니다. 개발자, 아키텍트, 그리고 제품 관리자 모두가 결합도의 중요성을 이해하고, 코드 리뷰와 설계 논의 과정에서 “이 변경이 다른 부분에 어떤 영향을 미칠까?”라는 질문을 습관처럼 던지는 문화를 만들어야 합니다. 그럴 때 비로소 우리는 단단하면서도 유연하고, 세월의 변화를 견뎌내는 위대한 소프트웨어를 만들어낼 수 있을 것입니다.