본문 바로가기

기능 요구와 수락 조건

신청 정책을 우선순위가 있는 결과 코드 표와 불변 조건으로 정하고, Given–When–Then 형식의 수락 조건으로 바꿉니다. 이 규칙을 담은 core.mjs 전체 코드를 함수별로 읽고, 수락 사례를 단언으로 실행해 확인합니다.

"신청을 잘 처리한다"는 말만으로는, 정원이 찬 모임에 이미 신청한 사람이 다시 신청할 때 "자리가 없습니다"라고 답해야 하는지 "이미 신청하셨습니다"라고 답해야 하는지 알 수 없습니다. 이런 결과를 코드를 짜는 사람이 그때그때 정하면, 같은 상황에서 화면과 테스트와 안내문이 서로 다른 답을 내놓게 됩니다. 이 글에서는 사용자·문제·범위에서 정한 정책을 결과 코드와 수락 조건으로 바꾸고, 그 규칙을 지키는 core.mjs를 만들어 실행으로 확인합니다. 이 파일은 이후 모든 장의 예제가 가져다 쓰는 실습의 중심입니다.

요구를 판단할 수 있는 조건으로 바꾼다

시스템이 해야 할 동작을 기능 요구(functional requirement)라 하고, 그 동작이 요구를 만족했는지 판단할 구체적인 조건을 수락 조건(acceptance criteria)이라고 합니다. "중복 신청을 막는다"가 기능 요구라면, "이미 신청한 아이디로 다시 신청하면 ALREADY_REGISTERED를 돌려주고 명단은 바뀌지 않는다"는 그 요구의 수락 조건입니다. 수락 조건이 있어야 요청한 사람과 만드는 사람이 같은 기준으로 완성 여부를 판단할 수 있습니다.

만든 것을 확인하는 일도 두 가지로 나뉩니다. 검증(verification)은 산출물이 정한 규칙대로 만들어졌는지, 다시 말해 제품을 올바르게 만들었는지 따지는 일입니다. 확인(validation)은 그 규칙이 사용자의 실제 문제를 해결하는지, 다시 말해 올바른 제품을 만들었는지 따지는 일입니다. 정원 검사가 표대로 정확히 동작하더라도 진행자에게 정말 필요했던 것이 대기 명단이었다면, 검증은 통과했어도 확인 단계에서 요구부터 다시 정해야 합니다.

결과 코드와 우선순위를 표로 정한다

먼저 아이디의 형식을 정합니다. 아이디는 영문 소문자로 시작하고 그 뒤에 소문자, 숫자, 하이픈(-)이 올 수 있는, 1자 이상 24자 이하의 문자열입니다. 이 형식은 명단에서 참가자를 구별하는 규칙일 뿐, 이름을 대신하거나 본인임을 확인하는 정보가 아닙니다. 또 모든 유효한 상태에서 늘 참이어야 하는 조건을 불변 조건(invariant)이라고 하는데, 이 모임의 불변 조건은 명단에 같은 아이디가 두 번 들어가지 않고 참가자 수가 정원을 넘지 않는다는 것입니다.

신청과 취소의 결과는 다음 표처럼 정합니다. 요구마다 R1부터 R6까지 번호를 붙여 두면, 테스트와 변경 기록에서 그 번호로 요구를 가리킬 수 있습니다.

요구조건결과 코드명단과 상태
R1아이디의 형식이 잘못됨INVALID_MEMBER그대로
R2형식은 맞지만 모임이 마감됨CLOSED그대로
R3열린 모임에 이미 신청한 아이디ALREADY_REGISTERED그대로
R4열린 모임, 새 아이디, 정원이 찼음FULL그대로
R5열린 모임, 새 아이디, 자리가 있음REGISTERED새 아이디 한 명 추가
R6신청한 참가자가 취소CANCELLED그 아이디만 빠지고 마감 여부는 그대로

신청을 처리할 때는 R1부터 차례로 조건을 보고, 처음 만족한 행의 결과를 씁니다. 그래서 마감된 모임에 이미 신청한 사람이 다시 신청하면 R3보다 앞선 R2에 걸려 CLOSED를 받고, 정원이 찬 열린 모임에 이미 신청한 사람이 다시 신청하면 R4보다 앞선 R3에 걸려 ALREADY_REGISTERED를 받습니다. 조건이 겹칠 때의 순서를 표에 적어 두었기 때문에, 구현하는 사람과 테스트를 쓰는 사람이 같은 결정을 따를 수 있습니다. 표에 없는 취소의 거절, 즉 형식이 틀린 아이디나 명단에 없는 아이디의 취소는 아래 코드에서 각각 INVALID_MEMBER와 NOT_REGISTERED로 처리합니다.

Given–When–Then으로 수락 조건을 쓴다

수락 조건은 Given–When–Then 형식으로 적으면 읽기 쉽습니다. Given에는 전제가 되는 상태를, When에는 사용자의 행동을, Then에는 기대하는 결과를 적습니다. R3을 이 형식으로 적으면 다음과 같습니다.

Given: 열린 모임의 정원은 2이고 min과 hana가 신청해 있다.
When: min이 다시 신청한다.
Then: ALREADY_REGISTERED를 돌려주고 명단은 min, hana 그대로다.

Given을 빼고 min의 신청만 시험하면 첫 신청인지 다시 하는 신청인지 구별할 수 없습니다. 또 정원이 찬 상태를 전제로 두었기 때문에, 이 사례 하나로 R3이 R4보다 먼저라는 순서까지 확인할 수 있습니다. Then에 결과 코드만 적으면 거절하면서 명단을 바꾸는 잘못을 놓치므로, 명단이 그대로라는 조건도 함께 적습니다.

SOFTWARE ENGINEERING · READING CLUB

업무 요구를 수락 사례와 자동 단언으로 잇는다

실행 결과에서 정답을 복사하지 않고 요구 R3에서 전제와 기대 값을 도출합니다.

업무 요구를 수락 사례와 자동 단언으로 잇는다업무 요구 R3: OPEN에서 기존 ID 신청, 만석이어도 중복으로 안내 → Given · When · Then: 정원 2 / min, hana, min 신청 → 중복, 상태 유지 → 실행 단언: code = 중복, 참가자 목록·객체 유지업무 요구 R3OPEN에서 기존 ID 신청만석이어도 중복으로 안내구체화Given · When · Then정원 2 / min, hanamin 신청 → 중복, 상태 유지검증실행 단언code = 중복참가자 목록·객체 유지
업무 요구를 수락 사례와 자동 단언으로 잇는다업무 요구 R3: OPEN에서 기존 ID 신청, 만석이어도 중복으로 안내 → Given · When · Then: 정원 2 / min, hana, min 신청 → 중복, 상태 유지 → 실행 단언: code = 중복, 참가자 목록·객체 유지
업무 요구 R3OPEN에서 기존 ID 신청만석이어도 중복으로 안내
구체화구체화업무 요구 R3 · OPEN에서 기존 ID 신청 · 만석이어도 중복으로 안내 → Given · When · Then · 정원 2 / min, hana · min 신청 → 중복, 상태 유지
Given · When · Then정원 2 / min, hanamin 신청 → 중복, 상태 유지
검증검증Given · When · Then · 정원 2 / min, hana · min 신청 → 중복, 상태 유지 → 실행 단언 · code = 중복 · 참가자 목록·객체 유지
실행 단언code = 중복참가자 목록·객체 유지

업무 요구를 변경하면 수락 사례·안내·구현의 연결을 따라 함께 갱신합니다.

그림처럼 요구 R3은 Given–When–Then 사례로 구체화되고, 그 사례는 다시 코드가 실행 중에 확인하는 단언(assertion)이 됩니다. 이때 기대 값은 프로그램을 실행해 나온 결과를 옮겨 적는 것이 아니라 요구에서 끌어내야 합니다. 실행 결과를 정답으로 삼으면 코드에 들어 있는 잘못까지 정답이 되기 때문입니다.

더 깊이: Given–When–Then은 어디서 왔나

이 형식은 행동 주도 개발(behaviour-driven development, BDD)을 처음 소개한 Dan North의 글 "Introducing BDD"에서 널리 알려졌습니다. 2006년 3월 Better Software 잡지에 실린 이 글에서 North는 테스트 주도 개발(TDD)을 가르치면서 프로그래머들이 무엇부터 테스트하고, 테스트의 이름을 어떻게 짓고, 실패를 어떻게 읽어야 할지 헷갈려 하는 것을 보고, 테스트를 시스템의 행동을 묘사하는 문장으로 쓰자고 제안합니다. 그 흐름에서 그는 이야기의 행동은 곧 그 수락 조건이라고 보고, 분석가에게 부자연스럽지 않으면서도 자동화할 수 있게 쪼갤 수 있는 틀로 Given–When–Then을 내놓았습니다.

글의 예는 현금 인출기입니다. "잔액이 있는 계좌"와 "한도를 넘어 마이너스인 계좌"처럼 같은 사건(When)에 전제(Given)만 다른 시나리오를 나란히 적고, 여러 전제나 결과는 And로 잇습니다. North는 이렇게 적은 전제와 사건, 결과의 조각을 코드와 짝지어 다시 쓸 수 있어야 한다고, 곧 수락 조건이 실행할 수 있어야 한다고 강조했습니다. 이 글의 R3 사례가 전제를 바꾼 사례를 함께 두고, 그것을 단언으로 옮겨 실행하는 것도 같은 생각입니다. Cucumber의 Gherkin은 Given, When, Then, And, But을 예약어로 두어 이런 시나리오를 실행할 수 있는 명세로 적게 하는 문법입니다.

신청 규칙을 담은 core.mjs

소프트웨어와 개발 산출물에서 만든 reading-lab 폴더에 core.mjs를 만들고, 아래 전체 코드를 저장합니다. 사용자·문제·범위의 createSession에 신청, 취소, 마감, 그리고 저장해 둔 상태를 되살리는 함수를 더한 것입니다. 처음 읽을 때는 createSession과 register를 먼저 보고, 저장 자료를 검사하는 restoreSession은 마지막에 읽어도 됩니다.

core.mjs 전체 코드
// This lab deliberately models one process and in-memory state.
// It is not a production authentication or concurrent storage service.
function validMember(value) {
  return typeof value === "string" && /^[a-z][a-z0-9-]{0,23}$/.test(value);
}

function state(value) {
  return Object.freeze({ ...value, participants: Object.freeze([...value.participants]) });
}

export function createSession({ id, title, capacity }) {
  if (typeof id !== "string" || !id.trim() || typeof title !== "string" || !title.trim()) {
    throw new TypeError("id and title must be non-empty strings");
  }
  if (!Number.isInteger(capacity) || capacity < 1 || capacity > 100) {
    throw new RangeError("capacity must be an integer from 1 to 100");
  }
  return state({ id, title, capacity, status: "OPEN", participants: [] });
}

export function register(session, memberId) {
  let code;
  if (!validMember(memberId)) code = "INVALID_MEMBER";
  else if (session.status !== "OPEN") code = "CLOSED";
  else if (session.participants.includes(memberId)) code = "ALREADY_REGISTERED";
  else if (session.participants.length >= session.capacity) code = "FULL";
  if (code) return { ok: false, code, state: session };
  return {
    ok: true,
    code: "REGISTERED",
    state: state({ ...session, participants: [...session.participants, memberId] }),
  };
}

export function cancel(session, memberId) {
  if (!validMember(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: state({ ...session, participants: session.participants.filter((id) => id !== memberId) }),
  };
}

export function closeSession(session) {
  return state({ ...session, status: "CLOSED" });
}

export function restoreSession(raw) {
  const initial = createSession(raw);
  if (!["OPEN", "CLOSED"].includes(raw.status) || !Array.isArray(raw.participants)) {
    throw new TypeError("invalid stored session");
  }
  if (raw.participants.some((id) => !validMember(id)) ||
      new Set(raw.participants).size !== raw.participants.length ||
      raw.participants.length > raw.capacity) {
    throw new TypeError("stored participants violate the session rules");
  }
  return state({ ...initial, status: raw.status, participants: raw.participants });
}

맨 위의 두 주석은 이 코드가 프로세스 하나의 메모리 안에서만 동작하는 실습용이며, 실제 서비스의 인증이나 동시 저장을 다루지 않는다는 점을 밝혀 둡니다. export가 붙은 함수는 다른 파일에서 import로 가져다 쓸 수 있고, export가 없는 validMember와 state는 이 파일 안에서만 쓰는 보조 함수입니다.

아이디 검사와 상태 고정

validMember는 값이 문자열인지 typeof로 먼저 확인하고, 문자열일 때만 정규식(regular expression)으로 형식을 검사합니다. &&는 앞의 조건이 거짓이면 뒤의 조건을 계산하지 않으므로, 숫자 12 같은 값은 정규식 검사까지 가지 않고 거절됩니다. 정규식 /^[a-z][a-z0-9-]{0,23}$/에서 ^와 $는 문자열의 처음과 끝을, [a-z]는 소문자 한 글자를, [a-z0-9-]{0,23}은 소문자, 숫자, 하이픈이 0개부터 23개까지 이어진다는 뜻입니다. 그래서 a-1은 받아들이지만 1a, A, 빈 문자열, 25자짜리 아이디, 끝에 공백이 붙은 "min "은 거절합니다.

state는 넘겨받은 상태를 복사해 고정하는 보조 함수입니다. { ...value }는 객체의 속성을 새 객체에 펼쳐 담는 펼침 구문(spread syntax)이고, [...value.participants]는 배열의 원소를 새 배열에 펼쳐 담으며, 이 문법은 구조 분해 할당과 스프레드 연산자에서 자세히 다룹니다. Object.freeze는 객체에 속성을 더하거나 바꾸지 못하게 고정하는데, 안쪽에 든 객체까지 고정하지는 않으므로 상태 객체와 참가자 배열을 따로 고정했습니다. 이 상태의 나머지 값은 문자열과 숫자뿐이라 두 겹의 고정으로 충분하고, 고정된 배열에 push하면 엄격 모드(strict mode)인지와 상관없이 TypeError가 납니다. session.status = "CLOSED"처럼 고정된 객체의 속성에 직접 대입하는 것은 엄격 모드가 아니면 오류 없이 무시되지만, 모듈 파일은 늘 엄격 모드로 실행되므로 이 실습에서는 이때도 TypeError가 납니다.

신청과 취소의 처리 순서

register는 let code로 거절 이유를 담을 변수를 만들고, if와 else if를 R1부터 R4의 순서대로 이어 둡니다. 앞의 조건이 참이면 뒤의 조건은 보지 않으므로, 코드에 적힌 순서가 곧 요구 표의 우선순위입니다. 거절 이유가 정해지면 { ok: false, code, state: session }처럼 넘겨받은 상태 객체를 그대로 돌려주고, 거절할 이유가 없으면 참가자 배열 끝에 새 아이디를 더한 새 상태를 만들어 REGISTERED와 함께 돌려줍니다. 여기서 code만 적은 속성은 code: code를 줄여 쓴 것입니다.

cancel은 아이디의 형식과 신청 여부를 확인한 뒤, filter로 취소할 아이디가 아닌 것만 남긴 새 배열을 만듭니다. (id) => id !== memberId는 아이디 하나를 받아 취소할 아이디와 다른지를 돌려주는 짧은 함수로, 화살표 함수(arrow function)라고 부릅니다. ===와 !==는 값의 종류를 바꾸지 않고 같은지, 다른지를 비교하므로 숫자 12와 문자열 "12"는 다른 값으로 봅니다. 상태의 다른 속성은 펼침 구문으로 그대로 복사하므로 마감된 모임은 취소한 뒤에도 마감 상태로 남고, closeSession은 상태만 CLOSED로 바꾼 새 상태를 돌려줍니다.

오류를 던질 때와 결과 코드를 돌려줄 때

createSession과 restoreSession은 잘못된 값을 받으면 throw로 오류를 던져 실행을 멈춥니다. 반면 register와 cancel은 거절할 때도 오류를 던지지 않고 결과 코드를 돌려줍니다. 중복 신청이나 만석은 사용자가 흔히 겪는 상황이라 화면에서 안내하면 되지만, 정원이 0인 모임이나 망가진 저장 자료는 그대로 계속 실행해서는 안 되는 잘못이기 때문입니다.

restoreSession은 파일 같은 곳에 저장해 둔 자료로 상태를 되살릴 때 씁니다. 먼저 createSession으로 아이디, 제목, 정원을 다시 검사하고, 상태가 OPEN이나 CLOSED인지, 참가자 목록이 배열인지 확인합니다. 이어서 some으로 형식이 틀린 아이디가 하나라도 있는지, 중복을 뺀 아이디의 수가 배열의 길이와 같은지, 참가자 수가 정원을 넘지 않는지 검사합니다. Set은 같은 값을 한 번만 담는 집합이라 new Set(["min", "min"]).size는 1이므로, 이 크기가 배열의 길이와 다르면 중복이 있다는 뜻입니다. register는 바깥에서 받은 자료를 바로 받지 않고, 이렇게 createSession이나 restoreSession을 거친 상태만 받는다고 전제합니다.

수락 사례를 실행해 확인한다

reading-lab/examples/ch2.mjs에 다음 코드를 저장하고, reading-lab 폴더에서 node examples/ch2.mjs로 실행합니다. 이 코드는 앞에서 만든 core.mjs의 함수를 불러와, cases 배열에 신청할 아이디와 기대하는 결과 코드를 짝지어 두고 실제 결과와 하나씩 비교합니다. 반복이 끝나면 모임을 마감하고, 마감 뒤의 새 신청과 취소가 어떤 결과를 내는지도 확인합니다.

import assert from "node:assert/strict";
import { createSession, register, cancel, closeSession } from "../core.mjs";
let session = createSession({ id: "reading-1", title: "주말 읽기 모임", capacity: 2 });
const cases = [
  ["min", "REGISTERED"], ["min", "ALREADY_REGISTERED"],
  ["hana", "REGISTERED"], ["jun", "FULL"], ["", "INVALID_MEMBER"],
];
for (const [id, expected] of cases) {
  const before = session;
  const result = register(session, id);
  assert.equal(result.code, expected);
  if (!result.ok) assert.equal(result.state, before);
  session = result.state;
  console.log(JSON.stringify({ id, code: result.code, count: session.participants.length }));
}
session = closeSession(session);
assert.equal(register(session, "jun").code, "CLOSED");
const cancelled = cancel(session, "min");
assert.equal(cancelled.code, "CANCELLED");
assert.equal(cancelled.state.status, "CLOSED");
console.log("closed cancellation", cancelled.code, cancelled.state.participants.join(","));
{"id":"min","code":"REGISTERED","count":1}
{"id":"min","code":"ALREADY_REGISTERED","count":1}
{"id":"hana","code":"REGISTERED","count":2}
{"id":"jun","code":"FULL","count":2}
{"id":"","code":"INVALID_MEMBER","count":2}
closed cancellation CANCELLED hana

node:assert/strict의 assert.equal은 두 값이 같지 않으면 오류를 던져 프로그램을 멈춥니다. 문자열은 내용이 같은지 비교하지만 객체는 같은 객체인지를 비교하므로, assert.equal(result.state, before)는 거절된 신청이 새 상태를 만들지 않고 넘겨받은 상태를 그대로 돌려주었는지 확인합니다. for (const [id, expected] of cases)는 두 값짜리 배열에서 아이디와 기대하는 결과 코드를 하나씩 꺼내고, JSON.stringify는 출력할 객체를 한 줄의 문자열로 바꿉니다. 마지막 두 단언은 마감 뒤의 새 신청이 CLOSED를 받는지, 마감 뒤에 취소해도 모임이 다시 열리지 않는지를 확인합니다.

상태를 다음 호출로 이어 가기

register는 넘겨받은 상태를 고치지 않으므로, 호출한 쪽이 돌려받은 result.state를 다음 호출에 넘겨야 신청 결과가 쌓입니다. 반복문의 각 호출에서 넘긴 상태와 돌려받은 상태를 나란히 놓으면 다음과 같습니다.

호출넘긴 명단돌려받은 명단넘긴 상태와 같은 객체인가
min 첫 신청비어 있음min아니요, 새 상태
min 다시 신청minmin예, R3으로 거절
hana 신청minmin, hana아니요, 새 상태
jun 신청min, hanamin, hana예, R4로 거절
빈 아이디 신청min, hanamin, hana예, R1로 거절

성공했을 때도 넘긴 상태의 명단이 나중에 바뀌는 것이 아니라, 새 명단을 가진 새 상태가 따로 생깁니다. 그래서 이전 상태와 새 상태를 나란히 두고 비교할 수 있습니다. 반복문 끝의 session = result.state가 돌려받은 상태를 다음 호출의 입력으로 이어 주며, session을 const가 아니라 let으로 선언한 것도 이 재대입 때문입니다. 객체의 내용을 바꾸는 것과 변수가 가리키는 대상을 바꾸는 것은 다른 일이라는 점을 기억해 두면, 이후 장에서 상태를 다루는 코드를 읽기가 한결 쉽습니다.

이 실행이 오류 없이 끝났다는 것은 적어 둔 다섯 가지 신청과 마감 뒤의 두 사례에서 구현이 수락 조건과 일치한다는 뜻입니다. 정원이 1인 모임이나 마감 뒤의 중복 신청처럼 적지 않은 상황까지 맞다고 말해 주지는 않습니다. 앞에서 Given–When–Then으로 적은 R3 사례, 곧 min과 hana로 정원이 찬 뒤 min이 다시 신청하는 경우도 cases에는 없어서, register에서 R3과 R4의 순서를 바꾸어도 이 실행은 그대로 통과합니다. 어떤 사례를 골라야 잘못을 잘 찾을 수 있는지는 테스트 사례와 오라클에서 다룹니다.

정리

  • 기능 요구는 시스템이 해야 할 동작이고, 수락 조건은 그 동작이 요구를 만족했는지 판단할 구체적인 조건입니다.
  • 검증은 정한 규칙대로 만들었는지를, 확인은 그 규칙이 사용자의 문제를 해결하는지를 따집니다.
  • 조건이 겹치는 요구는 우선순위를 표에 적고, 코드의 검사 순서를 그 표와 맞춥니다.
  • Given–When–Then으로 수락 조건을 쓸 때는 전제가 되는 상태와 거절 뒤에도 바뀌지 않아야 할 것을 함께 적고, 기대 값은 실행 결과가 아니라 요구에서 끌어냅니다.
  • core.mjs는 거절할 때 넘겨받은 상태를 그대로, 성공할 때 고정된 새 상태를 돌려주며, 호출한 쪽이 result.state를 다음 호출로 이어 갑니다.
  • 사용자가 흔히 겪는 거절은 결과 코드로 돌려주고, 계속 실행해서는 안 되는 잘못된 값에는 오류를 던집니다.

연습 문제

1. 검사 순서를 바꾸면

누군가 register에서 마감 검사(R2)와 중복 검사(R3)의 순서를 바꾸었습니다. 어떤 사례에서 결과가 달라지나요? 그 차이를 잡아낼 수락 조건을 Given–When–Then으로 적으세요.

해설 보기

결과가 달라지는 것은 마감된 모임에 이미 신청한 사람이 다시 신청하는 경우뿐입니다. 원래 순서에서는 R2가 먼저라 CLOSED를 받지만, 순서를 바꾸면 중복 검사에 먼저 걸려 ALREADY_REGISTERED를 받습니다. 새 아이디는 어느 순서에서도 CLOSED를 받으므로, 새 아이디로만 시험하면 이 변화를 잡아내지 못합니다. 수락 조건은 "Given: 정원 2인 모임에 min과 hana가 신청한 뒤 마감되었다. When: min이 다시 신청한다. Then: CLOSED를 돌려주고 명단과 마감 상태는 그대로다."처럼 적을 수 있습니다.

2. 마감 뒤의 취소

R6의 수락 조건을 모임이 마감된 상황에서 Given–When–Then으로 적으세요. Then에 결과 코드만 적으면 어떤 잘못을 놓칠 수 있나요?

해설 보기

"Given: 정원 2인 모임에 min과 hana가 신청한 뒤 마감되었다. When: min이 취소한다. Then: CANCELLED를 돌려주고, 명단에는 hana만 남으며, 모임은 여전히 마감 상태다."처럼 적을 수 있습니다. 결과 코드만 적으면 취소하면서 다른 사람을 지우거나 아무도 지우지 않는 잘못, 빈자리가 생겼다고 모임을 다시 여는 잘못을 놓칩니다. 이 글의 예제가 cancelled.state.status가 CLOSED인지까지 확인하는 것도 그래서입니다. 명단에 없는 jun의 취소처럼 거절되는 사례에는 NOT_REGISTERED와 함께 명단이 그대로라는 조건을 따로 적습니다.

3. 재대입을 빠뜨리면

examples/ch2.mjs의 반복문에서 session = result.state; 줄을 지우고 실행하면 무엇이 출력되나요? 그 결과가 무엇을 알려 주는지 설명하세요.

해설 보기

첫 줄은 {"id":"min","code":"REGISTERED","count":0}으로 바뀝니다. register가 새 상태를 돌려주었지만 session은 여전히 빈 모임을 가리키므로 참가자 수가 0으로 나옵니다. 둘째 반복에서도 빈 모임에 min이 신청하므로 결과는 REGISTERED이고, 기대한 ALREADY_REGISTERED와 달라 assert.equal이 AssertionError를 던지며 실행이 멈춥니다. 상태를 이어 주지 않는 잘못을 단언이 곧바로 드러낸 것이며, 단언 없이 출력만 보았다면 첫 줄의 count가 0인 것을 놓치기 쉬웠을 것입니다.

수락 조건을 코드로 실행해 통과했더라도, 그 조건이 진행자가 원한 정책인지는 진행자와 다시 확인해야 합니다. 다음 글 비기능 요구와 변경 추적에서는 속도, 보안, 복구처럼 얼마나 잘 동작해야 하는지를 정하는 요구를 다루고, 요구가 바뀔 때 함께 고칠 곳을 찾는 방법을 정합니다.