2025-08-24 13:28

좋은 코드의 비밀 응집도 완벽 분석 핸드북

소프트웨어 개발의 세계는 복잡성과의 싸움입니다. 수십만, 수백만 줄의 코드가 얽히고설켜 만들어내는 거대한 시스템 속에서 우리는 어떻게 길을 잃지 않고 원하는 기능을 수정하고 추가할 수 있을까요? 위대한 개발자들이 수십 년간 고민해 온 이 질문에 대한 가장 중요한 답 중 하나가 바로 **응집도(Cohesion)**입니다.

응집도는 단순히 ‘코드를 깔끔하게 정리하는 것’ 이상의 의미를 가집니다. 이것은 소프트웨어의 수명을 결정하고, 개발팀의 생산성을 좌우하며, 예측 불가능한 버그로부터 우리를 지켜주는 강력한 설계 원칙입니다. 이 핸드북을 통해 응집도가 왜 만들어졌는지, 그 본질은 무엇이며, 어떻게 해야 높은 응집도를 가진 코드를 작성할 수 있는지 깊이 있게 탐험해 보겠습니다.

1. 응집도는 왜 세상에 나왔을까? 혼돈 속에서 질서를 찾아서

1960년대와 70년대, 소프트웨어 개발은 ‘소프트웨어 위기(Software Crisis)‘라 불리는 거대한 혼돈에 빠져 있었습니다. 프로젝트는 예산을 초과하고, 약속된 기간을 훌쩍 넘기기 일쑤였으며, 완성된 소프트웨어는 버그로 가득 차 신뢰할 수 없었습니다.

가장 큰 원인은 코드의 구조 부재였습니다. 당시에는 GOTO 문을 남발하여 코드의 실행 흐름이 스파게티 면처럼 얽혀있는 ‘스파게티 코드’가 만연했습니다. 이런 코드에서는 작은 기능 하나를 수정해도 전혀 예상치 못한 부분에서 오류가 발생하는 ‘파급 효과(Ripple Effect)‘가 빈번했습니다.

이러한 문제를 해결하기 위해 래리 콘스탄틴(Larry Constantine)과 에드워드 요돈(Edward Yourdon)과 같은 선구자들은 **구조적 설계(Structured Design)**라는 새로운 접근법을 제시했습니다. 이들은 거대한 문제를 작고 관리 가능한 단위로 나누는 ‘분할 정복(Divide and Conquer)’ 전략을 소프트웨어에 적용했습니다. 이때 ‘어떻게 잘 나눌 것인가?‘에 대한 핵심 기준이 바로 응집도와 **결합도(Coupling)**였습니다.

비유: 잘 정리된 공구함 vs 잡동사니 서랍

낮은 응집도는 잡동사니 서랍과 같습니다. 망치, 건전지, 영수증, 헌 칫솔이 한데 섞여있죠. 망치를 찾으려면 서랍 전체를 뒤져야 하고, 물건 하나를 꺼내면 다른 물건들이 딸려 나옵니다.

높은 응집도는 잘 정리된 공구함과 같습니다. ‘드라이버 칸’에는 종류별 드라이버만 있고, ‘볼트/너트 칸’에는 크기별로 볼트와 너트만 있습니다. 필요한 공구를 즉시 찾을 수 있고, 다른 공구에 영향을 주지 않으며, ‘드라이버 칸’ 전체를 다른 작업장으로 쉽게 가져가 재사용할 수 있습니다.

응집도는 바로 이 ‘공구함의 칸’처럼, 소프트웨어의 모듈(함수, 클래스, 컴포넌트 등)이 단 하나의 명확한 역할에만 집중하도록 만드는 원칙입니다.

2. 응집도의 7단계: 천국으로 가는 계단

응집도는 ‘있다/없다’의 이분법적인 개념이 아닙니다. 최악의 수준부터 가장 이상적인 수준까지 7개의 단계로 나뉩니다. 코드를 작성하거나 리뷰할 때, 우리가 만든 모듈이 이 중 어느 단계에 속하는지 파악하는 것은 코드 품질을 높이는 첫걸음입니다.

아래로 갈수록 더 높은 수준(더 좋은)의 응집도입니다.

단계이름 (영문)설명
1단계우연적 응집도 (Coincidental)최악. 서로 아무 관련 없는 기능들이 단지 파일 하나에 있다는 이유로 묶여있음.
2단계논리적 응집도 (Logical)유사한 성격의 기능들을 하나로 묶었지만, 호출 시 특정 기능 하나만 선택해서 실행함.
3단계시간적 응집도 (Temporal)특정 시점에 함께 실행되어야 하는 기능들이 묶여있음.
4단계절차적 응집도 (Procedural)기능들이 정해진 순서대로 실행되어야 해서 묶여있음.
5단계통신적 응집도 (Communicational)동일한 입력 데이터를 사용하거나 동일한 출력 데이터를 만들기 위해 묶여있음.
6단계순차적 응집도 (Sequential)한 기능의 출력이 다음 기능의 입력으로 사용되는 순서로 묶여있음.
7단계기능적 응집도 (Functional)이상적. 모듈 내의 모든 기능이 단 하나의 명확한 목적을 수행하기 위해 존재함.

1단계: 우연적 응집도 (Coincidental Cohesion)

가장 낮은 수준의 응집도입니다. 모듈 내의 요소들이 아무런 의미 있는 관계없이, 단지 편의상 혹은 우연히 한 파일에 모여있는 경우입니다. 잡동사니 서랍 그 자체입니다.

  • 특징: “CommonUtils”, “HelperFunctions” 같은 이름의 파일에서 흔히 발견됩니다.

  • 문제점:

    • 모듈의 목적을 전혀 이해할 수 없습니다.

    • 하나의 기능을 수정해도 다른 기능에 어떤 영향을 미칠지 예측이 불가능합니다.

    • 재사용은 거의 불가능합니다. printUser 함수가 필요해서 CommonUtils를 가져왔더니, 전혀 상관없는 calculateInterest 함수까지 딸려 오는 꼴입니다.

// 예시: CommonUtils.js - 우연적 응집도
function formatCurrency(amount) {
  // ... 금액 포맷팅 로직
}
 
function validateEmail(email) {
  // ... 이메일 유효성 검사 로직
}
 
function getUserById(id) {
  // ... 데이터베이스에서 사용자 조회
}
 
function openDatabaseConnection() {
    // ... DB 연결
}

위 코드에서 각 함수는 서로 아무런 관련이 없습니다. 통화, 유효성 검사, 데이터 조회가 하나의 모듈에 있는 것은 그저 우연일 뿐입니다.

2단계: 논리적 응집도 (Logical Cohesion)

모듈이 수행하는 작업들이 ‘논리적으로’ 유사한 범주에 속하지만, 실제로는 서로 다른 작업인 경우입니다. 보통 하나의 함수가 플래그(flag)나 타입(type) 파라미터를 받아서 내부에서 분기 처리를 하는 형태로 나타납니다.

  • 특징: 모든 종류의 ‘처리’를 담당하는 processTransaction(transaction, type) 같은 함수가 대표적입니다.

  • 문제점:

    • 사용하는 쪽에서 모듈의 내부 구현을 알아야 합니다. (어떤 type을 넘겨야 할지 알아야 함)

    • 새로운 type이 추가될 때마다 모듈 내부의 분기문이 계속 늘어나 복잡해집니다.

    • 관련 없는 코드들이 하나의 함수에 섞여 있어 가독성이 떨어집니다.

// 예시: updateHandler(data, type) - 논리적 응집도
function updateHandler(data, type) {
  if (type === 'USER_NAME') {
    // 사용자 이름 업데이트 로직
  } else if (type === 'PRODUCT_PRICE') {
    // 상품 가격 업데이트 로직
  } else if (type === 'ORDER_STATUS') {
    // 주문 상태 업데이트 로직
  }
}

이름, 가격, 주문 상태 업데이트는 모두 ‘업데이트’라는 논리적 범주에 속하지만, 실제로는 완전히 다른 작업입니다. 이들을 별도의 함수(updateUserName, updateProductPrice)로 분리하는 것이 좋습니다.

3단계: 시간적 응집도 (Temporal Cohesion)

모듈 내의 요소들이 특정 ‘시간’에 함께 실행되기 때문에 묶여있는 경우입니다. 프로그램 시작 시 필요한 모든 초기화 작업을 하나의 initialize() 함수에 모아두는 것이 대표적인 예입니다.

  • 특징: initialize, startup, shutdown 같은 이름의 함수에서 자주 보입니다.

  • 문제점:

    • 관련 없는 여러 모듈의 초기화 코드가 한 곳에 모여있어 결합도가 높아집니다. 예를 들어, initialize 함수가 데이터베이스, 로깅, UI, 네트워크 모듈을 모두 초기화한다면, 이 함수는 4개 모듈 모두에 의존하게 됩니다.

    • 초기화 순서가 중요해지면서 코드가 깨지기 쉬워집니다.

    • 일부 기능만 초기화하고 싶을 때 재사용하기 어렵습니다.

// 예시: initializeApplication() - 시간적 응집도
function initializeApplication() {
  connectToDatabase(); // DB 연결
  loadConfigFile();    // 설정 파일 로드
  initializeLogger();  // 로거 초기화
  renderMainMenu();    // 메인 메뉴 UI 렌더링
}

이 작업들은 ‘앱 시작 시’라는 시간적 공통점만 있을 뿐, 각기 다른 책임(DB, 설정, 로깅, UI)을 가집니다. 각 모듈이 스스로 초기화하도록 책임을 위임하는 것이 더 나은 설계입니다.

4단계: 절차적 응집도 (Procedural Cohesion)

모듈 내의 요소들이 특정 ‘절차’나 ‘순서’에 따라 실행되어야 하기 때문에 묶여있는 경우입니다. 시간적 응집도보다 발전했지만, 여전히 여러 개의 서로 다른 기능이 하나의 모듈에 포함되어 있습니다.

  • 특징: 한 기능의 출력이 다음 기능의 입력으로 직접 이어지지는 않지만, 실행 순서가 중요합니다.

  • 문제점:

    • 모듈이 여전히 여러 책임을 가지고 있습니다.

    • 절차의 일부만 재사용하기 어렵습니다.

    • 절차가 변경되면 모듈 전체를 수정해야 합니다.

// 예시: processUserData() - 절차적 응집도
function processUserData(userId) {
  const user = getUserFromDB(userId);
  if (!user) {
    throw new Error('User not found');
  }
 
  const hasPermission = checkUserPermission(user, 'READ_DATA');
  if (!hasPermission) {
    throw new Error('Permission denied');
  }
 
  logAccess(userId); // 접근 로그 남기기
 
  return user;
}

위 함수는 ‘사용자 조회’, ‘권한 확인’, ‘로깅’이라는 세 가지 절차를 순서대로 수행합니다. 이들은 논리적으로 분리될 수 있는 별개의 책임입니다.

5단계: 통신적 응집도 (Communicational Cohesion)

모듈 내의 요소들이 동일한 ‘데이터’를 사용하기 때문에 묶여있는 경우입니다. 절차적 응집도보다 발전한 형태로, 실행 순서보다는 ‘공유 데이터’가 중심이 됩니다.

  • 특징: 동일한 데이터 구조를 입력으로 받아 여러 다른 작업을 수행합니다.

  • 문제점:

    • 여전히 모듈이 여러 책임을 가질 수 있습니다.

    • 수행하는 작업 중 일부만 필요할 경우에도 전체 데이터 구조를 넘겨야 합니다.

// 예시: processBookData(book) - 통신적 응집도
function processBookData(book) {
  // book 객체를 사용
  updateBookPriceInDB(book.id, book.price);
 
  // book 객체를 사용
  generateBookReport(book.title, book.author);
 
  // book 객체를 사용
  addBookToSearchIndex(book);
}

가격 업데이트, 리포트 생성, 검색 인덱싱은 모두 book이라는 동일한 데이터를 사용하지만, 각기 다른 책임입니다. 이들을 별도의 함수로 분리하고 book 객체를 각각 전달하는 것이 더 좋습니다.

6단계: 순차적 응집도 (Sequential Cohesion)

모듈 내의 한 요소의 ‘출력’이 다음 요소의 ‘입력’으로 사용되는 конвейер(파이프라인) 형태를 가질 때입니다. 통신적 응집도보다 더 강하게 연결되어 있으며, 꽤 좋은 수준의 응집도입니다.

  • 특징: 데이터가 모듈 내에서 단계적으로 가공되어 전달됩니다.

  • 문제점:

    • 결합도가 높아질 수 있습니다. 중간 단계 하나가 바뀌면 전체 파이프라인에 영향을 줄 수 있습니다.

    • 파이프라인의 일부만 재사용하기는 여전히 어렵습니다.

// 예시: readAndProcessFile(filePath) - 순차적 응집도
function readAndProcessFile(filePath) {
  const rawData = readFile(filePath); // 1. 파일 읽기
  const jsonData = parseJSON(rawData); // 2. JSON 파싱 (1의 출력이 2의 입력)
  const validatedData = validateSchema(jsonData); // 3. 유효성 검사 (2의 출력이 3의 입력)
  return validatedData;
}

이 함수는 그 자체로 하나의 작업을 수행하는 것처럼 보이지만, ‘파일 읽기’, ‘JSON 파싱’, ‘스키마 검증’이라는 세 개의 하위 기능으로 명확히 나눌 수 있습니다. 각 기능을 별도의 함수로 만들고 조합하는 것이 재사용성 측면에서 더 유리합니다.

7단계: 기능적 응집도 (Functional Cohesion)

가장 이상적인 최상의 응집도입니다. 모듈 내의 모든 요소가 단 하나의, 잘 정의된 기능을 수행하는 데 필수적인 경우입니다.

  • 특징:

    • 모듈의 이름을 “동사 + 명사” 형태로 명확하게 지을 수 있습니다. (calculateSalesTax, getUserProfile)

    • 모듈이 무엇을 하는지 설명할 때 “그리고(and)“라는 단어를 사용할 필요가 없습니다.

  • 장점:

    • 이해하기 쉽다: 모듈의 목적이 명확하여 코드를 읽고 이해하기 매우 쉽습니다.

    • 재사용성이 높다: 단일 기능을 수행하므로 다른 곳에서 필요할 때 가져다 쓰기 완벽합니다.

    • 테스트가 쉽다: 입력과 출력이 명확한 단일 기능이므로 단위 테스트(Unit Test)를 작성하기 매우 용이합니다.

    • 유지보수가 쉽다: 기능 변경이 필요할 때 수정해야 할 범위가 해당 모듈로 명확하게 한정됩니다.

// 예시: calculateSquareRoot(number) - 기능적 응집도
function calculateSquareRoot(number) {
  if (number < 0) {
    throw new Error("Cannot calculate square root of a negative number.");
  }
  return Math.sqrt(number);
}

이 함수는 오직 ‘제곱근 계산’이라는 단 하나의 기능에만 집중합니다. 함수 내의 모든 코드는 이 목적을 달성하기 위해 존재합니다. 더 이상 나눌 수도 없고, 다른 기능이 섞여 있지도 않습니다. 이것이 우리가 지향해야 할 목표입니다.

3. 높은 응집도를 달성하기 위한 실천 가이드

그렇다면 어떻게 해야 코드의 응집도를 높일 수 있을까요? 다음 원칙들을 의식하며 코드를 작성해 보세요.

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP)을 따르세요. SOLID 원칙 중 첫 번째인 SRP는 클래스(또는 모듈)는 변경되어야 할 이유가 단 하나여야 한다는 원칙입니다. 이는 기능적 응집도의 다른 표현과 같습니다. “이 클래스를 수정해야 하는 이유는 무엇인가?”라는 질문에 대한 답이 여러 개라면, 응집도가 낮다는 신호입니다.

  2. 이름을 명확하고 구체적으로 지으세요. 모듈의 이름이 모호하다면 응집도가 낮을 가능성이 높습니다. processData, handleStuff 같은 이름 대신 parseCsvAndValidateRecords와 같이 구체적으로 이름을 지어보세요. 만약 이름에 ‘and’가 들어간다면, 해당 모듈이 여러 책임을 가지고 있을 수 있다는 강력한 신호이므로 분리를 고려해야 합니다.

  3. 작게 만드세요. 함수나 클래스가 너무 길고 복잡하다면, 여러 가지 일을 하고 있을 가능성이 큽니다. 코드를 작은 단위로 나누는 리팩토링(Refactoring) 기법, 예를 들어 ‘함수 추출(Extract Method)‘이나 ‘클래스 추출(Extract Class)‘을 적극적으로 활용하여 응집도를 높일 수 있습니다.

  4. 스스로에게 질문하세요: “이 코드가 여기에 있는 것이 최선인가?” 코드를 작성할 때마다 이 코드가 현재 모듈의 핵심 책임과 직접적으로 관련이 있는지 자문해보세요. 만약 조금이라도 관련이 없다면, 그 코드가 있어야 할 더 적합한 장소가 있는지 고민해야 합니다.

결론: 응집도는 단순한 기술이 아닌, 장인 정신입니다.

응집도는 단순히 코드를 분류하는 학문적인 개념이 아닙니다. 이것은 복잡한 시스템을 명확하고, 견고하며, 유연하게 만드는 실용적인 도구이자 철학입니다.

높은 응집도를 가진 코드는 당장 눈앞의 기능을 빠르게 구현하는 것보다 더 많은 시간과 고민을 요구할 수 있습니다. 하지만 이러한 노력은 장기적으로 엄청난 보상으로 돌아옵니다. 버그를 찾기 위해 밤을 새우는 시간이 줄어들고, 새로운 요구사항에 자신감 있게 대응할 수 있게 되며, 동료들과 협업하는 즐거움이 커질 것입니다.

오늘부터 여러분의 코드 한 줄 한 줄에 응집도의 렌즈를 적용해 보세요. 여러분의 소프트웨어는 더욱 단단해지고, 개발자로서의 여러분은 한 단계 더 성장하게 될 것입니다.

레퍼런스(References)

응집도