본문 바로가기

저장 방식 변경과 의존성

메모리·JSON 저장소를 같은 비동기 계약으로 교체하고 파일 실패·동시성·내구성 한계를 구분합니다.

메모리 저장은 프로그램을 끝내면 명단을 잃습니다. JSON 파일에 저장하면 다음 실행에서 읽을 수 있지만 파일 오류·손상·동시 수정이라는 새 문제가 생깁니다. 저장소를 교체한다는 것은 함수 이름을 바꾸는 일뿐 아니라 보존·복사·실패의 계약을 맞추는 일입니다.

앞 본문의 인터페이스·Adapter와 async/await 흐름을 읽었다면 진행할 수 있습니다. 이번 목표는 같은 업무 함수에 두 저장 구현을 넣어 결과가 같은지 확인하고, 이 파일 모델이 보장하지 않는 운영 조건을 설명하는 것입니다.

업무가 필요한 저장 계약

여기서는 한 모임 상태를 저장하는 작은 계약을 씁니다. 교재 전체 앱의 모든 명령과 기능을 한 번에 다시 구현하지 않고 ‘모집 종료’라는 행동 하나로 저장 경계를 확인합니다.

연산결과·부수효과약속
await load()모임 상태 반환호출자의 변경이 저장된 상태에 직접 새지 않는다
await save(next)전체 모임 상태 저장성공할 때 완료, 실패는 예외로 전달
업무 함수읽기 → 새 상태 생성 → 저장저장 성공 전에는 완료로 응답하지 않는다

이 계약에서는 파일이 없을 때 자동으로 빈 명단을 만들어 반환하지 않습니다. ‘아직 생성하지 않음’과 ‘기존 파일을 잃음’을 같은 정상 상태로 취급하면 데이터 손실을 숨길 수 있습니다. 예제는 첫 저장을 명시적으로 수행한 뒤 읽습니다. 새 앱을 만들 때는 초기 생성과 기존 데이터 열기를 다른 동작으로 설계할 수 있습니다.

의존 방향을 읽는다

의존성 주입(dependency injection)은 사용할 협력 객체를 외부에서 전달하는 방식입니다. closeUsingStore(store)는 특정 파일 이름이나 메모리 변수에 접근하지 않고 load/save를 가진 객체를 받습니다. 시작점에서 어떤 저장소를 만들지 선택합니다.

의존 관계 역전(DIP)은 중요한 업무 정책이 쉽게 바뀌는 세부 구현 대신 필요한 추상 계약에 의존하게 하는 설계 방향입니다. 여기서는 업무가 Node 파일 API를 직접 부르지 않습니다. 함수 인수로 객체를 넣는 문법만 썼다고 모든 의존이 잘 설계된 것은 아닙니다. 객체의 계약이 여전히 파일 경로·내부 배열 형식에 얽혀 있으면 효과가 제한됩니다.

SOFTWARE ENGINEERING · CH7-3

업무는 필요한 계약을 사용하고 저장 구현을 교체한다

실선은 업무의 계약 의존, 점선은 구현이 충족하는 계약을 뜻합니다.

업무는 필요한 계약을 사용하고 저장 구현을 교체한다모집 종료 업무는 load/save 계약에 의존하고 메모리와 JSON 파일 구현이 같은 계약을 충족합니다. 화살표는 의존이며 데이터 흐름이나 실행 순서가 아닙니다.사용하는 계약구현구현모집 종료 업무특정 저장 위치를 모름저장소 계약async load · save메모리객체 사본JSON 파일UTF-8 텍스트

모집 종료 업무는 load/save 계약에 의존하고 메모리와 JSON 파일 구현이 같은 계약을 충족합니다. 화살표는 의존이며 데이터 흐름이나 실행 순서가 아닙니다.

도표의 화살표는 실행 중 데이터가 이동하는 방향이 아니라 사용하는 계약으로 향하는 의존을 나타냅니다. 파일 구현은 load/save 계약을 충족하고 시작점이 이를 업무에 전달합니다. 실제 호출 시 어떤 구현이 실행되는지는 시작점의 선택에 달려 있습니다.

실제 파일에 쓰고 다시 읽는다

아래 예제는 Node 기본 모듈만 사용합니다. mkdtemp는 운영체제 임시 폴더에 이번 실행만의 새 폴더를 만들고, 파일 이름은 그 폴더 아래의 session.json으로 고정합니다. 예제가 끝나면 만든 파일과 빈 폴더만 정리합니다. 사용자의 기존 데이터나 원본 교재 경로를 사용하지 않습니다.

readFile/writeFile은 UTF-8 텍스트를 읽고 씁니다. JSON.stringify는 객체를 문자열로, JSON.parse는 저장된 문자열을 값으로 바꿉니다. JSON은 문자열·숫자·boolean·배열·객체·null 같은 데이터 표현입니다. 함수나 임의 객체의 행동까지 저장하는 형식은 아닙니다.

저장소 최상위 폴더에서 다음 명령을 실행합니다. Node.js가 설치되어 있어야 하며 패키지를 추가로 설치하지 않습니다. 두 번째 인수를 생략하면 이 장의 예제를 모두 실행합니다.

node dev-note/scripts/software-engineering-lab/examples/ch7.mjs ch7-3
ch7-3 실행 예제
import { mkdtemp, readFile, writeFile, unlink, rmdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

function createMemoryStore(seed) {
  let value = structuredClone(seed);
  return {
    async load() { return structuredClone(value); },
    async save(next) { value = structuredClone(next); },
  };
}
function createJsonStore(filePath) {
  return {
    async load() { return JSON.parse(await readFile(filePath, "utf8")); },
    async save(next) {
      await writeFile(filePath, JSON.stringify(next, null, 2), "utf8");
    },
  };
}
async function closeUsingStore(store) {
  const current = await store.load();
  const next = { ...current, status: "CLOSED" };
  await store.save(next);
  return next;
}
const seed = {
  id: "book-08", title: "저장과 경계",
  capacity: 2, status: "OPEN", participants: ["member-a"],
};
const directory = await mkdtemp(join(tmpdir(), "swe-store-"));
const filePath = join(directory, "session.json");
try {
  const memory = createMemoryStore(seed);
  const file = createJsonStore(filePath);
  await file.save(seed);
  console.log("메모리:", (await closeUsingStore(memory)).status);
  console.log("JSON:", (await closeUsingStore(file)).status);
  const reopened = createJsonStore(filePath);
  console.log("새 저장소 객체로 읽기:", (await reopened.load()).status);
} finally {
  await unlink(filePath).catch((error) => {
    if (error.code !== "ENOENT") throw error;
  });
  await rmdir(directory);
}

세 줄의 모집 상태가 모두 CLOSED인지 확인합니다. closeUsingStore를 바꾸지 않고 메모리·파일 구현을 대입했습니다. 새 JSON 저장소 객체로 읽어도 같은 파일의 상태를 얻습니다. 이는 저장소 객체의 내부 메모리가 아니라 실제 파일 내용을 읽었다는 확인입니다. 프로세스 재시작·전원 장애를 실제 수행한 검증은 아닙니다.

finally는 처리 중 실패하더라도 이번 실습의 임시 파일 정리를 시도합니다. 파일이 생성되지 않은 경우의 ENOENT만 무시하고 다른 삭제 오류는 숨기지 않습니다. rmdir는 빈 폴더만 지우며 임의 하위 파일을 재귀 삭제하지 않습니다.

파일 오류는 업무 거절과 다르다

FULL이나 CLOSED는 정상적으로 읽은 모임의 정책 판단입니다. 디스크 쓰기 실패는 정책 판단이 아니라 저장을 완료하지 못한 장애입니다. 저장 호출을 await하지 않고 바로 성공 응답을 반환하면 사용자에게 완료라고 말한 뒤 저장이 실패할 수 있습니다.

장별 실행 파일은 아래 저장 실패 대역도 검사합니다. 직접 파일을 만드는 경로에서는 위 실행 코드 뒤에 추가합니다. assert.rejects는 비동기 작업이 지정한 오류로 거절되는지 확인하며, 저장 실패 뒤 원래 입력 상태도 검사합니다.

import assert from "node:assert/strict";
const failure = {
  async load() { return seed; },
  async save() { throw new Error("write failed"); },
};
await assert.rejects(closeUsingStore(failure), /write failed/);
assert.equal(seed.status, "OPEN");

이 예제의 업무 함수는 실패를 호출자에게 예외로 전달합니다. 실제 UI는 입력·현재 상태를 보존하고 적절한 장애 안내를 표시해야 합니다. 읽기 실패를 빈 상태로 덮어쓴다거나 알 수 없는 예외를 ‘이미 신청’으로 바꾸지 않습니다. 오류 종류와 복구 선택을 분명히 해야 합니다.

파일에서 읽은 데이터는 믿을 수 있는 함수 반환값과 같게 취급하지 않습니다. JSON 문법이 맞아도 정원이 음수, ID가 중복, status가 미정의 문자열일 수 있습니다. 이 최소 파일 구현은 스키마 검증까지 넣지 않았습니다. 전체 앱에 사용할 때는 타입·범위·불변식을 검증하고 잘못된 파일을 원래 자료와 구분해 처리해야 합니다.

이 교체가 해결하지 않는 문제

save가 호출되어 완료되었다고 여러 요청의 읽기–판정–쓰기 전체가 하나의 원자적 작업이 되는 것은 아닙니다. 두 요청이 같은 명단을 읽고 서로 다른 새 명단을 저장하면 나중 쓰기가 이전 쓰기를 덮어쓸 수 있습니다. 같은 파일에 병렬 write를 시작하는 것과 별도로 읽기 이후의 변경 경쟁도 검토해야 합니다. Node.js 파일 API 안내는 Promise 기반 파일 작업의 완료와 같은 파일에 대한 쓰기 주의점을 설명합니다.

직접 파일을 덮어쓰는 중에 실패하면 일부 내용만 남을 수 있습니다. 임시 파일을 완성한 뒤 바꾸는 방식, 백업, 파일 시스템과 운영체제의 보장, 접근 권한을 검토할 수 있지만 이 예제는 내구성·동시성·원자적 교체를 보장하지 않습니다. 웹의 다중 사용자 서비스에서는 트랜잭션·제약·경쟁 제어를 제공할 저장 방식을 별도로 선택해야 합니다.

변경을 쉽게 만든 경계와 안전한 운영 저장은 서로 다른 검증 대상입니다. 이 실습에서 확인한 것은 같은 계약으로 두 구현을 호출한 정상 흐름과 저장 실패의 전달입니다.

독립 문제: 설정 저장소의 실패 계약

설정 프로그램은 loadSettings/saveSettings로 테마와 글자 크기를 저장합니다. 기존 저장소는 실패하면 false를 반환하고 새 업무 함수는 실패가 예외로 전달된다고 가정합니다. 입력 파일에 fontSize: "크게"가 있어도 JSON 문법은 정상입니다.

기존 저장소를 그대로 대입하면 어떤 잘못된 성공이 생길 수 있는지 설명하고 Adapter에서 실패를 맞추는 방법을 적습니다. 읽은 설정의 검증 항목 두 개와, 두 구현에 공통으로 실행할 계약 검사 두 개를 제안합니다.

힌트

await false는 예외가 아닙니다. 문법 파싱과 업무 데이터 검증도 다른 단계입니다.

풀이

업무 함수가 save 결과의 false를 보지 않고 다음 줄에서 완료를 반환하면 실제 저장 실패를 성공으로 알립니다. Adapter는 false를 확인해 예외로 바꾸거나 업무 계약을 명시적으로 결과 객체 방식으로 통일해야 합니다. 실패 약속을 선택하고 모든 구현과 호출자가 따라야 합니다.

읽은 테마가 허용 목록에 있는지, 글자 크기가 숫자이며 허용 범위 안인지 확인합니다. 계약 검사는 저장한 값을 다시 읽어 동일한지, 읽은 객체를 외부에서 수정해도 저장 값이 자동 변경되지 않는지입니다. 실패 구현을 넣어 성공 안내가 나오지 않는지도 추가로 확인합니다.

학습을 이어 가는 질문

저장 구현을 바꾸어도 업무 계약은 유지되지만 실행 환경의 위험은 달라집니다. 다음 계획·품질·변경 관리 단원에서는 이런 가정과 한계를 기록하고 새 요구가 생겼을 때 어떤 검증을 다시 수행할지 연결합니다.


원자료와 더 읽기

장원사이버평생교육원 「소프트웨어공학」 PDF 물리 128–148·181–185·197–200쪽의 주제를 바탕으로 설명·도표·실습을 새로 구성했습니다.

실제 파일 동작은 실행 환경과 같은 주 버전의 Node.js 24 파일 API를 참고합니다. 예제는 격리 임시 파일의 순차 읽기·쓰기를 확인하며 운영 저장의 내구성·동시성을 주장하지 않습니다.