동시 저장과 복구 계약
두 요청의 교차를 시간축으로 추적하고 버전 조건 저장·재판정·기준 해시 복원의 최소 모델을 실행합니다.
아키텍처 예제의 순차 load→판정→save를 이해했다면 두 요청이 같은 값을 읽는 경우로 범위를 넓힙니다. async 함수·Promise·await가 한 호출의 진행을 기다린다는 설명부터 확인합니다. 이 절은 외부 패키지와 실제 파일을 사용하지 않는 한 Node 프로세스의 메모리 모델입니다.
복사와 await만으로는 충돌을 막지 못한다
정원은 1이고 명단은 빈 배열입니다. min과 hana가 모두 빈 상태를 읽으면 각각 자신의 ID를 넣은 새 상태를 계산합니다. 저장소의 단순 save가 마지막 상태로 덮어쓰면 두 사람은 성공을 받는데 명단은 한 사람만 남습니다. 정원 숫자가 1이라는 사실만으로 ‘성공을 받은 사람과 최종 명단이 일치한다’는 계약을 지키지는 못합니다.
서로 독립적인 읽기와 저장으로 성공 기록이 사라진다
빈 상태에서 자신의 새 상태를 계산한다. 낡은 next가 min의 기록을 잃게 한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
서로 독립적인 읽기와 저장이 성공 기록을 잃는다
공유 코어가 올바른 상태를 만들더라도 저장 계약이 교차한 요청의 결과를 결정합니다.
실선은 호출, 점선 화살표는 응답, 수직 점선은 참여자의 생명선입니다. 위치는 책임, 위아래는 이 한 실행의 시간 순서입니다. 여기서는 버전 조건 없는 save를 사용합니다. CAS 계약의 대조 사례는 다음 그림입니다. 이 그림은 멀티프로세스 저장소 구현이나 실제 디스크 내구성을 보장하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 min 읽기부터 hana 덮어쓰기까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 서로 독립적인 읽기와 저장으로 성공 기록이 사라진다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| min 읽기 빈 상태에서 자신의 새 상태를 계산한다. |
|
|
| hana 읽기 min 저장 전에 같은 빈 상태를 읽는다. |
|
|
| min 저장 조건 없는 save로 min을 저장한다. |
|
|
| hana 덮어쓰기 낡은 next가 min의 기록을 잃게 한다. |
|
|
서로 독립적인 읽기와 저장으로 성공 기록이 사라진다
빈 상태에서 자신의 새 상태를 계산한다. 낡은 next가 min의 기록을 잃게 한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
서로 독립적인 읽기와 저장이 성공 기록을 잃는다
공유 코어가 올바른 상태를 만들더라도 저장 계약이 교차한 요청의 결과를 결정합니다.
실선은 호출, 점선 화살표는 응답, 수직 점선은 참여자의 생명선입니다. 위치는 책임, 위아래는 이 한 실행의 시간 순서입니다. 여기서는 버전 조건 없는 save를 사용합니다. CAS 계약의 대조 사례는 다음 그림입니다. 이 그림은 멀티프로세스 저장소 구현이나 실제 디스크 내구성을 보장하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 min 읽기부터 hana 덮어쓰기까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 서로 독립적인 읽기와 저장으로 성공 기록이 사라진다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| min 읽기 빈 상태에서 자신의 새 상태를 계산한다. |
|
|
| hana 읽기 min 저장 전에 같은 빈 상태를 읽는다. |
|
|
| min 저장 조건 없는 save로 min을 저장한다. |
|
|
| hana 덮어쓰기 낡은 next가 min의 기록을 잃게 한다. |
|
|
| 순서 | 요청/저장소의 일 | 관측 |
|---|---|---|
| 1 | min이 빈 상태를 읽음 | next=[min] 계산 |
| 2 | hana도 아직 빈 상태를 읽음 | next=[hana] 계산 |
| 3 | min 저장 | 명단 [min], 성공 |
| 4 | hana가 오래된 읽기를 바탕으로 저장 | 명단 [hana], 성공; min 업데이트 유실 |
이 교차가 모든 환경에서 언제나 일어나는 것은 아닙니다. 실행 순서를 제어해 이런 반례가 가능함을 확인하면 계약의 빈틈을 찾을 수 있습니다. Promise.all은 여러 호출의 완료를 기다리며, 그 호출들을 한 번에 한 요청씩 저장하도록 잠그지는 않습니다.
버전을 비교하고 같은 단계에서 저장한다
새 저장 계약은 load()가 {version,state}를 반환하고 compareAndSet(expectedVersion,next)가 현재 버전 비교와 저장을 한 원자적 단계로 수행하도록 요구합니다. 읽은 버전이 아직 현재 버전이면 저장한 뒤 버전을 1 올립니다. 달라졌다면 CONFLICT를 반환하며 저장하지 않습니다.
async compareAndSet(expectedVersion, next) {
if (expectedVersion !== version) {
return { ok: false, code: "CONFLICT" };
}
state = restoreSession(next);
version += 1;
return { ok: true, version };
}이것은 저장소 객체 안의 메서드 부분입니다. 실행할 전체 구현은 docs/cs-textbooks/software-engineering-lab/deepening.mjs의 createVersionStore에 있습니다. 검사와 쓰기 사이에 await가 없어서 이 단일 프로세스의 실행 중에는 다른 호출이 그 사이로 들어가지 않습니다. 원격 저장소에서 load→클라이언트에서 버전 비교→보통 save로 나누면 같은 원자성을 얻지 못합니다. 데이터베이스/저장 서비스 자체에서 조건부 갱신이나 트랜잭션을 제공해야 합니다.
버전 계약이 두 번째 낡은 저장을 거절한다
두 요청이 같은 빈 상태를 읽었다. 최신 상태로 업무 규칙을 다시 판정한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
버전 계약이 두 번째 낡은 저장을 거절한다
공유 코어가 올바른 상태를 만들더라도 저장 계약이 교차한 요청의 결과를 결정합니다.
실선은 호출, 점선 화살표는 응답, 수직 점선은 참여자의 생명선입니다. 위치는 책임, 위아래는 이 한 실행의 시간 순서입니다. CAS는 기대 버전 확인과 저장을 하나로 수행합니다. 이 그림은 멀티프로세스 저장소 구현이나 실제 디스크 내구성을 보장하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 같은 버전 읽기부터 재조회 후 FULL까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. min과 hana는 모두 빈 상태를 읽습니다. min의 버전 0 저장만 버전 1로 반영되고 hana의 버전 0 저장은 충돌로 거절됩니다. 이후 재조회와 FULL 판단은 본문 표에서 이어집니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 버전 읽기 두 요청이 같은 빈 상태를 읽었다. |
|
|
| min 커밋 비교와 저장을 한 원자적 동작으로 수행한다. |
|
|
| hana 충돌 오래된 버전의 쓰기를 거절한다. |
|
|
| 재조회 후 FULL 최신 상태로 업무 규칙을 다시 판정한다. |
|
|
버전 계약이 두 번째 낡은 저장을 거절한다
두 요청이 같은 빈 상태를 읽었다. 최신 상태로 업무 규칙을 다시 판정한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
버전 계약이 두 번째 낡은 저장을 거절한다
공유 코어가 올바른 상태를 만들더라도 저장 계약이 교차한 요청의 결과를 결정합니다.
실선은 호출, 점선 화살표는 응답, 수직 점선은 참여자의 생명선입니다. 위치는 책임, 위아래는 이 한 실행의 시간 순서입니다. CAS는 기대 버전 확인과 저장을 하나로 수행합니다. 이 그림은 멀티프로세스 저장소 구현이나 실제 디스크 내구성을 보장하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 같은 버전 읽기부터 재조회 후 FULL까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. min과 hana는 모두 빈 상태를 읽습니다. min의 버전 0 저장만 버전 1로 반영되고 hana의 버전 0 저장은 충돌로 거절됩니다. 이후 재조회와 FULL 판단은 본문 표에서 이어집니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 버전 읽기 두 요청이 같은 빈 상태를 읽었다. |
|
|
| min 커밋 비교와 저장을 한 원자적 동작으로 수행한다. |
|
|
| hana 충돌 오래된 버전의 쓰기를 거절한다. |
|
|
| 재조회 후 FULL 최신 상태로 업무 규칙을 다시 판정한다. |
|
|
| 순서 | 기대 버전 | 실제 저장 버전 | 결과 |
|---|---|---|---|
| min과 hana가 읽음 | 둘 다 0 | 0 | 둘 다 아직 참가 가능하다고 계산 |
| min이 조건부 저장 | 0 | 0 | commit, 저장 버전 1·명단 [min] |
| hana가 조건부 저장 | 0 | 1 | CONFLICT, 명단 유지 |
| hana가 다시 읽고 업무 규칙 재판정 | 1 | 1 | 정원 1이 이미 차서 FULL; 새 저장 없음 |
충돌 뒤 같은 next=[hana]를 다시 쓰면 min을 지울 수 있습니다. 다시 읽기 → 요구 우선순위로 다시 판정 → 새 버전으로 저장해야 합니다. 무한 재시도도 피합니다. 구현은 최대 3번 시도하고 계속 충돌하면 CONFLICT를 호출자에게 돌려줍니다. 마지막 FULL은 저장 오류가 아니라 최신 상태를 읽은 뒤의 업무 거절입니다.
두 읽기가 먼저 완료되도록 재현한다
이번 심화 모델은 저장소 예제 묶음의 경로를 사용합니다. 실행 위치 확인에 따라 저장소 최상위 폴더에서 실행합니다. 직접 만든 reading-lab의 장별 경로와 혼동하지 않습니다.
node docs/cs-textbooks/software-engineering-lab/run-deepening.mjs
node --test docs/cs-textbooks/software-engineering-lab/deepening.test.mjsbarrierForFirstTwoLoads는 처음 두 읽기가 모두 끝날 때까지 두 호출을 기다리게 합니다. 이후 재읽기에는 장벽을 적용하지 않습니다. 따라서 우연한 시간 지연이나 sleep 값으로 충돌을 기대하지 않고, 같은 버전 0을 읽은 상황을 만든 뒤 저장 계약을 관찰합니다.
const trace = [];
const store = createVersionStore(initial, trace);
const guarded = barrierForFirstTwoLoads(store);
const results = await Promise.all([
reserveWithVersion(guarded, "min"),
reserveWithVersion(guarded, "hana"),
]);먼저 의도적으로 버전 조건 없는 저장의 반례를 실행하고 출력을 확인합니다. 두 결과 REGISTERED,REGISTERED, 이벤트 load,load,blind-save,blind-save, 최종 명단 [hana]를 확인합니다. 이는 실패 계약을 보여 주기 위해 만든 실행이며 실제 제품에서 새 오류를 발견했다는 뜻이 아닙니다.
그다음 버전 계약의 결과 코드는 REGISTERED,FULL이고 이벤트는 load,load,commit,conflict,load입니다. 저장 명단 [min]·버전 1을 함께 확인합니다. 이 실행에서는 min 호출을 먼저 시작해 min이 첫 저장을 하지만, 운영 환경에서 언제나 min이 이긴다는 공정성 보장은 아닙니다. 취소·마감도 동일 저장 계약을 사용해야 전체 업무의 일관성을 말할 수 있습니다. 이번 실행 함수는 신청만 구현합니다.
동시성 보장과 복구 보장은 별개다
메모리 버전 검사는 전원이 꺼져도 저장된다는 뜻이 아닙니다. 기존 백업 실험의 해시 길이 64 검사는 해시 형식만 확인합니다. 여기서는 별도 최소 모델로 신뢰하는 기준 해시와 실제 바이트의 해시를 비교한 뒤 스키마·정원·중복 규칙을 검증합니다.
| 복원 단계 | 확인하는 계약 | 실패 때 행동 |
|---|---|---|
| 기준 SHA-256과 실제 계산값 비교 | 기준 백업과 동일한 내용인가 | 불일치 거절 |
| schemaVersion·정수 version 검사 | 지원하는 저장 형식인가 | 알 수 없는 형식 거절 |
| restoreSession | 명단/정원/ID 불변식을 만족하는가 | 잘못된 상태 거절 |
| 검증된 결과 반환 | 이전 정상 상태를 대체할 후보인가 | 호출자가 승인한 후 사용 |
예제는 기준 백업에 min을 넣은 뒤, 유효한 JSON의 명단만 hana로 바꿉니다. JSON 문법 검사는 통과할 수 있지만 기준 해시와 달라 snapshot checksum mismatch로 거절됩니다. 정상 백업은 min으로 복원됩니다. 파일 쓰기와 실제 장애는 하지 않습니다.
해시와 데이터가 함께 공격자에게 바뀌면 일반 SHA-256 비교만으로 출처를 인증할 수 없습니다. 기준 해시의 신뢰 경계도 필요합니다. 디스크 내구성에는 쓰기 완료·flush/fsync·교체와 디렉터리 메타데이터·장애 중 복원 절차를 환경에 맞게 다뤄야 합니다. Node writeFile 공식 문서는 기본 flush=false와 동시 호출 주의를 설명합니다. 여기의 메모리 실험을 실제 전원 중단 복구 시험으로 확대하지 않습니다.
독립 문제: 충돌을 성공으로 바꾸고 싶은 제안
팀원이 CONFLICT를 받으면 같은 next를 보통 save로 저장한 뒤 성공을 반환하자고 합니다. 정원 1의 두 요청 사례에서 잃는 계약과 올바른 다음 처리, 동시성 검증만으로 증명되지 않는 운영 보장을 적습니다.
힌트·해설
오래된 next는 현재 저장된 다른 참가자를 포함하지 않을 수 있습니다. 충돌을 무시하면 저장 유실과 성공 명단 불일치가 돌아옵니다. 최신 버전을 다시 읽고 업무 규칙을 재판정해야 하며, 재시도 상한도 둡니다. 원자적 조건부 쓰기와 데이터베이스 트랜잭션은 환경에서 별도로 확인합니다. 디스크 내구성·백업 신뢰·인증·공정성은 이 메모리 모델만으로 증명되지 않습니다.
이 본문은 원자료에 없는 교차·조건부 저장·기준 해시 비교를 한정된 모델로 보강한 자체 설명입니다. 입문 core의 두 층 freeze와 ID 규칙은 변경하지 않습니다.