Appendix 02. 결합도와 응집도

윤희빈·2026년 7월 22일

결합도와 응집도

예시같은거 외어라!

결합도(Coupling)

모듈과 모듈 사이의 의존 정도

→ 낮을수록 좋음

ex) 핸들을 통해서 바퀴를 조종한다고 하지만, 타이어를 보수한다고 핸들까지 교체할 필요는 없다!

1. 자료 결합도

  • 가장 좋은 형태!
  • 기본 자료형 데이터만 주고받음
  • 예: 시간, 할인율 같은 값만 전달

class Bill { //주차 요금 청구서
    public static int getBillFee(int time, int discount) {
        double discountPercentage = discount / 100;
        
        //매서드 안에서 또 다른 메서드를 호출해 의존되고 있긴 하지만, 메서드에
        //단순 파라미터 데이터를 보내는 형태
        return Fee.calculateFee(time) * discountPercentage;
    }
}

class Fee { //주차 요금 계산 모듈
    public static int calculateFee(int time) {
        int defaultMoney = 1000;
        return defaultMoney * time;
    }
}

public class Main{
		public static void main(String[] args){
				Bill.getBillFee(2,80); //2시간 이용, 80% 할인
		}
}

2. 스탬프 결합도

  • 최소한 스탬프 결합도까지만이라도 해도 굳
  • 배열, 객체 같은 자료구조 전체를 넘김
  • 필요한 것보다 더 많은 정보에 의존할 수 있음

// 이용기록 자료구조 형태
class Record {
    public int carNum;
    public int useTime;
    private int fee;
		
		Record(int carNum, int useTime){
				this.carNum = carNum;
				this.useTiem =useTime;
		}
		
    public void setFee(int fee) {
        this.fee += fee;
    }

    public int getFee() {
        return this.fee;
    }
}

class BillFee {
		//주차장 이용 청구서 작성 모듈
		public static int getBillFee(String name, int hour){
				Record record = new Record(name, hour); //객체 생성
				
				Bill.calculateFee(record); //객체를 메서드에 넘김
				Fee.calculateFeeAnoter(record); // 객체를 메서드에 넘김
				
				return record.getFee();
		}
}

class Bill {
		//주차비용 계산 모듈
    public static void calculateFee(Record record) {
        int defaultMoney = 1000;
        record.setFee(defaultMoney * record.useTime);
    }
}

class Fee {
		//기타비용 계산 모듈
		public static int calcutateFeeAnother(Record record) {
				int defaultMoney = 5000;
				record.setFee(defaultMoney * record.useTime);
		}
}

record 객체를 생성하여 메서드에 넘기므로 스탬프 결합이다.

//이용기록 자료구조 형태
class Record {
    public int carNum;
    ~~public int useTime;~~
		public int useMin;
    private int fee;
    
    Record(int carNum, int useTime){
	    this.carNum = carNum;
	    this.useTime = useTime;
	  }

    public void setFee(int fee) {
        this.fee += fee;
    }

    public int getFee() {
        return this.fee;
    }
}

레코드 변경 시, 다른 참조들도 변경해야하는 문제가 생긴다.

3. 제어 결합도

  • 한 모듈이 다른 모듈의 내부 흐름을 제어
  • 보통 boolean, mode 값으로 제어
  • 상위 모듈이 하위 모듈의 상세한 처리 절차를 통제
  • Encapsulation의 원칙에 위배
    객체 지향의 맛을 상실함.

fee가 쓸데없이 조건 제어에 의존한다!


class User {
    ...
} // 회원 정보 클래스

class Bill { // 주차 요금 청구서 모듈
    public static int getBillFee(int time) {
        double discountPercentage = discount / 100;
        User user1 = new User();

        // 인수값에 따라서 모듈 내부 로직의 처리가 달라지는 결합 형태
        return Fee.calculateFee(time, user.isJoin) * discountPercentage;
    }
}

class Fee { // 주차 요금 계산 모듈
    public static int calculateFee(int time, boolean isJoin) {
        int defaultMoney = 1000;

        // 회원 여부에 따라 주차요금을 계산하는 로직이 달라짐
        if (isJoin) {
            // ...
        } else {
            // ...
        }

        return defaultMoney * time - 400; // 회원이면 400원 할인
    }
}

user.isJoin의 값에 따라서 다른 모듈 내부 로직의 처리가 달라지므로 제어 결합이다.

public class DataProcessor {
    public void processData(int[] data, boolean isVerbose) {
        if (isVerbose) {
            System.out.println("Processing started...");
        }

        for (int i = 0; i < data.length; i++) {
            data[i] *= 2;
        }

        if (isVerbose) {
            System.out.println("Processing complete.");
        }
    }
}

public class Main{
		public static void main(String[] args){
				int[] data = {1,2,3};
				DataProcessor processor = new DataProcessor();
				processor.processData(data,true);
		}
}
public class DataProcessor {
    public void processData(int[] data) {
        for (int i = 0; i < data.length; i++) {
            data[i] *= 2;
        }
    }
}

public class DataProcessorLogger {
    private DataProcessor processor;

    public DataProcessorLogger(DataProcessor processor) {
        this.processor = processor;
    }

    public void processWithLogging(int[] data) {
        System.out.println("Processing started...");
        processor.processData(data);
        System.out.println("Processing complete.");
    }
}

public class Main {
    public static void main(String[] args) {
        int[] data = {1, 2, 3};

        DataProcessor processor = new DataProcessor();
        DataProcessorLogger logger = new DataProcessorLogger(processor);

        logger.processWithLogging(data);
    }
}

4. 외부 결합도

  • 외부 모듈이나 외부 형식에 의존함
  • 회원 여부를 아예 따로 외부 모듈로 분리
    → 주차요금청구서와 계산기가 모두 외부에 의존함.

class Addfee {
    private int plusFee;
    private int plusTime;

    public Addfee(int plusFee, int plusTime) {
        this.plusFee = plusFee;
        this.plusTime = plusTime;
    }

    public int getPlusFee() {
        return plusFee;
    }

    public int getPlusTime() {
        return plusTime;
    }
}

// 주차 요금 계산 모듈
class Fee {
    public int calculateFee(int time, Addfee f) {
        int defaultMoney = 1000;
        return defaultMoney * (time + f.getPlusTime());
    }//자기가 계산해도 되는데, 굳이 외부에서 가져옴 -> 외부가 바뀌면 자기도 바뀜..
}

// 요금 청구 모듈
class Bill {
    private Fee fee;

    public Bill() {
        this.fee = new Fee();
    }

    public int getBillFee(int time, int discount, Addfee f) {
        double discountPercentage = discount / 100.0;
        return (int) (fee.calculateFee(time, f) * discountPercentage + f.getPlusFee());
    }
}

모듈인 AddFee를 참조하여 최종 Fee를 결제하려 하고 있으므로 외부 결합이다.

그나마 개선 시도.

//그나마 개선한 것.
// 외부 모듈: 추가 요금 및 시간 정보
class Addfee {
    private int plusFee;
    private int plusTime;

    public Addfee(int plusFee, int plusTime) {
        this.plusFee = plusFee;
        this.plusTime = plusTime;
    }

    public int getTotalExtraAmount() {
        return plusFee;
    }

    public int applyToTime(int baseTime) {
        return baseTime + plusTime;
    }
}
// 메인 클래스
public class Main {
    public static void main(String[] args) {
        int a = 1000; // 추가 요금
        int b = 2;    // 추가 시간
				//추가 요금과 추가 시간은 각자 클래스에서 하는게 낫다 -> 개선이라 하긴 애매함..
        Addfee addfee = new Addfee(a, b);
        Bill bill = new Bill();

        int totalFee = bill.getBillFee(2, 80, addfee);
        System.out.println("최종 요금: " + totalFee + "원");
    }
}
class Fee { // 주차 요금 계산 모듈
    private static final int DEFAULT_MONEY = 1000;

    public int calculate(int totalTime) {
        return DEFAULT_MONEY * totalTime;
    }
}

class Bill { // 요금 청구 모듈
    private Fee fee;

    public Bill(Fee fee) {
        this.fee = fee;
    }

    public int getBillFee(int baseTime, int discountRate, Addfee addfee) {
        int adjustedTime = addfee.applyToTime(baseTime);
        int rawFee = fee.calculate(adjustedTime);
        double discountedFee = rawFee * (discountRate / 100.0);

        return (int) (discountedFee + addfee.getTotalExtraAmount());
    }
}

그냥 애초에 자료결합으로 가는게 낫다.

AddFee와 같은 모듈 없애기. 외부 참조 자체를 없애기

5. 공통 결합도 Common Coupling

  • 전역 데이터를 함께 사용
  • 전역변수 하나가 여러 모듈에 영향 줌
  • 외부 결합은 그나마 퍼블릭으로 하는거 였음.
  • 근데, 공통 결합도에서는 전역변수를 사용함 → 누가 접근했는지 알 수가 없다.

class Discount {
    public static int discount = 50; //공통 전역 데이터
    public static int discountDefault = 500; //공통 전역 데이터
}

class Bill {
    // 주차 요금 청구서 모듈
    public int getBillFee(int time) {
        double discountPercentage = Discount.discount / 100.0; // 공통 데이터 사용
        return (int) (calculateFee(time) * discountPercentage);
    }

    private int calculateFee(int time) {
        int defaultMoney = 1000 - Discount.discountDefault; // 공통 데이터 사용
        return defaultMoney * time;
    }
}

전역 변수의 값에 따라서 외부의 모듈 반환값까지 결정한다. 모듈이 매개변수 대신에 전역 변수를 이용하여 데이터를 교환하는 경우도 모두 공통 결합이다

6. 내용 결합도

  • 다른 모듈의 내부 데이터 직접 접근/수정
  • 가장 안 좋은 쪽에 가까움
  • 아까는 퍼블릭하고 스태틱하게 들어난건데, 이번에는 외부가 아예 내부의 데이터를 직접 건듦..
class UserData {
    public String name = "홍길동"; // 외부에서 직접 접근 가능
    public int age = 30;
}

class UserProcessor {
    public void makeUserOlder(UserData user) {
        user.age += 10; // 내부 구현(필드)에 직접 접근해서 변경

        if (user.age > 100) { // 비정상적인 내부 값 조작
            user.name = "고령자 " + user.name;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        UserData user = new UserData();
        UserProcessor processor = new UserProcessor();

        processor.makeUserOlder(user);

        System.out.println(user.name + " (" + user.age + "세)");
    }
}

UserData의 내부 필드에 직접 접근해서 변경하고 값을 조작하므로 내용 결합이다.


응집도(Cohesion)

한 모듈 내부 요소들이 얼마나 잘 모여 있는가

→ 높을수록 좋음

  • 여러 기능을 가지고 있다 → 응집도가 낮다!
  • ex) 주문 처리 클래스에서 회원 정보 업데이트하는 메서드가 있다 → 개 에바참치

1. 기능적 응집도

  • 하나의 기능만 수행
  • 가장 이상적
public class Stack {
    public void push(int element) { }
    public int pop() { return 0; }
    public int size() { return 0; }
}

모든 기능이 “stack” 자료구조를 위해 존재함! → 응집도 굳굳

2. 순차적 응집도

  • 앞의 출력이 뒤의 입력이 됨
class Sequential {
    void processGrade(Grade grade) {
        String numberGrade = grade.getGrade();
        //바로 다음 입력값으로 이용
        String letterGrade = grade.computeLetter(numberGrade);
        grade.displayLetter(letterGrade);
    }
}

3. 교환적 응집도 Communication

  • 같은 입력/출력 데이터를 공유 (공통된 파라미터가 메서드 호출에 사용됨!)
    • 동일한 파라미터를 이용했는데 다른 기능을 호출!
  • 순서는 크게 중요하지 않음 →(순차적 응집도와 다른점!)
class Communicational {

		void Compute_MatrixMatrix(Matrix marix) {
				int[][] aMatrix = marix.setGraph({
						{0, 0, 0},
						{0, 0, 0}
				});
		transform_matrix = marix.trans(aMatrix); //공통된 파라미터
		inverse_matrix = marix.inverse(aMatrix); //공통된 파라미터
		// 순서는 중요하지 않음
		}
}

4. 절차적 응집도 Procedural

  • 하나의 클래스에서 다수의 기능을 순차적으로 수행할 때
  • 여러개의 메서드을 호출
class Procedural extends Letter {
    void sendLetter() {
        this.writerBody();
        this.writerSalutation();
        this.send();
    } //다른 기능이 하나로 엮여있다.
}

5. 시간적 응집도 Temporal

  • 특정 시점에 함께 실행되는 기능 묶음 → 각 기능 요소들이 순서에 상관없이 수행
  • 예: 초기화, 에러 처리
public class ApplicationInitializer {
    public void initialize() {
        loadConfiguration();
        initializeLogger();
        connectToDatabase();
        initializeCache();
    }
}

6. 논리적 응집도 Logical

  • 비슷한 기능을 한 곳에 모음
  • switch/if 많아지면 복잡해짐
  • 새로운 메시지 타입이 추가될 때마다 메서드를 수정 → OCP를 위반
public class MessageHandler {
    public void handleMessage(String messageType, String message) {
        switch (messageType) {
            case "EMAIL":
                sendEmail(message);
                break;
            case "SMS":
                sendSms(message);
                break;
        }
    }
}

방정식이라는 점에서 비슷하지만, 방정식마다 역할이 다르므로 어차피 내가 나중에 선택할 것이므로
한번에 넣지말자.
-> 따로따로 객체를 만들어야지, 큰 하나의 객체를 만들고 그 안에서 결정하라는게 객체지향적이지 못함

7. 우연적 응집도 Concidental

  • 관련 없는 기능이 그냥 한데 섞여 있음
  • 가장 좋지 않음

한 줄 정리

  • 결합도: 낮을수록 좋음
  • 응집도: 높을수록 좋음

시험식 암기

  • 자료 결합도 ← 좋음
  • 내용 결합도 ← 나쁨
  • 기능적 응집도 ← 좋음
  • 우연적 응집도 ← 나쁨
profile
비니비니히비니의 정리블로그

0개의 댓글