2025-08-08 00:37

Tags:

핸드북: 결합도 (Coupling)

1. 만들어진 이유: 왜 결합도를 신경 써야 하는가?

소프트웨어는 여러 개의 작은 부품(모듈)이 모여 하나의 완성품이 되는 것과 같음. 이때 각 부품이 서로 얼마나 끈끈하게 얽혀 있는지를 나타내는 척도가 바로 결합도.

마치 레고 블록을 생각해보면 쉬움. 각 레고 블록은 정해진 돌기(인터페이스)를 통해 다른 블록과 연결됨. 어떤 블록을 빼거나 다른 블록으로 교체해도 전체 구조에 큰 영향을 주지 않음. 이것이 결합도가 낮은(Loose Coupling) 상태.

반면, 강력 접착제로 붙어버린 레고 블록들을 생각해보면, 하나를 떼어내려면 주변 블록까지 부서지거나 망가짐. 이것이 결합도가 높은(Tight Coupling) 상태.

소프트웨어 설계에서 낮은 결합도를 추구하는 이유는 명확함.

  • 유지보수 용이성: 특정 모듈의 코드를 수정했을 때, 그 영향이 다른 모듈로 퍼져나가는 것을 최소화. 버그 수정이나 기능 추가가 쉬워짐.

  • 재사용성 증가: 모듈이 다른 모듈에 대한 의존성이 낮으면, 해당 모듈을 떼어내 다른 프로젝트에서도 쉽게 재사용 가능.

  • 테스트 용이성: 각 모듈을 독립적으로 테스트하기 쉬워짐. 의존하는 모든 모듈을 한꺼번에 준비할 필요가 없음.

  • 유연성과 확장성: 새로운 기능을 추가하거나 기존 기능을 변경할 때, 마치 레고 블록을 갈아 끼우듯 유연하게 대처 가능.

결론적으로, 결합도라는 개념은 **‘변경의 파급 효과를 제어’**하여 예측 가능하고 관리하기 쉬운 소프트웨어를 만들기 위해 탄생.

2. 구조: 결합도의 종류

결합도는 ‘나쁜 결합’에서 ‘좋은 결합’ 순으로 여러 단계로 나뉨. 일반적으로 아래로 갈수록 더 바람직한(느슨한) 결합.

① 내용 결합 (Content Coupling) - 가장 나쁜 결합

  • 정의: 한 모듈이 다른 모듈의 내부 데이터나 코드를 직접 수정하거나 참조하는 경우.

  • 비유: 옆집에 마음대로 들어가서 냉장고 안의 음식을 꺼내 먹고 가구 배치를 바꾸는 것.

  • 문제점: 상대 모듈의 내부 구현이 바뀌면, 이 모듈도 즉시 수정해야 함. 완전한 종속 관계.

  • 예시 (자바):

    public class ModuleA {
        public int data = 10; // public 필드
    }
    
    public class ModuleB {
        public void manipulateData() {
            ModuleA a = new ModuleA();
            a.data = 20; // ModuleA의 내부 데이터를 직접 수정
        }
    }
    

② 공통 결합 (Common Coupling)

  • 정의: 두 개 이상의 모듈이 **전역 변수(공유 메모리)**와 같은 공통의 데이터 영역을 공유하고 사용하는 경우.

  • 비유: 마을 중앙 광장에 있는 게시판에 누구나 글을 쓰고 지울 수 있는 상황. 누가 언제 정보를 바꿨는지 추적하기 어려움.

  • 문제점: 공통 데이터를 변경하면 이를 참조하는 모든 모듈이 영향을 받음. 변경의 예측이 매우 어려워짐.

  • 예시 (자바):

    public class GlobalData {
        public static String currentUser = "Guest";
    }
    
    public class ModuleC {
        public void login() {
            GlobalData.currentUser = "Admin"; // 공통 데이터 변경
        }
    }
    
    public class ModuleD {
        public void displayUser() {
            System.out.println(GlobalData.currentUser); // 공통 데이터 참조
        }
    }
    

③ 제어 결합 (Control Coupling)

  • 정의: 한 모듈이 다른 모듈의 동작을 제어하기 위한 정보(제어 플래그, 명령어)를 전달하는 경우.

  • 비유: 상사가 부하 직원에게 “A 방식으로 일해” 또는 “B 방식으로 일해”라고 구체적인 행동 방식을 지시하는 것.

  • 문제점: 제어하는 모듈이 제어받는 모듈의 내부 동작 방식을 알아야 함. 제어받는 모듈의 기능이 변경되면 제어하는 모듈도 함께 바뀌어야 할 수 있음.

  • 예시 (자바):

    public class Worker {
        public void doWork(String mode) { // "email" 또는 "sms" 모드를 받음
            if ("email".equals(mode)) {
                // 이메일 발송 로직
            } else if ("sms".equals(mode)) {
                // SMS 발송 로직
            }
        }
    }
    
    public class Manager {
        public void orderWork() {
            Worker worker = new Worker();
            worker.doWork("email"); // Worker의 동작 방식을 제어
        }
    }
    

④ 스탬프 결합 (Stamp Coupling)

  • 정의: 두 모듈이 자료 구조(객체, 레코드 등)를 파라미터로 주고받지만, 실제로는 그 자료 구조의 일부 필드만 사용하는 경우.

  • 비유: 편지를 보내기 위해 우체부에게 우리 집 열쇠 꾸러미 전체를 주는 것. 실제 필요한 것은 집 주소 정보뿐인데도.

  • 문제점: 불필요한 데이터까지 의존하게 됨. 사용하지 않는 필드가 포함된 자료 구조가 변경되어도 영향을 받을 수 있음.

  • 예시 (자바):

    public class UserProfile {
        String name;
        int age;
        String address;
        String phoneNumber;
    }
    
    public class Printer {
        // 이름만 출력하면 되는데 UserProfile 객체 전체를 받음
        public void printUserName(UserProfile profile) {
            System.out.println(profile.name);
        }
    }
    

⑤ 자료 결합 (Data Coupling) - 가장 바람직한 결합

  • 정의: 두 모듈이 오직 필요한 데이터만을 파라미터를 통해 주고받는 경우.

  • 비유: 자판기에 딱 맞는 금액의 동전만 넣고 원하는 음료수 버튼을 누르는 것.

  • 장점: 모듈 간의 의존성이 명확하고 단순함. 다른 모듈의 내부 구조를 전혀 알 필요가 없음.

  • 예시 (자바):

    public class Validator {
        // 나이(int)라는 데이터만 받아서 유효성을 검사
        public boolean isAdult(int age) {
            return age >= 19;
        }
    }
    
    public class MainApp {
        public void checkAge() {
            Validator validator = new Validator();
            boolean result = validator.isAdult(25); // 필요한 데이터 '25'만 전달
        }
    }
    

3. 사용법: 어떻게 결합도를 낮출 것인가?

결합도를 낮추는 것은 특정 기술이 아닌, 설계 철학에 가까움. 다음은 느슨한 결합을 달성하기 위한 일반적인 원칙과 패턴.

① 인터페이스와 추상화 활용

  • 구체적인 ‘구현 클래스’에 의존하지 말고, 추상적인 **‘인터페이스’**에 의존.

  • 원리: “무엇을 하는지(What)“만 알고, “어떻게 하는지(How)“는 몰라도 되게 만듦.

  • 예시: List<String> list = new ArrayList<>(); 코드에서 ArrayListLinkedList 등 구체적인 구현이 바뀌어도 List 인터페이스의 메소드(add, get 등)를 사용하는 코드는 영향을 받지 않음.

② 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

  • 상위 모듈이 하위 모듈에 직접 의존하지 않고, 둘 모두 **추상화(인터페이스)**에 의존해야 한다는 원칙.

  • 원리: 의존성의 방향을 역전시켜, 세부 사항이 변경되어도 상위 수준의 정책은 영향을 받지 않게 함.

  • 예시: 자동차 클래스가 스노우타이어 클래스에 직접 의존하는 대신, 자동차스노우타이어 모두 타이어라는 인터페이스에 의존하도록 설계.

③ 이벤트 기반 아키텍처 (Event-Driven Architecture)

  • 모듈 간에 직접 메소드를 호출하는 대신, 한 모듈이 **‘이벤트’를 발행(publish)**하면 관심 있는 다른 모듈이 그 이벤트를 **‘구독(subscribe)‘**하여 처리하는 방식.

  • 원리: 이벤트를 발행하는 쪽은 누가 이벤트를 받아서 처리하는지 전혀 알 필요가 없음.

  • 예시: 사용자가 ‘주문 완료’ 버튼을 누르면 OrderServiceOrderCompleted 이벤트를 발행. EmailServiceInventoryService는 이 이벤트를 구독하여 각각 고객에게 이메일을 보내고 재고를 차감. OrderService는 이메일이나 재고 시스템의 존재를 모름.

4. 심화: 결합도와 응집도 (Cohesion)

결합도는 소프트웨어 품질을 결정하는 동전의 양면과 같음. 한쪽 면이 결합도라면, 다른 쪽 면은 응집도(Cohesion).

  • 결합도 (Coupling): 모듈과 모듈 사이의 의존성. (낮을수록 좋음)

  • 응집도 (Cohesion): 하나의 모듈 내부의 요소들이 얼마나 밀접하게 관련되어 있는지. (높을수록 좋음)

좋은 설계란?

결합도는 낮추고 (Low Coupling), 응집도는 높여라 (High Cohesion).

  • High Cohesion: 하나의 모듈은 하나의 책임만 명확하게 수행. (예: EmailSender 모듈은 이메일 발송과 관련된 기능만 가짐)

  • Low Coupling: 각 모듈은 다른 모듈에 대해 최소한의 정보만 알고 독립적으로 작동.

결국, 응집도를 높여 모듈의 역할을 명확히 하면, 자연스럽게 다른 모듈에 대한 불필요한 의존성(결합도)이 줄어드는 선순환이 일어남.

결합도에 대해 더 궁금한 점이나, 특정 상황에서 어떤 종류의 결합에 해당하는지 논의하고 싶으신 부분이 있나요?

References

결합도