상태와 데이터 모델
모집 상태와 참가 관계를 나누어 모델링하고 종료 뒤 취소·불변식·데이터 보존을 검증합니다.
신청 결과가 REGISTERED였다고 해서 모임 자체의 상태도 ‘신청 완료’가 되는 것은 아닙니다. 한 모임에는 여러 참가자가 있고 모집은 계속될 수 있습니다. 다른 대상을 가리키는 상태를 섞으면 취소와 종료의 의미도 뒤엉킵니다. 이 단원은 무엇의 상태인가를 먼저 정하고, 저장해야 할 사실과 사건에 따른 변화를 연결합니다.
앞 본문에서 모임 신청의 정상·대안 흐름을 읽었다면 진행할 수 있습니다. 이번 목표는 모임의 모집 상태와 회원의 참가 관계를 구별하고, 종료 뒤 취소를 허용하는 규칙을 코드로 검증하는 것입니다.
상태는 다음 행동의 허용 여부를 설명한다
상태(state)는 특정 시점에 대상이 가진 정보 중 다음 행동을 판단하는 데 필요한 부분입니다. 사건(event)은 신청 요청, 취소 요청, 모집 종료처럼 변화를 촉발하는 일입니다. 전이(transition)는 사건과 조건이 맞을 때 이전 상태에서 다음 상태로 바뀌는 규칙입니다.
이 교재에서 모임의 status는 OPEN 또는 CLOSED입니다. OPEN은 신청을 받을 수 있는 모집 상태이고, CLOSED는 새 신청을 받지 않는 모집 상태입니다. 참가자 수가 정원에 도달했다고 자동으로 CLOSED로 바꾸지 않습니다. 정원이 찬 것과 운영자가 모집을 종료한 것은 서로 다른 사실입니다. 정원이 찬 OPEN 모임에서 누군가 취소하면 다시 신청할 수 있지만, CLOSED 모임은 자리가 생겨도 신청을 받지 않습니다.
신청·취소는 참가 관계를 바꿉니다. REGISTERED와 CANCELLED는 요청 처리 결과 코드이며 모임의 status 값이 아닙니다. 같은 문자열로 모든 상태를 표현하려고 하면 ‘모임은 종료되었지만 한 회원은 참가 중’이라는 정상 상황을 표현할 수 없게 됩니다.
모집 상태와 참가 관계는 따로 바뀐다
신규 신청을 받을 수 있다. 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · CH3-2
모집 상태와 참가 관계는 따로 바뀐다
운영자의 종료 요청은 OPEN을 CLOSED로 바꾸고 명단은 보존합니다.
모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 모집 중부터 종료 뒤 취소까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 모집 중 신규 신청을 받을 수 있다. |
|
|
| 모집 종료 운영자 요청으로 모집 상태만 바꾼다. |
|
|
| 종료 뒤 취소 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. |
|
|
모집 상태와 참가 관계는 따로 바뀐다
신규 신청을 받을 수 있다. 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · CH3-2
모집 상태와 참가 관계는 따로 바뀐다
운영자의 종료 요청은 OPEN을 CLOSED로 바꾸고 명단은 보존합니다.
모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 모집 중부터 종료 뒤 취소까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 모집 중 신규 신청을 받을 수 있다. |
|
|
| 모집 종료 운영자 요청으로 모집 상태만 바꾼다. |
|
|
| 종료 뒤 취소 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. |
|
|
상태 전이와 변화하지 않는 부분
| 이전 모집 상태 | 사건과 조건 | 이후 모집 상태 | 참가자 목록 |
|---|---|---|---|
| OPEN | 유효한 신규 신청·자리 있음 | OPEN | 한 ID 추가 |
| OPEN | 기존 참가자 취소 | OPEN | 한 ID 제거 |
| OPEN | 운영자가 종료 | CLOSED | 그대로 |
| CLOSED | 신규 신청 | CLOSED | 그대로·CLOSED 응답 |
| CLOSED | 기존 참가자 취소 | CLOSED | 한 ID 제거 |
종료 뒤 취소를 허용하는 것은 이 프로젝트에서 선택한 업무 규칙입니다. 취소가 모집을 다시 여는 것도 아니고 종료가 참가자를 모두 지우는 것도 아닙니다. ‘종료면 모든 기능 금지’라는 공통 조건을 취소 함수에도 붙이면 이 요구를 어깁니다.
상태 그림은 가능한 모든 필드를 그리지 않습니다. 어떤 필드를 생략했는지 적어야 합니다. 여기서는 두 모집 상태와 종료 사건을 그림에 두고, 참가 관계의 변화와 거절 결과는 표로 보완합니다. 신청 ID, 취소 시각, 대기자 등의 정보는 아직 모델에 없습니다. 필요가 생기면 저장할 사실과 허용 행동을 다시 정해야 합니다.
데이터 모델은 구별해야 할 사실을 담는다
엔티티(entity)는 구별해서 관리할 대상이며 속성(attribute)은 그 대상의 사실입니다. 모임은 id로 구별하고 title, capacity, status를 가집니다. 회원은 회원 ID로 구별합니다. 모임과 회원 사이에는 ‘참가한다’는 관계가 있습니다.
한 모임에는 0명 이상의 회원이 참가할 수 있고 한 회원은 여러 모임에 참가할 수 있습니다. 다중성(최소·최대 수)은 현재 데이터 개수가 아니라 허용 규칙입니다. 지금 참가자가 0명이라는 이유로 ‘모임은 회원을 가질 수 없다’는 관계를 그리지 않습니다. 정원이 모임마다 달라서 단순한 0..N 관계 표시만으로 정원 제한을 설명할 수도 없습니다. 정원 규칙은 별도 조건으로 남깁니다.
이 실습은 참가 관계를 participants: [memberId, ...] 배열로 저장합니다. 배열 위치를 회원의 정체성으로 쓰지 않습니다. 앞에 있는 회원의 참가를 취소하면 뒤 회원의 위치가 바뀌기 때문입니다. 정원과 현재 인원도 서로 다릅니다. 정원은 정책이고 현재 인원은 명단 길이에서 계산됩니다.
참가 시각·출석·취소 이력이 필요해지면 관계 자체에 식별자와 속성을 가진 신청 객체를 둘 수 있습니다. 처음부터 모든 미래 필드를 넣지는 않지만, ‘현재 명단만으로 과거 취소 시각을 알 수 없다’는 한계는 분명히 이해해야 합니다. 데이터베이스의 테이블과 키를 자세히 다루기 전에 여기서 어떤 사실을 보존할지 먼저 정합니다.
종료한 뒤 취소해 본다
저장소 최상위 폴더에서 다음 명령을 실행합니다. Node.js가 설치되어 있어야 하며 패키지를 추가로 설치하지 않습니다. 두 번째 인수를 생략하면 이 장의 예제를 모두 실행합니다.
node dev-note/scripts/software-engineering-lab/examples/ch3.mjs ch3-2function cancel(session, memberId) {
if (typeof memberId !== "string" || !/^[a-z][a-z0-9-]{0,23}$/.test(memberId)) {
return { ok: false, code: "INVALID_MEMBER", state: session };
}
if (!session.participants.includes(memberId)) {
return { ok: false, code: "NOT_REGISTERED", state: session };
}
return {
ok: true, code: "CANCELLED",
state: {
...session,
participants: session.participants.filter((id) => id !== memberId),
},
};
}
function closeSession(session) {
return { ...session, status: "CLOSED" };
}
const open = {
id: "book-02", title: "질문하는 독서",
capacity: 2, status: "OPEN", participants: ["member-a", "member-b"],
};
const closed = closeSession(open);
const cancelled = cancel(closed, "member-a");
console.log(closed.status, closed.participants.length);
console.log(cancelled.code, cancelled.state.status,
cancelled.state.participants.join(","));
console.log("종료 전 명단:", open.participants.join(","));첫 출력은 CLOSED 2이고 다음은 CANCELLED CLOSED member-b입니다. 마지막 줄의 종료 전 명단은 member-a,member-b로 남아 있습니다. 취소가 새 참가자 배열을 만들고, 종료가 모집 상태만 바꾸는지 각각 확인합니다. 종료 전에 사용하던 객체까지 바꾸는 구현이라면 마지막 출력이 달라집니다.
filter는 지정한 ID를 제외한 새 배열을 만듭니다. 이 함수는 명단에 같은 ID가 한 번만 존재한다는 앞 단원의 불변식을 전제로 합니다. 불변식(invariant)은 정상 상태에서 계속 성립해야 하는 규칙입니다. 중복 없는 명단, 정원을 넘지 않는 인원, 두 종류 중 하나인 모집 상태가 예입니다. 외부 파일을 읽을 때도 이 전제가 맞는지 검증해야 합니다.
취소에서도 신청과 같은 회원 ID 형식을 검사합니다. 소문자로 시작하는 1~24자이며 이후에는 소문자·숫자·하이픈만 허용합니다. 잘못된 ID는 명단을 찾기 전에 INVALID_MEMBER로 거절합니다. 목록에 없는 유효한 ID는 NOT_REGISTERED이므로 형식 오류와 참가 관계 없음은 다른 결과입니다.
객체 복사는 모든 깊이를 자동으로 복제하지 않습니다. 이 모델의 참가자는 문자열 배열이라 새 배열을 만드는 것으로 충분합니다. 참가자가 중첩 객체가 되면 내부 객체의 공유와 수정 여부까지 검사해야 합니다.
정적 구조와 동적 추적을 맞춘다
정적 모델은 어떤 대상·속성·관계가 있는지 보여 주고, 동적 모델은 실행 중 언제 바뀌는지 보여 줍니다. 시퀀스 그림은 참여자 사이의 호출 순서를, 통신 그림은 참여자 연결과 메시지를, 상태 그림은 한 대상의 사건별 변화를 읽는 데 유용합니다. 서로 답하는 질문이 다르므로 이름만 바꾸어 같은 그림을 재사용하지 않습니다.
이번 실행도 다음 중간 상태로 추적할 수 있습니다.
| 시점 | 모집 상태 | 참가 관계 | 바뀐 사실 |
|---|---|---|---|
| 시작 | OPEN | a·b 참가 | 아직 처리 없음 |
| 종료 뒤 | CLOSED | a·b 참가 | 모집 상태만 변경 |
| a 취소 뒤 | CLOSED | b 참가 | a와의 참가 관계 제거 |
이 표에서 마지막 상태를 ‘CANCELLED 모임’으로 적으면 요청 결과와 대상 상태를 섞은 것입니다. 이력 모델이 필요하다면 현재 관계와 별도로 ‘a가 어느 시각 취소했다’는 사건을 저장해야 합니다. 표에 a가 없어졌다는 사실만으로 취소인지 삭제 오류인지 알 수는 없습니다.
모델 사이의 불일치를 찾는 질문
문서에서 종료 뒤 취소를 허용한다고 쓰고 그림에서 CLOSED를 최종 상태라며 아무 행동도 못 한다고 읽게 만들면 충돌이 생깁니다. 그림의 종료 표시가 ‘모집 종료’인지 ‘모든 업무 종료’인지 이름을 구체적으로 붙입니다.
‘정원이 차면 닫힘’이라는 모델도 반례가 있습니다. 한 명이 취소한 뒤 자리가 생겨도 닫힘을 그대로 유지하면 재신청이 거절됩니다. 업무가 이를 원하지 않았다면 모집 상태와 인원 조건을 분리해야 합니다. 한 가지 상태값에 여러 사실을 압축할수록 조건 조합을 놓치기 쉽습니다.
독립 문제: 공동 작업 문서의 상태
공동 작업 문서는 DRAFT와 PUBLISHED 두 상태를 가집니다. 초안에서는 내용을 수정할 수 있습니다. 발행하면 내용 수정은 막지만 작성자의 열람 권한 회수는 허용합니다. 열람 권한을 가진 회원은 여러 명이며 현재 열람 중인지 여부는 이번 범위에 없습니다.
문서 상태와 권한 관계를 나누어 저장할 필드를 제안하고, ‘발행·내용 수정·권한 회수’ 사건의 전이 표를 만듭니다. 발행 뒤 마지막 열람 권한을 회수하면 문서가 자동으로 초안이 되어야 하는지도 판단합니다. 처음 문서와 처리 결과를 비교하는 검증 두 개를 작성합니다.
힌트
발행 상태는 내용 변경 가능성을, 권한 목록은 누가 읽을 수 있는지를 설명합니다. 목록이 비었다는 사실이 발행 취소 사건을 대신할 수 있는지 생각합니다.
풀이
문서에는 id, content, status, readerIds를 둡니다. 발행은 DRAFT → PUBLISHED이며 내용·권한은 유지합니다. 내용 수정은 DRAFT에서만 내용을 바꾸고, PUBLISHED에서는 거절 후 상태를 보존합니다. 권한 회수는 두 상태에서 해당 ID를 제거하되 문서 상태는 유지합니다.
마지막 권한이 사라져도 PUBLISHED를 유지합니다. 요구에 없는 자동 발행 취소를 만들면 안 됩니다. 검증은 발행 뒤 수정 요청이 내용·상태를 바꾸지 않는지, 발행 뒤 권한 회수가 해당 ID만 제거하고 PUBLISHED를 유지하는지입니다. 오래된 원본 객체가 바뀌지 않는지도 추가로 확인할 수 있습니다.
복습과 다음 연결
유스케이스가 목표를 이룰 경로를 설명했다면 상태 모델은 그 경로가 어느 초기 상태에서 허용되는지 보여 줍니다. 데이터 모델은 경로가 끝난 뒤 남길 사실을 정합니다. 다음 본문에서는 이 상태·결과를 사용자에게 어떻게 보여 주어 재요청과 오류 복구를 돕는지 화면 설계로 연결합니다.
원자료와 더 읽기
장원사이버평생교육원 「소프트웨어공학」 PDF 물리 110–123·197–200쪽의 주제를 바탕으로 설명·도표·실습을 새로 구성했습니다.
OMG UML 2.5.1 공식 명세는 상태·구조 모델을 여러 관점에서 표현하는 표준입니다.