메모리와 파일

메모리의 데이터를 영구적으로 보관하기 위해서 데이터 포맷을 정의하고 파일로 저장해봅니다.
메모리, 파일, 데이터베이스, 데이터 포맷


데이터를 파일에 보관하기

Memory vs. FileSystemMemory vs. FileSystem

CPU는 메모리에 있는 데이터만을 이용해 작업을 할 수 있습니다. 지난 4장에서 작성한 TodoList 서비스는 모든 데이터를 메모리 상에 유지하였습니다. 때문에 **서버 프로그램을 종료하게 되면 데이터의 변경사항을 잃어버리게 됩니다. **또한 메모리는 그 용량이 보조기억장치에 비해 작기 때문에 대규모의 데이터를 유지하지 못합니다.
이러한 한계를 극복하기 위해서 메모리의 데이터를 OS의 파일 시스템을 통해 보조기억장치에 파일로 저장 할 수 있습니다. 또한 데이터를 파일의 형태로 저장함으로써, 프로그램과 데이터를 분리 할 수 있고, 이를 통해 데이터의 보존과 이동이 가능합니다.

LoadingLoading

특히 게임 같이 이미지, 미디어 등의 용량이 큰 리소스를 많이 다루는 프로그램엔 로딩(Loading)이라는 작업이 빈번합니다. 메모리를 하나의 프로세스가 독식 할 수 없고, 또 메모리 용량보다 프로그램에 필요한 전체 리소스의 용량이 큰 경우엔 리소스 전체를 메모리에 유지 할 수가 없습니다. 이 때문에 리소스를 파일의 형태로 저장해둔 채로, 실행 중에 실시간으로 필요한 리소스를 조금씩 메모리 상으로 로드하고, 불필요한 리소스는 메모리에서 해제하는 과정이 필요합니다. 이 과정을 로딩이라고 표현합니다.

데이터 포맷

데이터를 파일 시스템을 통해 저장하려면 데이터를 어떻게 구조화해서 저장 할 지 계획해야 합니다.

바이너리

바이너리바이너리

이때 독자적인 파일 포맷을 고안해서 바이너리로 저장 할 수 있습니다. (이미지, 비디오나 오디오, 포토샵, PDF 등)

텍스트 포맷

또 쉽게는 데이터를 특정 인코딩을 따라 텍스트로 변환해서 파일로 저장 할 수 있습니다. 이때 데이터를 구조화하기 위해서 CSV, XML, JSON, YAML 등과 같은 널리 쓰이는 텍스트 포맷을 이용 할 수 있겠습니다. 텍스트로 저장하는 경우엔 사람이나, 다른 프로그램이 데이터를 이해하기에 좋습니다. 다만 인코딩 과정에서 파일의 크기가 증가 할 수 있습니다.

TodoList 리팩토링

웹 서비스에서 유저들이 바이너리 데이터를 생산 할 일이 많지는 않습니다. 4장에서 작성했던 TodoList의 데이터를 JSON 포맷의 텍스트 파일로 저장하고 관리 할 수 있도록 리팩토링해보겠습니다.

todolist_file/models/model.js

const fs = require('fs-promise'); // fs 코어 모듈의 콜백 기반 API를 promise로 대체한 모듈
const path = require('path');
const dataFilePath = path.join(__dirname, 'data');

module.exports = {
    getAll: getAll,
    getList: getList,
    addToList: addToList,
    deleteFromList: deleteFromList
};

function getAll(){
    return fs.readFile(dataFilePath).then(JSON.parse);
}

// user_id의 유저의 리스트를 가져온다.
function getList(user_id){
    return fs.readFile(dataFilePath).then(data => {

        data = JSON.parse(data);
        let user = data.find(user => user.id == user_id);

        return user.list;
    });
}

// user_id의 유저의 리스트에 아이템을 추가한다.
function addToList(user_id, item){
    return fs.readFile(dataFilePath).then(data => {

        if (item) { // 일종의 데이터 검증...
            data = JSON.parse(data);
            data.find(user => user.id == user_id).list.push(item);
            data = JSON.stringify(data);

            return fs.writeFile(dataFilePath, data);
        }
    });
}

// user_id의 유저의 리스트에서 item_index번째 아이템을 삭제한다.
function deleteFromList(user_id, item_index){
    return fs.readFile(dataFilePath).then(data =>{

        data = JSON.parse(data);
        data.find(user => user.id == user_id).list.splice(item_index, 1);
        data = JSON.stringify(data);

        return fs.writeFile(dataFilePath, data);
    });
}

todolist_file/controllers/list-router.js

const express = require('express');
const bodyParser = require('body-parser');
const model = require('../models/model');
const router = express.Router();

// 할 일 리스트 보여주기
router.get('/', (req,res)=>{
    model.getList(req.session.user.id).then(list => {
        res.render('list', {
            list: list
        });
    });
});

// 할 일 추가
router.post('/', bodyParser.urlencoded(), (req,res)=>{
    model.addToList(req.session.user.id, req.body.item).then(() => {
        res.redirect('/list');  
    });
});

// 할일 데이터 지우기
router.get('/delete', (req,res)=>{
    model.deleteFromList(req.session.user.id, req.query.index).then(() => {
        res.redirect('/list');
    });
});

module.exports = router;

전체 소스코드

파일에 데이터를 읽고 쓰는 것을 model이라는 모듈로 추상화하여 데이터를 관리하고 있습니다. 잘 작동합니다만 몇가지 고려해볼 문제가 있습니다.

파일 시스템의 한계

  • 데이터의 구조적 변경이 쉽지 않음
  • 모든 유저의 데이터를 한개의 파일이 갖고 있어서 여러명이 동시에 읽고 쓸 수가 없음
    유저 별로 파일을 분리할까?
  • 아이템을 추가하고 삭제하는 작업에도 파일 전체를 읽었다가 해석했다가 다시 저장하는 비용이 큰 작업을 반복함
    버퍼링을 할까?

버퍼링(Buffering)은 다양한 의미로 쓰이는 단어입니다. 파일 시스템에서 버퍼링이란, 파일에 변경이 있을 때마다 보조기억장치에 데이터를 저장하는 작업을 수행하지 않고 (메모리의 IO와 HDD의 IO 속도는 10만배까지도 차이가 납니다), 덮어 쓸 파일의 내용이나 변경사항들을 메모리에 기억해두었다가, 여러번의 파일 변경 작업을 한꺼번에 보조기억장치에 반영하여 성능을 향상시키는 방법입니다.

이 강의를 포함한 커리큘럼
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.