결합도와 응집도
모듈 사이의 의존을 보는 결합도와 모듈 안의 관련성을 보는 응집도를 구별하고, 전통적인 분류를 실제 코드의 위험과 연결합니다. 저장 형식이 바뀌어도 자리 계산이 그대로인 예제를 실행해, 나눈 곳이 변경을 얼마나 막아 주는지 확인합니다.
프로그램을 파일 열 개로 나누었는데 하나를 고칠 때마다 나머지 아홉 개도 함께 바꿔야 한다면, 나누기 전보다 이해하기 쉬워졌다고 말하기 어렵습니다. 반대로 어떤 규칙을 고칠 때 모듈 하나 안에서 필요한 부분을 모두 찾을 수 있다면 변경이 그 안에서 끝납니다. 결합도는 모듈 사이를, 응집도는 모듈 안을 보는 기준입니다. 이 글에서는 두 기준에 점수를 매기기보다, 무엇이 바뀌었을 때 어디까지 영향을 받는지를 비교합니다.
결합도는 서로 얼마나 기대는지를 본다
결합도(coupling)는 모듈 사이에 서로 기대는 정도입니다. 모듈 A가 모듈 B의 함수 이름만 아는지, B의 내부 배열 구조와 파일 경로, 전역 변수까지 아는지에 따라 B를 고칠 때 A가 받는 영향이 달라집니다. 그렇다고 호출 자체를 없애라는 뜻은 아닙니다. 모듈은 서로 협력해야 기능을 완성하므로, 필요한 의존은 분명하게 드러내고 불필요한 의존을 줄이는 것이 목표입니다.
예를 들어 신청 정책 함수가 app.cache.current.sessions[0].members를 읽는다면, 이 함수는 캐시의 구조와 배열의 첫 번째 위치에 기대고 있습니다. 정원 규칙은 그대로인데 캐시를 없애거나 배열의 순서만 바꾸어도 정책 함수가 망가집니다. 정책 함수가 필요한 참가자 수와 정원만 받도록 하면 이런 저장 구조에 대한 의존을 줄일 수 있습니다.
인수가 적다고 결합이 느슨한 것도 아닙니다. check(3, 5, true)를 부르는 쪽은 각 숫자의 단위와 true가 무슨 뜻인지 알아야 합니다. 반면 { participantCount: 3, capacity: 5 }처럼 이름이 붙은 객체로 넘기면 어떤 정보가 필요한지 드러납니다. 인수의 개수보다, 넘기는 값의 의미가 분명한지와 그 값이 얼마나 자주 바뀔지를 봅니다.
응집도는 함께 묶인 이유를 본다
응집도(cohesion)는 한 모듈 안의 요소들이 하나의 목적에 얼마나 관련되어 있는지를 나타냅니다. 참가 명단의 조회, 추가, 제거는 같은 명단의 규칙을 다루므로 한 모듈에 모일 이유가 있습니다. 반면 파일 이름 정하기, 장학금 계산, 화면 색 고르기가 "오늘 추가한 함수"라는 이유로 misc라는 모듈에 모여 있다면, 함께 바뀔 이유가 거의 없습니다.
응집도가 높다는 것은 코드가 짧다는 뜻도, 이름에 같은 단어가 들어간다는 뜻도 아닙니다. "출력"이라는 이름 아래 모임 안내 출력과 영수증 파일 저장을 묶었는데, 안내 문구와 회계 형식이 서로 다른 때에 바뀐다면 목적이 갈라진 것입니다. 한 기능을 이해하려 할 때 관계없는 규칙까지 읽어야 한다면 응집도가 낮다는 신호입니다.
응집도는 높이고 결합도는 낮추는 것이 일반적인 방향이며, 두 기준은 서로를 돕습니다. 같은 책임을 모듈 안에 모아 두고 바깥에는 필요한 함수만 드러내면, 안쪽을 고쳐도 그 영향이 바깥으로 덜 퍼집니다. 다만 그렇게 해도 모든 의존이 사라지거나 모든 변경이 파일 하나에서 끝나지는 않습니다.
SOFTWARE ENGINEERING · CH4-2
저장 구조 대신 필요한 사실로 계산한다
정원 계산은 참가자 수·정원만 읽고 파일 이름을 알지 않습니다.
저장된 모임에서 참가자 수와 정원을 추출해 자리 계산과 안내 표현에 전달하며 계산은 저장 파일 이름에 의존하지 않습니다.
전통적인 분류를 실제 코드에 연결한다
결합도와 응집도에는 전통적으로 쓰는 분류 이름이 있습니다. 이름의 순서를 외우기보다, 각 형태가 실제 코드에서 어떤 위험으로 나타나는지 연결해 두는 편이 쓸모 있습니다. 결합도는 다음 표에서 아래로 갈수록 강하다고 봅니다.
| 결합의 형태 | 코드에서 보이는 모습 | 확인할 질문 |
|---|---|---|
| 자료 결합 | 필요한 숫자나 아이디만 인수로 넘긴다 | 값의 의미와 단위가 분명한가 |
| 스탬프 결합 | 큰 객체를 통째로 넘기고 일부만 쓴다 | 쓰지 않는 구조까지 알아야 하는가 |
| 제어 결합 | 플래그 값으로 상대의 내부 동작을 고른다 | 서로 다른 일을 다른 함수로 나눌 수 있는가 |
| 외부 결합 | 여러 모듈이 같은 파일 형식이나 장치 규약을 각자 가정한다 | 형식을 해석하는 곳이 한 곳인가 |
| 공통 결합 | 여러 모듈이 같은 전역 명단을 함께 고친다 | 상태를 누가 갖고 어디로 고치는지 분명한가 |
| 내용 결합 | 상대의 내부 데이터나 처리를 직접 바꾼다 | 공개된 함수로 협력할 수 있는가 |
객체 하나를 넘겼다고 해서 무조건 나쁜 스탬프 결합은 아닙니다. 모임에 대한 정책이 모임의 여러 속성을 함께 읽는 것은 자연스러울 수 있으므로, 바깥의 형식이 바뀔 때 쓸데없는 영향이 생기는지를 실제 변경으로 확인합니다. 제어 플래그도 뜻이 분명한 선택 사항에는 쓸 수 있지만, mode 값에 따라 서로 관계없는 업무를 처리하기 시작하면 책임이 흐려집니다.
응집도는 다음 표에서 위로 갈수록 강하다고 봅니다.
| 응집의 형태 | 묶인 이유 | 예 |
|---|---|---|
| 기능적 응집 | 하나의 목적을 함께 이룬다 | 명단의 조회, 추가, 제거 |
| 순차적 응집 | 앞 단계의 출력이 다음 단계의 입력이 된다 | 출석 기록 읽기와 출석률 계산 |
| 통신적 응집 | 같은 데이터를 읽거나 만든다 | 같은 명단으로 인원 수와 안내 목록 만들기 |
| 절차적 응집 | 정해진 순서로 실행되지만 데이터를 주고받지 않는다 | 화면 초기화, 기록 남기기, 버튼 표시 |
| 시간적 응집 | 같은 시점에 실행된다 | 프로그램을 시작할 때 하는 설정 모음 |
| 논리적 응집 | 비슷한 종류의 일이다 | 모든 출력 함수를 모은 모듈 |
| 우연적 응집 | 묶인 이유가 없다 | misc나 utils에 쌓인 함수 |
같은 시점에 실행된다거나 비슷한 종류라는 이유만으로 묶은 코드는, 그 안의 일들이 서로 다른 때에 바뀌기 쉽습니다. 이 분류는 설계를 두고 이야기할 때 쓰는 관점이며, 모듈의 품질을 숫자로 정확히 재는 측정법은 아닙니다.
필요한 사실만 받아 계산한다
그림처럼 저장된 모임에서 필요한 사실만 뽑아 자리 계산과 안내에 넘기면, 계산하는 함수는 저장 형식을 몰라도 됩니다. 다음 코드를 reading-lab/examples/ch4-2.mjs로 저장하고 node examples/ch4-2.mjs로 실행합니다. 이 예제는 core.mjs를 쓰지 않습니다.
function hasAvailableSeat({ participantCount, capacity }) {
return participantCount < capacity;
}
function availabilityText({ title, participantCount, capacity }) {
const remaining = Math.max(0, capacity - participantCount);
return title + ": 남은 자리 " + remaining;
}
function factsFromV1(stored) {
return { title: stored.title, participantCount: stored.participants.length, capacity: stored.capacity };
}
function factsFromV2(stored) {
return { title: stored.meta.title, participantCount: stored.members.length, capacity: stored.meta.limit };
}
const v1 = { fileName: "sessions.json", title: "설계를 읽다", capacity: 3, participants: ["member-a"] };
const v2 = {
fileName: "data/book-06.json",
meta: { title: "설계를 읽다", limit: 3 },
members: [{ id: "member-a", joinedAt: "2026-10-01" }],
};
for (const facts of [factsFromV1(v1), factsFromV2(v2)]) {
console.log(hasAvailableSeat(facts), availabilityText(facts));
}true 설계를 읽다: 남은 자리 2
true 설계를 읽다: 남은 자리 2v1과 v2는 같은 모임을 서로 다른 저장 형식으로 담은 것입니다. 파일 이름이 바뀌고, 정원의 이름이 limit으로 바뀌고, 참가자가 아이디 문자열에서 가입일을 가진 객체로 바뀌었지만, 두 형식 모두 같은 결과를 냈습니다. 형식이 바뀔 때 고친 곳은 저장 자료를 사실로 바꾸는 factsFromV2 하나이고, hasAvailableSeat와 availabilityText는 손대지 않았습니다. "자리 계산은 저장 형식을 모른다"는 설계 의도를 이렇게 실제 변경으로 확인할 수 있습니다.
hasAvailableSeat가 true라고 해서 신청이 반드시 성공하는 것은 아닙니다. 아이디 형식, 마감 여부, 중복 검사도 통과해야 하기 때문입니다. 또 Math.max는 안내에 음수가 나오지 않게 할 뿐, 정원을 넘은 명단을 정상으로 만들어 주지 않습니다. 그런 자료는 상태와 데이터 모델에서 본 것처럼 저장 자료를 읽을 때 불변 조건 검사로 찾아야 하며, 화면 표시를 다듬는 일과 불변 조건을 지키는 일을 섞지 않습니다.
작은 변경으로 나눈 곳을 평가한다
나눈 곳이 적절한지는 예상되는 변경을 하나씩 적용해 보면 알 수 있습니다. "남은 자리" 문구를 바꾸는 변경은 availabilityText만 고치면 되고, 자리 계산의 결과는 그대로입니다. 참가자를 배열이 아니라 데이터베이스 조회 결과로 받는 변경은 사실로 바꾸는 함수에서 인원 수를 만들어 주면 되므로 자리 계산 함수는 그대로 둘 수 있습니다. 반면 "진행자는 정원에 세지 않는다"는 업무 규칙의 변경은 참가자 수의 뜻 자체를 바꾸므로, 정책과 집계 방법을 함께 다시 살펴야 합니다.
이처럼 표현의 변경, 저장의 변경, 업무 규칙의 변경은 영향을 주는 범위가 서로 다릅니다. 모든 변경을 막는 설계는 없으므로, 예상되는 변경마다 영향을 받는 범위를 설명할 수 있는 설계가 검토하기 쉽습니다.
공통 결합이 위험한 이유도 같은 방식으로 볼 수 있습니다. 짧은 함수 여러 개가 전역 변수인 명단과 남은 자리 수를 함께 고치면, 한 함수가 둘 중 하나만 고쳐도 같은 사실이 두 곳에서 서로 달라집니다. 또 모듈이 내부 배열을 그대로 돌려주어 호출하는 쪽이 push할 수 있다면, 이름만 감춘 것일 뿐 내용 결합과 다르지 않습니다. 다음 글에서는 공개한 함수를 통해서만 내부 상태를 바꾸게 하는 방법을 봅니다.
정리
- 결합도는 모듈 사이에 서로 기대는 정도이고, 응집도는 한 모듈 안의 요소가 하나의 목적에 관련된 정도입니다.
- 결합도를 볼 때는 인수의 개수보다 상대의 내부 구조나 저장 형식을 알아야 하는지를 확인합니다.
- 응집도를 볼 때는 같은 시점에 실행되거나 비슷한 종류라는 이유가 아니라 함께 바뀔 이유가 있는지를 확인합니다.
- 전통적인 분류 이름은 실제 코드의 위험을 이야기하는 관점이며, 품질을 숫자로 재는 측정법이 아닙니다.
- 나눈 곳이 적절한지는 예상되는 변경을 적용해 영향을 받는 범위가 좁은지로 판단합니다.
연습 문제
1. 알림 도구 다시 나누기
notifyTools 모듈에는 수업 취소 메일 본문 만들기, 문자 메시지 보내기, 이미지 크기 바꾸기, 출석 점수 계산이 들어 있습니다. 메일 문구는 교무 담당자가, 문자 발송 업체는 운영 담당자가 바꿉니다. 시험을 돌릴 때마다 실제 문자가 나가는 것도 문제입니다. 모듈을 새로 나누고, 발송 업체를 바꾸어도 다른 기능에 영향이 없도록 함수의 입력과 출력을 정하세요.
해설 보기
buildCancellationMessage는 수업 정보와 취소 이유를 받아 메시지 문자열을 돌려주고, sendMessage는 받는 사람과 메시지를 받아 발송 업체에 넘깁니다. 이미지 크기 바꾸기와 출석 점수 계산은 알림과 바뀌는 이유가 다르므로 각자 다른 모듈로 옮깁니다. 시험에서는 메시지를 만든 결과와 sendMessage에 넘길 값을 확인하고, 실제 업체와의 연결은 따로 확인합니다. 발송 업체가 바뀌면 sendMessage의 안쪽만 고치면 되고, 메시지를 만드는 함수와 점수 계산은 영향을 받지 않습니다.
2. 같은 때에 실행된다는 이유
1번에서 "네 기능은 모두 알림을 보내기 직전에 실행되니 시간적 응집이 있다, 그러니 한 모듈에 두어도 된다"는 의견이 나왔습니다. 이 판단을 평가하세요.
해설 보기
같은 때에 실행된다는 것은 시간적 응집일 뿐, 함께 바뀔 이유가 있다는 뜻은 아닙니다. 이 넷을 한 모듈에 두면 이미지 형식을 바꾸는 일이 알림 모듈 전체를 다시 확인하게 만들고, 문자 발송을 흉내 내려 해도 관계없는 코드까지 함께 불러오게 됩니다. 실행 순서를 맞추는 상위 함수는 따로 둘 수 있지만, 각 기능은 바뀌는 이유에 따라 나누는 편이 변경을 이해하기 쉽습니다.
3. 두 곳에 적은 같은 사실
다음 코드에서 join("min"), join("hana"), leave("min")을 차례로 실행하면 participants와 availableSeats는 어떻게 되나요? 무엇이 잘못되었는지 설명하고 고치는 방향을 제안하세요.
let participants = [];
let availableSeats = 2;
function join(id) {
participants.push(id);
availableSeats -= 1;
}
function leave(id) {
participants = participants.filter((p) => p !== id);
}해설 보기
participants는 [ 'hana' ]가 되지만 availableSeats는 0으로 남아, 실제로는 자리가 하나 있는데 남은 자리가 없다고 나옵니다. leave가 availableSeats를 고치는 일을 빠뜨렸기 때문이며, 같은 사실을 두 변수에 나누어 적고 여러 함수가 함께 고치는 공통 결합의 위험이 그대로 드러난 것입니다. 남은 자리는 따로 저장하지 말고 필요할 때 capacity - participants.length로 계산하면, 고쳐야 할 곳이 명단 하나로 줄어 둘이 어긋날 수 없습니다.
결합도는 필요한 의존을 드러내고 불필요한 내부 의존을 줄이는 기준이고, 응집도는 같은 책임에 필요한 요소를 한곳에 모으는 기준입니다. 다음 글 인터페이스·객체·관계에서는 이렇게 나눈 곳을 객체와 인터페이스로 나타내 보고, 클래스 문법을 쓰는 것만으로 설계가 좋아지지는 않는다는 점을 확인합니다.
원자료와 더 읽기
장원사이버평생교육원 「소프트웨어공학」 PDF 물리 159–169쪽의 주제를 바탕으로 설명, 도표, 실습을 새로 구성했습니다. 결합도와 응집도의 전통적인 분류는 이름의 순서보다 변경이 미치는 영향의 사례로 다루었으며, JavaScript 파일의 공개 범위는 MDN 모듈 안내를 참고합니다.