개발스토리

파일 시스템 접근하기 본문

node.js

파일 시스템 접근하기

무루뭉 2020. 12. 5. 02:22
  • fs 모듈은 파일 시스템에 접근하는 모듈이다. 즉, 파일을 생성하거나 삭제하고, 읽거나 쓸 수 있다. 폴더도 만들거나 지울 수 있다.
//readme.txt
안녕하세용
//readFile.js

const fs = require('fs');

fs.readFile('./readme.txt',(err,data)=>{
    if(err){
        throw err;
    }
    console.log(data.toString()); //안녕하세용
})
  • fs 모듈을 불러온 뒤 읽을 파일의 경로를 지정한다.
    • 파일의 경로는 node 명령어를 실행하는 콘솔 기준이라는 점에 유의하자.

fs는 기본적으로 콜백 형식의 모듈이므로 실무에서 사용하기 불편하다. 따라서 fs 모듈을 프로미스 형식으로 바꿔주는 방법을 사용한다.

// readFilePromise.js

const fs = require('fs');

fs.readFile('./readme.txt')
    .then((data)=>{
      console.log(data.toString());
})
    .catch((err)=>{
      console.error(err);
});

이번에는 파일을 만들어보자.

//writeFile.js

const fs = require('fs');

fs.writeFile('./writeme.txt', '글이 입력된다.')
    .then((data)=>{
      console.log(data.toString());
})
    .catch((err)=> {
      console.error(err);
});
  • writeFile 메서드에 생성될 파일의 경로와 내용('글이 입력된다.')을 입력한다.

동기 메서드와 비동기 메서드

  • 노드는 대부분의 메서드를 비동기 방식으로 처리한다. 하지만 몇몇 메서드는 동기 방식으로도 사용할 수 있다.
    • 특히, fs 모듈이 그런 메서드를 많이 가지고 있다.

파일 하나를 여러 번 읽어보자.

// readme.txt

여러번 읽어보세용
// async.js
const fs = require('fs');

console.log('start');
fs.readFile('./readme.txt',(err,data)=>{
    if(err){
        throw err;
    }
    console.log('1번', data.toString());
});
fs.readFile('./readme.txt',(err,data)=>{
    if(err){
        throw err;
    }
    console.log('2번', data.toString());
});
fs.readFile('./readme.txt',(err,data)=>{
    if(err){
        throw err;
    }
    console.log('3번', data.toString());
});
console.log("end");
  • 같은 파일을 세 번 읽었다.

  • 결과는 의도와는 다르게 나올 것이다. 반복 실행될 때 마다 결과가 달라진다. 끝이 먼저 로그가 찍힌다.

  • 왜 일까?

    *비동기 메서드들은 백그라운드에 해당 파일을 읽으라고만 요청하고 다음 작업으로 넘어간다. 따라서 파일 읽기 요청만 세 번을 보내고 '끝'을 찍는다. 나중에 읽기가 완료되면 백그라운드가 다시 메인 스레드에 알린다. 메인 스레드는 그제서야 등록된 콜백 함수를 실행한다. *

이 방식은 상당히 좋다. 수백 개의 I/O 요청이 들어와도 메인 스레드는 백그라운드에 요청 처리를 위임한다. 그 후로도 얼마든지 요청을 더 받을 수 있다. 나중에 백그라운드가 각각의 요청 처리가 완료되었다고 알리면 그떄 콜백 함수를 처리하면 된다.

! 동기와 비동기, 블로킹과 논 블로킹

  • 동기와 비동기 : 백그라운드 작업 완료 확인 여부
  • 블로킹과 논 블로킹 : 함수가 바로 return되는 지 여부
  • 노드에서는 동기-블로킹 방식과 비동기-논 블로킹 방식이 대부분이다.
    • 동기-블로킹 : 백그라운드 작업 완료 여부를 계속 확인, 호출한 함수가 바로 return되지 않고 백그라운드 작업이 끝나야 return 된다.
    • 비동기-논 블로킹 : 호출한 함수가 바로 return되어 다음 작업으로 넘어가며, 백그라운드 작업 완료 여부는 신경쓰지 않고 나중에 백그라운드가 알림을 줄 때 비로소 처리한다.

자, 그러면 위에 코드를 순서대로 찍히게 해보자.

//sync.js

const fs = require('fs');

console.log('start');
let data = fs.readFileSync('./readme.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./readme.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./readme.txt');
console.log('3번', data.toString());
console.log('end');
  • 결과는 순서대로 출력된다.
  • 이러한 방식에는 치명적인 단점이 있다.
    • 요청이 수백 개 이상 들어올 때 성능에 문제가 생긴다. 이 전 작업이 완료되어야 마저 일을 하는데 이 말은 백그라운드가 작업하는 동안 메인 스레드는 아무것도 못한다는 뜻이다.

자, 그러면! 비동기 방식으로 순서를 유지하고 싶다면??

  • 이전 readFile의 콜백에 다음 readFile을 넣으면 된다.

    • 콜백 지옥이 펼쳐지지만 원하는대로 출력은 가능하다.
    • 콜백 지옥은 promise나 async/await으로 어느 정도 해결은 가능하다.

버퍼와 스트림

  • 앞에서 readFile 메서드를 사용할 때 읽었던 파일이 버퍼 형식으로 출력되기 때문에 toString을 사용해주었다.

  • 노드는 파일을 읽을 때 파일 크기만큼 공간을 마련해두어 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 한다. 이때 메모리에 저장된 데이터가 바로 버퍼이다.

  • 버퍼를 직접 다룰 수 있는 클래스가 바로 Buffer이다.

    • from(문자열) : 문자열을 버퍼로 바꾼다. length 속성은 버퍼의 크기를 바이트 단위로 알린다.
    • toString(버퍼) : 버퍼를 다시 문자열로 바꾼다. 이때 base64나 hex를 인수로 넣으면 해당 인코딩 변환
    • concat(배열) : 배열 안에 든 버퍼들을 하나로 합친다.
    • alloc(바이트) : 빈 버퍼를 생성한다. 바이트를 인수로 넣으면 해당 크기의 버퍼가 생성된다.
  • readFile 방식의 버퍼가 편리하기는 하지만 문제점도 있다.

    • 만약 용량인 100MB인 파일이 있으면 읽을 때 메모리에 100MB 버퍼를 만들어야 한다. 크기가 커지면 메모리 문제가 발생한다.
    • 모든 내용을 버퍼에 다 쓴 후에야 다음 동작으로 넘어가므로 파일 읽기, 압축, 쓰기 등의 조작을 연달아 할 때 매번 전체 용량을 처리해야 다음 단계로 넘어갈 수 있다.
  • 그래서 버퍼의 크기를 작게 만든 후 여러번으로 나눠 보내는 방식이 등장했다.

    • 예를 들면, 버퍼 1MB를 만든 후 100MB 파일을 백 번에 걸쳐서 나눠 보내는 것이다.
    • 이를 편리하게 만든 것이 스트림이다.

스레드풀 알아보기

  • 이전 절에서 파일 시스템 실습을 하면서 fs 모듈의 비동기 메서드들을 사용해봤다.
  • 비동기 메서드들은 백그라운드에서 실행되고, 실행된 후에는 다시 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행된다. 이때 fs 메서드를 여러 번 실행해도 백그라운드에서 동시에 처리되는데, 바로 스레드풀이 있기 때문이다.
  • fs 외에도 내부적으로 스레드풀을 사용하는 모듈은 crypto, zlib, dns.lookup 등이 있다.
  • 실행할 때마다 시간과 순서가 달라진다. 스레드풀이 작업을 동시에 처리하므로 여러 작업 중에서 어느 것이 먼저 처리될 지 모른다.

우리는 스레드풀을 직접 컨트롤할 수는 없지만 개수를 조절할 수 있다.

윈도우라면 SET UV_THREADPOOL_SIZE = 1을, 맥과 리눅스라면 터미널에 UV_THREADPOOL_SIZE = 1을 입력하면 작업이 순서대로 실행된다. 스레드풀 개수를 하나로 제한했으므로 작업이 한 번에 하나씩 처리된다.

예외처리

  • 예외 처리는 정말 중요하다. 예외들은 실행 중인 노드 프로세스를 멈추게 한다.

에러가 발생할 것 같은 부분을 try/catch문으로 감싸면 된다.

  • 에러는 발생하지만 try/catch로 잡을 수 있고 직접 멈추기 전까지 계속 실행된다.

  • 에러가 발생했을 때 에러를 throw하면 노드 프로세스가 멈춘다. 따라서 throw하는 경우에는 반드시 try/catch문으로 throw한 에러를 잡아야 한다.

 

 

 

본 내용은 Node.js 교과서 (길벗 출판사) 개정 2판의 내용을 기반으로 작성되었습니다.

'node.js' 카테고리의 다른 글

REST와 라우팅  (0) 2020.12.10
요청과 응답  (0) 2020.12.10
노드 내장 객체  (0) 2020.12.05
노드 기능  (0) 2020.12.05
프론트엔드 자바스크립트  (0) 2020.12.04
Comments