릴리스·복구·운영 피드백
릴리스와 배포, 롤백과 복구를 구별하고, 버전과 저장 형식, 확인 결과를 묶어 전달을 준비합니다. 임시 폴더에 가상 자료를 만들어 후보 파일을 검사한 뒤 교체하고 백업에서 되살리는 실험을 실행하며, 운영에서 얻은 정보를 다음 요구로 이어 가는 방법을 다룹니다.
새 버전에 문제가 있어 이전 코드로 되돌렸는데, 새 버전이 저장한 파일을 이전 코드가 읽지 못합니다. 사용할 구성을 식별해 전달하는 일을 릴리스(release)라 하고, 그 구성을 실제 실행 환경에 적용하는 일을 배포(deployment)라고 합니다. 이 글에서는 전달하기 전에 무엇을 함께 묶어야 하는지 정하고, 저장 자료를 검사하고 되살리는 과정을 작은 실험으로 확인합니다. 내 컴퓨터에서 실험이 성공했다고 해서 운영 환경의 배포가 성공한 것은 아니라는 점도 함께 기억해 둡니다.
전달하기 전에 묶을 것
릴리스에는 버전, 변경 요약, 지원하는 실행 환경, 인수 테스트와 회귀 테스트의 결과, 저장 형식의 호환성, 그리고 되돌릴 때 쓸 자료와 조건을 함께 묶습니다. 사용자가 알아차릴 정책의 변화와 내부의 개선은 나누어 설명합니다. 실행하지 않은 테스트나 실제 환경에서 확인하지 않은 부분이 있다면, 그 범위를 숨기지 않고 그대로 적어 둡니다.
롤백(rollback)은 이전 구성으로 되돌리는 대응이고, 복구(recovery)는 필요한 데이터와 서비스를 다시 쓸 수 있게 만드는 활동입니다. 저장 형식이 바뀌었다면 코드만 되돌려서는 충분하지 않을 수 있습니다. 그래서 되돌릴 수 있는 데이터, 이전 버전과의 호환성, 어디까지의 데이터 손실을 받아들일지를 릴리스 전에 함께 정해 둡니다.
전달 구성과 복구 자료를 함께 검증한다
min이 참가한 스키마 1 자료와 백업을 만든다. 호환 백업에서 실제 참가 상태를 복원한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
전달 구성과 복구 자료를 함께 검증한다
코드를 이전 버전으로 되돌릴 수 있어도 저장 자료가 호환되지 않을 수 있습니다.
예제는 가상 자료의 로컬 복원 실험입니다. 실제 서비스 배포·동시성·권한·장애 내구성은 별도 환경에서 검증합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 호환 자료 준비부터 백업 복원까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호환 자료 준비 min이 참가한 스키마 1 자료와 백업을 만든다. |
|
|
| 후보 확인 읽어서 스키마와 상태를 검사한 뒤 이름을 바꾼다. |
|
|
| 백업 복원 호환 백업에서 실제 참가 상태를 복원한다. |
|
|
전달 구성과 복구 자료를 함께 검증한다
min이 참가한 스키마 1 자료와 백업을 만든다. 호환 백업에서 실제 참가 상태를 복원한다. 각 단계의 값과 전환 이유를 비교합니다.
SOFTWARE ENGINEERING · READING CLUB
전달 구성과 복구 자료를 함께 검증한다
코드를 이전 버전으로 되돌릴 수 있어도 저장 자료가 호환되지 않을 수 있습니다.
예제는 가상 자료의 로컬 복원 실험입니다. 실제 서비스 배포·동시성·권한·장애 내구성은 별도 환경에서 검증합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 호환 자료 준비부터 백업 복원까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호환 자료 준비 min이 참가한 스키마 1 자료와 백업을 만든다. |
|
|
| 후보 확인 읽어서 스키마와 상태를 검사한 뒤 이름을 바꾼다. |
|
|
| 백업 복원 호환 백업에서 실제 참가 상태를 복원한다. |
|
|
임시 폴더에서 하는 복원 실험
그림의 과정을 코드로 옮겨 봅니다. 다음 코드는 운영체제의 임시 폴더 안에 새 폴더를 하나 만들어 가상의 모임 자료만 쓰고, 끝나면 그 폴더만 지웁니다. 다른 프로젝트나 사용자의 자료는 읽거나 바꾸지 않으므로, 실행하기 전에 코드를 읽어 폴더를 만들고 지우는 범위를 확인해 보세요. reading-lab/examples/ch10-2.mjs로 저장해 실행합니다.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, writeFile, rename, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createSession, register, restoreSession } from "../core.mjs";
function decodeSnapshot(encoded) {
const snapshot = JSON.parse(encoded);
if (snapshot.schemaVersion !== 1) throw new Error("unsupported schema version");
return restoreSession(snapshot.session);
}
const registered = register(createSession({ id: "book-1", title: "첫 책", capacity: 2 }), "min").state;
const encoded = JSON.stringify({ schemaVersion: 1, session: registered });
const sha256 = createHash("sha256").update(encoded).digest("hex");
assert.throws(() => decodeSnapshot('{"schemaVersion":2}'), /unsupported/);
const directory = await mkdtemp(path.join(tmpdir(), "reading-lab-release-"));
try {
const backup = path.join(directory, "backup.json");
const candidate = path.join(directory, "candidate.json");
const current = path.join(directory, "current.json");
await writeFile(backup, encoded, "utf8");
await writeFile(candidate, encoded, "utf8");
decodeSnapshot(await readFile(candidate, "utf8"));
await rename(candidate, current);
const restored = decodeSnapshot(await readFile(backup, "utf8"));
assert.deepEqual(restored.participants, ["min"]);
assert.equal(register(restored, "min").code, "ALREADY_REGISTERED");
console.log("schema 1, sha256 length", sha256.length);
console.log("local backup restore PASS");
} finally {
await rm(directory, { recursive: true, force: true });
}schema 1, sha256 length 64
local backup restore PASS저장 자료에는 형식의 버전을 나타내는 schemaVersion을 넣고, decodeSnapshot은 버전 1만 받아들여 버전 2는 분명하게 거절합니다. 알 수 없는 버전을 버전 1로 여기고 읽으면 새 버전에서 더한 필드를 아무 경고 없이 잃을 수 있기 때문입니다. 첫 줄의 해시 길이 64는 SHA-256 해시가 정상적인 형식으로 만들어졌다는 것만 확인할 뿐이며, 백업의 내용이 바뀌지 않았는지까지 확인하려면 동시 저장과 복구에서처럼 기준 해시와 비교해야 합니다.
새 파일은 바로 덮어쓰지 않고, 후보 파일 candidate.json에 먼저 쓴 뒤 읽어서 형식과 불변 조건을 검사하고, 통과하면 같은 폴더 안에서 current.json으로 이름을 바꿉니다. 마지막으로 백업에서 되살린 상태로 min의 재신청이 중복으로 거절되는지 확인해, 참가 상태가 제대로 돌아왔음을 확인합니다. rm의 recursive: true는 폴더 안의 파일까지 지우는 옵션이므로, 이 코드처럼 방금 만든 임시 폴더에만 써야 합니다. 함께 넘긴 force: true는 지울 경로가 없어도 오류를 내지 않게 합니다.
이 실험은 내 컴퓨터에서 차례로 실행한 단순한 확인입니다. 실제 파일 시스템이 장애를 견디는지, 여러 프로세스가 동시에 읽고 쓰는 경우, 권한 문제와 디스크 오류까지 확인한 저장 시스템은 아닙니다. 파일 함수가 무엇을 약속하는지는 Node.js 파일 시스템 문서에서 확인합니다.
더 깊이: 이름을 바꾸어 교체하는 방식이 지키는 것과 지키지 못하는 것
후보 파일에 다 쓴 뒤 이름을 바꾸어 교체하는 방식은 Linux 같은 POSIX 계열 운영체제의 rename()이 주는 약속에 기댑니다. rename(2)에 따르면 새 이름의 파일이 이미 있으면 한 번에 바꾸어, 다른 프로세스가 그 이름을 열려고 할 때 파일이 없는 순간이 생기지 않습니다. 그래서 current.json을 읽는 쪽은 예전 내용이나 새 내용 가운데 하나를 통째로 보고, 반쯤 쓴 파일을 보지 않습니다. 두 이름이 서로 다른 파일 시스템에 있으면 rename()은 EXDEV 오류로 실패하므로, 후보 파일은 같은 폴더처럼 같은 파일 시스템 안에 둡니다.
다만 이 약속은 다른 프로세스가 무엇을 보느냐에 대한 것이지, 전원이 꺼진 뒤에 무엇이 남느냐에 대한 것은 아닙니다. 쓴 데이터는 운영체제의 버퍼에 머물다 나중에 디스크로 가므로, fsync(2)는 파일의 내용을 저장 장치까지 내보내려면 fsync()를 부르고, 그 파일을 가리키는 폴더의 항목까지 남기려면 폴더에 대해서도 fsync()를 따로 불러야 한다고 적습니다. Node.js 24의 fsPromises.writeFile은 flush: true를 주면 다 쓴 뒤 filehandle.sync()로 내용을 내보내지만, 이름을 바꾼 폴더의 항목은 이 옵션으로 함께 남지 않습니다. 임시 파일에 쓰고 fsync()한 뒤 이름을 바꾸고 폴더까지 fsync()하는 순서 전체는 운영체제 교재의 파일 시스템 실무에서 코드로 따라가 볼 수 있습니다.
운영에서 얻은 정보를 다음 요구로 잇는다
운영 중에 오류가 나면 시각, 실행 환경, 버전, 문제를 다시 일으키는 입력, 실제로 나타난 결과를 기록합니다. 이때 실제 사용자의 아이디나 비밀번호 같은 값은 그대로 남기지 않습니다. 예를 들어 "저장에 실패했는데 성공했다고 안내했다"는 기록은 다음 반복의 수락 사례와 오류 처리 개선의 입력이 됩니다. 원인을 모른 채 배포만 자주 하면, 무엇이 나아졌는지 비교할 근거가 사라집니다.
정리
- 릴리스는 사용할 구성을 식별해 전달하는 일이고, 배포는 그 구성을 실제 환경에 적용하는 일입니다.
- 릴리스에는 버전, 변경 요약, 지원 환경, 테스트 결과, 저장 형식의 호환성, 되돌릴 자료와 조건을 함께 묶습니다.
- 롤백은 이전 구성으로 되돌리는 것이고 복구는 데이터와 서비스를 다시 쓸 수 있게 하는 것이며, 저장 형식이 바뀌면 코드의 롤백만으로는 부족할 수 있습니다.
- 저장 자료에는 형식의 버전을 넣고, 알 수 없는 버전은 거절하며, 새 파일은 검사를 통과한 뒤에 교체합니다.
- 운영의 오류 기록은 개인 정보를 빼고 남겨 다음 반복의 요구와 수락 사례로 이어 갑니다.
연습 문제
1. 호환되지 않는 롤백
새 버전은 참가자의 아이디 문자열 대신 내부에서 쓰는 숫자 아이디를 저장합니다. 이전 버전은 문자열 아이디만 받아들입니다. 새 버전이 만든 파일을 그대로 둔 채 코드만 되돌리면 어떤 위험이 있고, 릴리스 전에 무엇을 준비해야 할까요?
해설 보기
이전 코드가 새 파일을 읽지 못해 거절하거나, 숫자를 문자열로 잘못 해석해 엉뚱한 명단을 만들 수 있습니다. 그래서 저장 형식의 버전, 형식을 바꾸는 규칙, 호환성 테스트, 바꾸기 전의 백업, 되돌리는 절차와 그때 잃을 수 있는 데이터를 미리 준비해야 합니다. 숫자 아이디에서 원래의 문자열 아이디를 찾을 대응표가 없다면 되돌리는 변환 자체가 불가능합니다. 그리고 코드의 롤백이 성공했는지와 명단이 제대로 복구되었는지는 따로 확인합니다.
2. 바로 덮어쓰기와 후보 파일
본문 예제는 새 자료를 current.json에 바로 쓰지 않고 candidate.json에 먼저 썼습니다. 바로 덮어쓰면 어떤 문제가 생길 수 있는지 설명하세요.
해설 보기
current.json을 바로 덮어쓰다가 중간에 실패하면 파일에 내용의 일부만 남아, 기존 자료도 새 자료도 아닌 망가진 상태가 될 수 있습니다. 또 새 자료에 형식이나 불변 조건의 문제가 있어도, 이미 덮어쓴 뒤라 문제를 발견했을 때는 원래 자료가 사라진 상태입니다. 후보 파일에 먼저 쓰고 검사를 통과한 뒤에 이름을 바꾸면, 검사에 실패해도 기존 파일은 그대로 남습니다. 다만 이름 바꾸기가 장애 중에도 안전한지는 운영체제와 파일 시스템에 따라 다르므로 따로 확인해야 합니다.
3. 오류 신고를 기록하기
참가자가 "신청 버튼을 눌렀더니 완료라고 나왔는데 명단에 제 이름이 없어요"라고 알려 왔습니다. 이 신고를 운영 기록으로 남길 때 적을 항목과, 다음 반복으로 이어 갈 작업을 제안하세요.
해설 보기
신고 시각, 사용한 앱의 버전과 환경, 신청한 모임, 그 시각의 저장 오류 기록 여부, 화면에 나온 안내 문구를 적고, 참가자의 실제 아이디는 가린 값으로 남깁니다. 다음 반복에서는 "저장에 실패하면 완료 안내를 보여 주지 않는다"는 수락 사례를 만들고, 저장 방식 변경과 의존성에서처럼 실패하는 저장소로 그 경로를 시험합니다. 원인이 저장 실패인지 다른 문제인지 확인하기 전에 서둘러 다시 배포하지 않습니다.
마지막 글 역공학과 재구조화에서는 문서가 부족한 기존 시스템을 넘겨받았을 때, 그 구조와 의도를 확인하고 개선하는 방법을 다룹니다.