본문 바로가기

책임을 나누는 모듈

정책 판단·상태 생성·입출력을 변경 이유로 나누고 추상화·정보 은닉·모듈화의 차이를 실행에서 확인합니다.

신청 판정, 파일 저장, 화면 문구를 모두 한 함수에 넣으면 한 기능을 바꿀 때 다른 기능까지 다시 이해해야 합니다. ‘정원이 찼을 때 안내를 더 친절하게 쓰자’는 요청에 파일 쓰기 코드까지 손대는 상황이 생깁니다. 모듈은 변경을 이해하고 통제할 수 있도록 책임을 나눈 단위입니다.

이번 본문에서는 앞 장의 신청 함수를 판단·상태 생성·호출 흐름으로 나눕니다. 함수를 호출하고 반환값을 받을 수 있으면 충분합니다. 파일 수나 함수 수를 늘리는 것이 목표가 아니라 각각이 무엇을 알고 무엇을 바꾸는지 설명할 수 있어야 합니다.

책임·추상화·정보 은닉

책임(responsibility)은 모듈이 해야 할 일과 알아야 할 정보입니다. ‘신청 가능 여부를 판단한다’는 책임에는 모집 상태·명단·정원이 필요하지만 파일 경로나 문구 색상은 필요하지 않습니다. ‘결과를 표시한다’는 책임에는 결과 코드와 사용자 문구가 필요합니다.

추상화(abstraction)는 목적에 필요한 특징을 드러내고 세부를 잠시 생략하는 것입니다. register(session, memberId)라는 이름으로 입력과 결과를 다룰 때 호출자는 includes나 배열 복사 순서를 매번 알 필요가 없습니다. 추상화는 어려운 이름을 붙이는 일이 아니라 필요한 약속을 읽기 쉽게 드러내는 일입니다.

정보 은닉(information hiding)은 외부가 바뀔 수 있는 내부 결정에 의존하지 않도록 그 결정을 감추는 설계입니다. 저장소가 배열인지 파일인지 몰라도 load/save 계약을 통해 사용할 수 있게 만드는 것이 예입니다. 캡슐화(encapsulation)는 관련 데이터와 동작을 경계 안에 묶는 수단입니다. 하나의 객체로 묶었다고 자동으로 모든 내부 정보가 보호되지는 않습니다. 내부 배열을 그대로 반환하면 바깥 코드가 수정할 수 있습니다.

모듈화(modularization)는 이런 책임과 경계를 기준으로 시스템을 나누는 작업입니다. 함수, 객체, 파일, 패키지 등 여러 크기의 단위를 쓸 수 있습니다. 모든 언어의 모듈이 독립 실행이나 독립 컴파일이 가능한 것은 아니므로 특정 언어의 특징을 보편적 정의로 삼지 않습니다.

변경 이유로 경계를 찾는다

예상 변경수정할 책임같이 바꿀 필요 없는 것
신청 정책 변경정책 판정한국어 안내 문구·파일 이름
저장 위치 변경저장소정원 규칙·명단 검사
안내 문구 변경표현신청 허용 조건
입력 방식 변경입력 변환이미 정규화된 ID의 정책

단일 책임 원칙(SRP)을 ‘함수는 반드시 한 줄’ 또는 ‘클래스에 메서드 하나’로 해석하지 않습니다. 같은 이유로 함께 바뀌는 것들을 모으고 다른 이유로 바뀌는 것들을 분리하는 기준입니다. 여러 계산 단계가 한 업무 책임에 속할 수 있습니다. 반대로 이름이 utils 하나여도 무관한 책임이 모여 있을 수 있습니다. Robert C. Martin의 설명도 변경 이유를 함께 묶을 기준으로 다룹니다.

분리에는 비용이 듭니다. 함수가 많아지면 호출 관계를 따라가야 하며 같은 정보를 여러 경계에서 반복 전달할 수 있습니다. 아직 한 번만 쓰는 한 줄을 무조건 파일 하나로 떼어 내면 이해가 어려워집니다. 설명하려는 규칙이 독립적인지, 별도로 검증할 이점이 있는지, 변경이 자주 갈라지는지를 보고 결정합니다.

신청의 외부 계약을 유지하며 내부 책임을 나눈다

유효한 member-c, OPEN, 빈 명단을 읽는다. 공개 register 계약에 따라 결과를 돌려준다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · CH4-1

신청의 외부 계약을 유지하며 내부 책임을 나눈다

호출자는 하나의 신청 결과를 받고 내부는 판단과 상태 생성을 분리합니다.

신청의 외부 계약을 유지하며 내부 책임을 나눈다신청 함수가 정책 판정을 받아 상태 생성에 전달하고 ok·code·state 결과로 호출자에게 돌려주는 내부 처리 흐름입니다.register외부에 공개할 신청 기능정책 판정입력을 읽고 코드 반환상태 생성성공일 때만 새 명단신청 결과ok · code · state

신청 함수가 정책 판정을 받아 상태 생성에 전달하고 ok·code·state 결과로 호출자에게 돌려주는 내부 처리 흐름입니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 정책 판정부터 결과 반환까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
정책 판정

유효한 member-c, OPEN, 빈 명단을 읽는다.

  • 정책·새 상태 1: capacity1
  • 정책·새 상태 2: participants []
  • code REGISTERED
새 상태 생성

성공을 나타내는 코드일 때만 새 명단을 만든다.

  • 정책·새 상태 1: 새 participants [member-c]
  • 정책·새 상태 2: 입력 명단 [] 유지
  • 새 상태 객체 생성
결과 반환

공개 register 계약에 따라 결과를 돌려준다.

  • 정책·새 상태 1: ok=true
  • 정책·새 상태 2: code=REGISTERED
  • 정책·새 상태 3: state의 명단 [member-c]
  • 호출자에게 결과 반환

신청의 외부 계약을 유지하며 내부 책임을 나눈다

유효한 member-c, OPEN, 빈 명단을 읽는다. 공개 register 계약에 따라 결과를 돌려준다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · CH4-1

신청의 외부 계약을 유지하며 내부 책임을 나눈다

호출자는 하나의 신청 결과를 받고 내부는 판단과 상태 생성을 분리합니다.

신청의 외부 계약을 유지하며 내부 책임을 나눈다신청 함수가 정책 판정을 받아 상태 생성에 전달하고 ok·code·state 결과로 호출자에게 돌려주는 내부 처리 흐름입니다.register외부에 공개할 신청 기능정책 판정입력을 읽고 코드 반환상태 생성성공일 때만 새 명단신청 결과ok · code · state

신청 함수가 정책 판정을 받아 상태 생성에 전달하고 ok·code·state 결과로 호출자에게 돌려주는 내부 처리 흐름입니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 정책 판정부터 결과 반환까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
정책 판정

유효한 member-c, OPEN, 빈 명단을 읽는다.

  • 정책·새 상태 1: capacity1
  • 정책·새 상태 2: participants []
  • code REGISTERED
새 상태 생성

성공을 나타내는 코드일 때만 새 명단을 만든다.

  • 정책·새 상태 1: 새 participants [member-c]
  • 정책·새 상태 2: 입력 명단 [] 유지
  • 새 상태 객체 생성
결과 반환

공개 register 계약에 따라 결과를 돌려준다.

  • 정책·새 상태 1: ok=true
  • 정책·새 상태 2: code=REGISTERED
  • 정책·새 상태 3: state의 명단 [member-c]
  • 호출자에게 결과 반환

판단과 상태 생성 사이의 계약

registrationCode는 입력만 읽고 결과 코드 하나를 돌려줍니다. nextRegistrationState는 판정된 코드에 따라 새 명단을 만듭니다. register는 둘을 연결해 앞 장과 같은 { ok, code, state } 결과를 반환합니다.

분리된 상태 생성 함수는 유효한 판정을 받는다는 내부 전제를 가집니다. 아무 호출자가 REGISTERED를 임의로 넣으면 중복이나 정원을 우회할 수 있습니다. 따라서 실제 파일 모듈로 나눌 때는 register만 공개하고 나머지 둘을 내부 구현으로 둘 수 있습니다. 연습 예제에서는 흐름을 읽기 위해 모두 보이게 했습니다.

저장소 최상위 폴더에서 다음 명령을 실행합니다. Node.js가 설치되어 있어야 하며 패키지를 추가로 설치하지 않습니다. 두 번째 인수를 생략하면 이 장의 예제를 모두 실행합니다.

node dev-note/scripts/software-engineering-lab/examples/ch4.mjs ch4-1
ch4-1 실행 예제
function registrationCode(session, memberId) {
  if (typeof memberId !== "string" || !/^[a-z][a-z0-9-]{0,23}$/.test(memberId)) return "INVALID_MEMBER";
  if (session.status !== "OPEN") return "CLOSED";
  if (session.participants.includes(memberId)) return "ALREADY_REGISTERED";
  if (session.participants.length >= session.capacity) return "FULL";
  return "REGISTERED";
}
function nextRegistrationState(session, memberId, code) {
  if (code !== "REGISTERED") return session;
  return { ...session, participants: [...session.participants, memberId] };
}
function register(session, memberId) {
  const code = registrationCode(session, memberId);
  return {
    ok: code === "REGISTERED",
    code,
    state: nextRegistrationState(session, memberId, code),
  };
}
const session = {
  id: "book-04", title: "느리게 읽기",
  capacity: 1, status: "OPEN", participants: [],
};
const result = register(session, "member-c");
console.log(result.code, result.state.participants.join(","));
console.log("입력 상태 보존:", session.participants.length === 0);

REGISTERED member-c와 입력 상태 보존: true를 확인합니다. 한 함수였던 앞 장의 구현과 외부 결과가 같습니다. 리팩터링(refactoring)은 관찰 가능한 동작을 유지하면서 내부 구조를 개선하는 작업입니다. 검사 순서나 오류 코드를 바꾸면 구조 변경과 동작 변경이 함께 일어난 것이므로 구분해 검증해야 합니다.

registrationCode에는 파일 쓰기나 출력이 없어 같은 입력에 같은 코드가 나옵니다. 이런 계산은 직접 실행해 경계 조건을 확인하기 쉽습니다. 반면 파일 저장은 외부 상태와 실패 가능성을 다루며 나중에 별도 경계로 둡니다.

실제 파일로 나눌 때

JavaScript에서는 export로 공개할 값·함수를 정하고 import로 가져옵니다. 함수가 다른 파일로 이동했다고 책임이 자동으로 개선되지는 않습니다. 외부가 내부 변수와 내부 도움 함수를 모두 가져가면 파일 경계만 생기고 의존 관계는 남습니다.

이 실행 파일은 설치 없이 비교하도록 한 파일 안에 함수를 두었습니다. 실제 앱에서는 registration.mjs가 register를 공개하고, presenter.mjs가 결과 코드를 문구로 바꾸며, main.mjs가 입력과 출력을 연결할 수 있습니다. 모듈을 옮길 때는 상대 경로·공개 함수 이름·순환 import도 확인해야 합니다. MDN 모듈 안내는 JavaScript의 import/export를 설명합니다.

실패하는 분리의 반례

정원 확인을 checkCapacity로 떼면서 내부에서 파일을 읽고 안내문까지 출력하게 만들었다고 합시다. 이름은 ‘검사’인데 실행하려면 디스크 상태와 콘솔까지 준비해야 합니다. 호출자가 결과를 받지 못하면 다른 UI로 재사용하기도 어렵습니다. 책임을 나눴다는 주장은 파일 개수보다 입력·출력·부수효과로 확인해야 합니다.

부수효과(side effect)는 반환값 밖의 변화입니다. 파일 쓰기·화면 출력·외부 객체 수정이 예입니다. 부수효과 자체가 나쁜 것은 아닙니다. 사용자가 얻는 결과에는 저장·출력이 필요합니다. 계산과 외부 변화의 위치를 알 수 있어야 테스트와 실패 처리가 분명해집니다.

독립 문제: 출석 요약을 나눈다

출석 보고 함수가 학생 명단을 읽고, 출석률을 계산하고, 장학 기준을 판정하고, HTML을 만들고, 파일에 저장합니다. 학교는 장학 기준과 보고서 모양을 별도로 바꿉니다.

책임을 나누어 함수 이름·입력·출력을 제안합니다. 외부 환경 없이 검증할 계산 두 개와 파일 오류를 처리할 경계 한 개를 고르고, ‘학생별 평균 한 줄마다 파일을 나눈다’는 제안이 도움이 되는지도 판단합니다.

힌트

출석률·장학 판정·HTML은 모두 보고 작업에 참여하지만 변경 이유가 같지는 않습니다. 계산 결과를 출력한 문자열로만 주고받으면 정책을 다시 쓰기 어려워집니다.

풀이

loadAttendance가 데이터를 읽고, attendanceRate가 비율을 계산하며, isEligible이 명시한 기준으로 장학 대상 여부를 반환합니다. renderReport는 계산 결과를 HTML로 바꾸고 saveReport는 문자열을 파일에 저장합니다. 비율 계산과 기준 판정은 배열·숫자만 주어 검증할 수 있고, 파일 오류는 저장 함수와 이를 호출하는 흐름이 처리합니다.

학생별 계산이 같은 규칙이라면 한 함수와 반복으로 표현할 수 있습니다. 학생마다 파일을 나누면 새 학생이 늘 때 파일과 import가 늘고 정책 변경을 반복해야 합니다. 책임·변경 이유가 갈라지는 지점에 경계를 두는 것이 더 낫습니다.

복습: 경계를 설명할 수 있는가

공개 입력·결과·보존할 상태를 설명하면 경계가 드러납니다. 다음 본문에서는 이 경계 사이의 의존이 얼마나 강한지, 한 경계 안의 기능이 얼마나 함께 바뀌는지 결합도와 응집도로 비교합니다.


원자료와 더 읽기

장원사이버평생교육원 「소프트웨어공학」 PDF 물리 128–139·159–169쪽의 주제를 바탕으로 설명·도표·실습을 새로 구성했습니다.

설계 기준은 단일 책임 원칙의 원저자 설명, 언어의 파일 경계는 MDN JavaScript 모듈을 참고합니다.

공통 ID 정책은 소문자로 시작하는 1~24자이며 이후 소문자·숫자·하이픈만 허용합니다. 실행 검증은 대문자·숫자 시작·공백·밑줄·25자와 유효한 24자를 공통 core 결과와 비교합니다. 책임을 나누는 과정에서 이 입력 계약을 완화하지 않습니다.