본문 바로가기

상태와 데이터 모델

모임의 모집 상태와 회원의 참가 관계를 나누어 상태, 사건, 전이로 정리하고, 엔티티와 속성, 다중성으로 저장할 사실을 정합니다. 마감 뒤의 취소와 저장 자료의 불변 조건 검사를 core.mjs로 실행해 확인합니다.

신청 결과가 REGISTERED였다고 해서 모임의 상태까지 "신청 완료"가 되는 것은 아닙니다. 한 모임에는 여러 참가자가 있고, 누군가 등록된 뒤에도 모집은 계속될 수 있습니다. 서로 다른 대상의 상태를 값 하나에 섞어 담으면 취소와 마감의 뜻까지 뒤엉킵니다. 이 글에서는 무엇의 상태인지부터 정한 뒤, 사건에 따라 상태가 바뀌는 규칙을 상태 모델로, 저장해야 할 사실을 데이터 모델로 정리합니다.

상태는 다음에 할 수 있는 일을 정한다

상태(state)는 어느 시점에 대상이 가진 정보 가운데 다음 행동을 받아들일지 판단하는 데 필요한 부분입니다. 신청 요청, 취소 요청, 모집 마감처럼 변화를 일으키는 일을 사건(event)이라 하고, 사건이 일어나고 조건이 맞을 때 한 상태에서 다른 상태로 옮겨 가는 규칙을 전이(transition)라고 합니다.

이 교재에서 모임의 status는 OPEN과 CLOSED 둘 중 하나입니다. OPEN은 새 신청을 받는 모집 상태이고, CLOSED는 새 신청을 받지 않는 모집 상태입니다. 참가자 수가 정원에 이르렀다고 해서 CLOSED로 바꾸지는 않는데, 정원이 찬 것과 진행자가 모집을 마감한 것은 서로 다른 사실이기 때문입니다. 정원이 찬 OPEN 모임에서 누군가 취소하면 다른 사람이 다시 신청할 수 있지만, CLOSED 모임은 자리가 생겨도 새 신청을 받지 않습니다.

신청과 취소는 모집 상태가 아니라 모임과 회원 사이의 참가 관계를 바꿉니다. REGISTERED와 CANCELLED는 요청을 처리한 결과 코드일 뿐 모임의 status 값이 아닙니다. 모든 것을 상태 문자열 하나로 나타내려 하면 "모집은 마감되었지만 한 회원은 여전히 참가 중"이라는 평범한 상황조차 나타낼 수 없습니다.

모집 상태와 참가 관계는 따로 바뀐다

신규 신청을 받을 수 있다. 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · CH3-2

모집 상태와 참가 관계는 따로 바뀐다

운영자의 종료 요청은 OPEN을 CLOSED로 바꾸고 명단은 보존합니다.

모집 상태와 참가 관계는 따로 바뀐다모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.종료 요청OPEN새 신청 가능CLOSED새 신청 거절기존 참가자는 취소 가능취소는 모집 상태를 바꾸지 않는다

모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 모집 중부터 종료 뒤 취소까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
모집 중

신규 신청을 받을 수 있다.

  • 모집·참가 1: status OPEN
  • 모집·참가 2: 참가자 명단 보유
  • 운영자 종료 요청 대기
모집 종료

운영자 요청으로 모집 상태만 바꾼다.

  • 모집·참가 1: status CLOSED
  • 모집·참가 2: 명단 유지
  • OPEN→CLOSED
종료 뒤 취소

기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다.

  • 모집·참가 1: status CLOSED
  • 모집·참가 2: 취소 ID만 제거
  • 참가 관계 변경; 모집 상태 유지

모집 상태와 참가 관계는 따로 바뀐다

신규 신청을 받을 수 있다. 기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · CH3-2

모집 상태와 참가 관계는 따로 바뀐다

운영자의 종료 요청은 OPEN을 CLOSED로 바꾸고 명단은 보존합니다.

모집 상태와 참가 관계는 따로 바뀐다모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.종료 요청OPEN새 신청 가능CLOSED새 신청 거절기존 참가자는 취소 가능취소는 모집 상태를 바꾸지 않는다

모집 상태는 OPEN에서 종료 요청으로 CLOSED가 되며 기존 참가자 취소는 두 상태 모두에서 가능하고 모집 상태를 바꾸지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 모집 중부터 종료 뒤 취소까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
모집 중

신규 신청을 받을 수 있다.

  • 모집·참가 1: status OPEN
  • 모집·참가 2: 참가자 명단 보유
  • 운영자 종료 요청 대기
모집 종료

운영자 요청으로 모집 상태만 바꾼다.

  • 모집·참가 1: status CLOSED
  • 모집·참가 2: 명단 유지
  • OPEN→CLOSED
종료 뒤 취소

기존 참가자 한 명을 제거해도 모집은 다시 열리지 않는다.

  • 모집·참가 1: status CLOSED
  • 모집·참가 2: 취소 ID만 제거
  • 참가 관계 변경; 모집 상태 유지

상태 전이 표

그림의 흐름을 사건별로 정리하면 다음 표가 됩니다. 그림이 "운영자 종료 요청"과 "모집 종료"로 적은 사건은 표의 "진행자의 마감"과 같습니다. 이 교재에서 모집을 마감하는 사람은 진행자이며, 문제가 생긴 뒤 데이터를 되살리는 운영자와는 다른 이해관계자입니다. 각 행은 어떤 상태에서 어떤 사건이 일어나면 모집 상태와 명단이 어떻게 되는지를 나타냅니다.

이전 모집 상태사건과 조건이후 모집 상태참가자 명단
OPEN유효한 새 아이디의 신청, 자리 있음OPEN아이디 하나 추가
OPEN신청한 참가자의 취소OPEN아이디 하나 제거
OPEN진행자의 마감CLOSED그대로
CLOSED새 신청CLOSED그대로, CLOSED로 거절
CLOSED신청한 참가자의 취소CLOSED아이디 하나 제거

마감 뒤에도 취소를 받는 것은 이 프로젝트가 사용자·문제·범위의 연습 문제에서 고르고 기능 요구와 수락 조건의 R6으로 적은 업무 규칙입니다. 취소가 모집을 다시 여는 것도 아니고, 마감이 참가자를 모두 지우는 것도 아닙니다. "마감되면 모든 기능을 막는다"는 조건을 취소에도 붙이면 이 요구를 어기게 됩니다.

상태 그림은 대상의 모든 정보를 그리지 않으므로, 무엇을 생략했는지 적어 두어야 합니다. 이 그림은 두 모집 상태와 마감 사건을 중심에 두고, 참가 관계의 변화와 거절 결과는 표로 보충합니다. 신청 시각, 취소 시각, 대기자 같은 정보는 아직 모델에 없으며, 필요해지면 저장할 사실과 받아들일 행동을 다시 정해야 합니다.

데이터 모델은 구별해야 할 사실을 담는다

데이터 모델에서는 따로 구별해 관리할 대상을 엔티티(entity), 그 대상에 대한 사실을 속성(attribute)이라고 합니다. 모임은 id로 구별하고 title, capacity, status를 속성으로 가지며, 회원은 회원 아이디로 구별합니다. 그리고 모임과 회원 사이에는 "참가한다"는 관계(relationship)가 있습니다. 엔티티와 속성을 찾아 나누는 방법은 엔티티·속성·식별자에서 더 자세히 다룹니다.

한 모임에는 0명 이상의 회원이 참가할 수 있고, 한 회원도 여러 모임에 참가할 수 있습니다. 관계의 양쪽에 몇 개까지 연결될 수 있는지를 나타내는 다중성(multiplicity)은 지금 들어 있는 데이터의 개수가 아니라 허용하는 범위입니다. 그래서 지금 참가자가 0명이라고 해서 "모임은 회원을 가질 수 없다"고 그리지 않습니다. 또 정원은 모임마다 다르므로 "0개 이상" 같은 다중성 표기만으로는 정원 제한을 나타낼 수 없고, 이 규칙은 별도의 조건으로 적습니다. 데이터베이스 설계에서 관계의 최대 수와 최소 수를 카디널리티와 참여도로 나누어 읽는 방법은 관계와 카디널리티에서 다룹니다.

이 실습은 참가 관계를 participants라는 아이디 배열로 저장합니다. 이때 배열 안의 위치를 회원을 구별하는 값으로 쓰지 않는데, 앞쪽 회원이 취소하면 뒤쪽 회원의 위치가 바뀌기 때문입니다. 정원과 현재 인원도 다른 값입니다. 정원은 진행자가 정한 정책이고, 현재 인원은 명단의 길이에서 계산되므로 따로 저장하지 않습니다.

참가 시각, 출석, 취소 이력이 필요해지면 참가 관계 자체를 식별자와 속성을 가진 "신청" 엔티티로 만들 수 있습니다. 앞으로 필요할지 모르는 필드를 처음부터 모두 넣을 필요는 없지만, 지금의 명단만으로는 누가 언제 취소했는지 알 수 없다는 한계는 알고 있어야 합니다.

마감한 뒤 취소해 본다

전이 표의 마지막 두 행을 기능 요구와 수락 조건의 core.mjs로 확인해 봅니다. 다음 코드를 reading-lab/examples/ch3-2.mjs로 저장하고 node examples/ch3-2.mjs로 실행합니다.

import { register, cancel, closeSession, restoreSession } from "../core.mjs";

const open = restoreSession({
  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("마감 뒤 새 신청:", register(cancelled.state, "member-c").code);
console.log("마감 전 명단:", open.participants.join(","));
CLOSED 2
CANCELLED CLOSED member-b
마감 뒤 새 신청: CLOSED
마감 전 명단: member-a,member-b

restoreSession은 두 명이 신청한 열린 모임을 저장 자료에서 되살린 것처럼 만들어 줍니다. 마감은 모집 상태만 바꾸어 명단의 두 명이 그대로 남고, 마감 뒤의 취소는 member-a만 빼면서 상태는 CLOSED로 둡니다. 취소로 자리가 하나 생겼지만 member-c의 새 신청은 여전히 CLOSED로 거절됩니다. 마지막 줄은 마감과 취소가 처음 객체를 고치지 않고 새 상태를 만들었다는 것을 보여 주며, 처음 객체까지 고치는 구현이었다면 이 줄이 달라졌을 것입니다.

불변 조건은 저장 자료에서도 확인한다

기능 요구와 수락 조건에서 정한 불변 조건(invariant)은 모든 유효한 상태에서 늘 참이어야 하는 조건입니다. 그 글에서 정한 두 조건은 명단에 같은 아이디가 두 번 들어가지 않는다는 것과 참가자 수가 정원을 넘지 않는다는 것이고, 이 글에서는 상태 모델에 따라 모집 상태가 OPEN과 CLOSED 가운데 하나라는 조건을 더합니다. cancel의 filter가 아이디를 한 번만 지우면 충분한 것도 명단에 같은 아이디가 한 번만 있다는 불변 조건 덕분입니다.

register와 cancel은 이 조건을 깨지 않도록 만들었지만, 파일에서 읽어 온 자료는 누가 어떻게 고쳤는지 알 수 없습니다. 그래서 restoreSession은 저장 자료를 받아들이기 전에 불변 조건을 검사합니다. 다음 코드를 reading-lab/examples/ch3-2-restore.mjs로 저장해 실행합니다.

import { restoreSession } from "../core.mjs";

const stored = [
  { status: "OPEN", participants: ["member-a", "member-a"] },
  { status: "OPEN", participants: ["member-a", "member-b", "member-c"] },
  { status: "FULL", participants: ["member-a"] },
  { status: "CLOSED", participants: ["member-a"] },
];
for (const { status, participants } of stored) {
  try {
    const session = restoreSession({ id: "book-03", title: "다시 읽기", capacity: 2, status, participants });
    console.log(status, participants.length, "복원됨", session.participants.join(","));
  } catch (error) {
    console.log(status, participants.length, "거절:", error.message);
  }
}
OPEN 2 거절: stored participants violate the session rules
OPEN 3 거절: stored participants violate the session rules
FULL 1 거절: invalid stored session
CLOSED 1 복원됨 member-a

정원이 2인 모임에서 중복 아이디가 든 명단과 세 명이 든 명단은 불변 조건을 어기므로 거절되고, 모집 상태에 FULL처럼 정하지 않은 값이 들어 있는 자료도 거절됩니다. 반면 마감되었지만 한 명이 남아 있는 자료는 앞에서 본 평범한 상태이므로 그대로 복원됩니다. 이 함수가 상태를 복사할 때 쓰는 펼침 구문은 한 겹만 복사하는데, 지금의 참가자는 문자열이라 새 배열을 만드는 것으로 충분합니다. 참가자가 객체로 바뀌면 안쪽 객체를 여러 상태가 함께 쓰는지까지 살펴야 합니다.

정적 모델과 동적 모델을 맞춘다

어떤 대상과 속성과 관계가 있는지 보여 주는 모델을 정적 모델(static model)이라 하고, 실행하는 동안 무엇이 언제 바뀌는지 보여 주는 모델을 동적 모델(dynamic model)이라고 합니다. UML의 시퀀스 다이어그램은 참여자 사이의 호출 순서를, 커뮤니케이션 다이어그램은 참여자의 연결과 주고받는 메시지를, 상태 머신 다이어그램은 한 대상이 사건에 따라 바뀌는 모습을 보여 줍니다. 그림마다 답하는 질문이 다르므로, 이름만 바꾸어 같은 그림을 다시 쓰지 않습니다.

앞의 실행을 시점별로 따라가면 다음과 같습니다.

시점모집 상태참가 관계바뀐 사실
시작OPENa, b 참가아직 처리한 사건 없음
마감 뒤CLOSEDa, b 참가모집 상태만 바뀜
a의 취소 뒤CLOSEDb 참가a의 참가 관계가 없어짐

이 표의 마지막 상태를 "CANCELLED인 모임"이라고 적으면 요청의 결과와 대상의 상태를 섞은 것입니다. 또 표에서 a가 사라졌다는 사실만으로는 a가 취소한 것인지 잘못 지워진 것인지 구별할 수 없습니다. 이력이 필요하다면 지금의 참가 관계와 별도로 "a가 언제 취소했다"는 사건을 저장해야 합니다.

모델 사이의 어긋남을 찾는 질문

문서에는 마감 뒤에도 취소를 받는다고 적었는데, 상태 그림에서 CLOSED를 아무 행동도 할 수 없는 최종 상태처럼 그리면 두 모델이 서로 어긋납니다. 그림에 마감을 표시할 때는 "모집 마감"인지 "모든 업무의 종료"인지 이름을 구체적으로 붙여야 합니다.

"정원이 차면 자동으로 마감한다"는 모델에도 반례가 있습니다. 한 명이 취소해 자리가 생겨도 마감 상태가 그대로 남으면 다른 사람의 신청이 거절됩니다. 진행자가 이를 원하지 않았다면 모집 상태와 인원 조건을 나누어 두어야 합니다. 상태값 하나에 여러 사실을 담을수록 조건의 조합을 놓치기 쉬워집니다.

정리

  • 상태는 다음 행동을 받아들일지 판단하는 정보이고, 사건은 변화를 일으키는 일이며, 전이는 사건과 조건에 따라 상태가 바뀌는 규칙입니다.
  • 모임의 모집 상태와 회원의 참가 관계는 서로 다른 대상의 정보이며, 결과 코드는 어느 쪽의 상태도 아닙니다.
  • 데이터 모델은 엔티티, 속성, 관계로 저장할 사실을 정하고, 다중성은 지금의 개수가 아니라 허용하는 범위를 나타냅니다.
  • 불변 조건은 코드가 만든 상태뿐 아니라 저장해 두었다가 읽어 온 자료에서도 검사해야 합니다.
  • 정적 모델과 동적 모델은 답하는 질문이 다르며, 문서와 그림이 같은 규칙을 말하는지 서로 맞추어 보아야 합니다.

연습 문제

1. 공동 작업 문서의 상태

공동 작업 문서는 DRAFT와 PUBLISHED 두 상태를 가집니다. 초안일 때는 내용을 고칠 수 있고, 발행한 뒤에는 내용을 고칠 수 없지만 작성자가 다른 회원의 열람 권한을 회수할 수는 있습니다. 열람 권한을 가진 회원은 여러 명입니다. 저장할 필드를 제안하고, 발행, 내용 수정, 권한 회수 사건의 전이 표를 만드세요.

해설 보기

문서에는 id, content, status, 열람할 수 있는 회원의 아이디 목록인 readerIds를 둡니다. 발행은 DRAFT에서 PUBLISHED로 옮기며 내용과 권한은 그대로 둡니다. 내용 수정은 DRAFT에서만 내용을 바꾸고, PUBLISHED에서는 거절하며 상태와 내용을 그대로 둡니다. 권한 회수는 두 상태 모두에서 해당 아이디만 목록에서 빼고 문서의 상태는 바꾸지 않습니다. 모집 상태와 참가 관계를 나눈 것처럼, 문서의 상태와 권한 관계를 서로 다른 필드에 둔 것입니다.

2. 마지막 권한을 회수하면

1번의 문서에서 발행한 뒤 마지막 남은 열람 권한까지 회수하면, 문서가 자동으로 초안으로 돌아가야 할까요? 이 판단을 확인할 검사 두 가지를 적으세요.

해설 보기

문서는 PUBLISHED로 남아야 합니다. 권한 목록이 비었다는 것은 지금 읽을 수 있는 사람이 없다는 사실일 뿐, 발행을 취소하는 사건이 아니므로 요구에 없는 자동 전이를 만들면 안 됩니다. 검사로는 발행 뒤의 수정 요청이 내용과 상태를 바꾸지 않는지, 발행 뒤의 권한 회수가 해당 아이디만 빼고 PUBLISHED를 유지하는지를 확인합니다. 여기에 처음 문서 객체가 바뀌지 않았는지도 함께 확인할 수 있습니다.

3. 정원을 줄이는 요청

이미 세 명이 신청한 정원 3의 모임에서 진행자가 정원을 2로 줄여 달라고 요청했습니다. 그대로 정원 값만 바꾸어 저장하면 어떤 일이 생기나요? 이 요청을 어떻게 다루어야 할지 설명하세요.

해설 보기

정원만 2로 바꾸면 참가자 수가 정원을 넘지 않는다는 불변 조건이 깨집니다. 이렇게 저장한 자료는 restoreSession이 stored participants violate the session rules로 거절하므로, 다음에 프로그램을 시작할 때 모임을 되살리지 못합니다. 따라서 이 요청은 코드에서 조용히 받아 줄 것이 아니라 요구로 다시 정해야 합니다. 인원보다 작은 정원으로는 바꾸지 못하게 거절할 수도 있고, 기존 참가자는 두고 새 신청만 막는 정책을 고를 수도 있지만, 뒤의 경우에는 불변 조건 자체를 다시 적고 복원 검사도 함께 고쳐야 합니다.

유스케이스가 목표를 이루는 경로를 보여 준다면, 상태 모델은 그 경로가 어떤 상태에서 허용되는지를, 데이터 모델은 경로가 끝난 뒤 남길 사실을 정합니다. 다음 글 화면 설계와 피드백에서는 이 상태와 결과를 사용자에게 어떻게 보여 주어야 다시 요청하거나 오류에서 벗어나기 쉬운지를 다룹니다.


원자료와 더 읽기

장원사이버평생교육원 「소프트웨어공학」 PDF 물리 110–123·197–200쪽의 주제를 바탕으로 설명, 도표, 실습을 새로 구성했습니다. 상태와 구조를 여러 관점에서 나타내는 표기는 OMG UML 2.5.1 공식 명세에서 확인할 수 있습니다.