비동기 처리 async & await in Node.js

­문재원·2024년 1월 31일

Node.js

목록 보기
2/2

오늘은 Node.js를 공부하려면 꼭 알아야하는 기능인 비동기처리에 대해서 알아보고자 한다.
우선 간략한 목차는 다음과 같다.

목차

  1. 동기 vs. 비동기
  2. Node.js에서 비동기처리를 사용해야하는 이유
  3. 비동기처리의 발전과정
  4. async & await
  5. 비동기처리의 효과

1. 동기 vs. 비동기

동기(synchronous):

동시에 일어난다는 의미로 하나의 요청이 들어오면 결과가 나온 후 다음 요청이 들어오고 결과가 나온다. 이렇게 요청이 순차적으로 처리된다.

비동기(asynchronous):

병렬적으로 처리된다는 의미로 여러 요청을 동시에 처리한다.

2. Node.js에서 비동기처리를 사용해야하는 이유

Node.js는 요청을 멀티 스레드로 처리하는 것이 아니라 병렬로 처리한다. 즉 Node.js는 비동기 IO(Input&Output)을 지원하고 싱글 스레드를 기반으로 동작한다.(*싱글 스레드는 처리시간이 긴 작업의 경우 시간이 많이 멀티 스레드에 비해 늘어날 수 있다는 단점이 있다.)

그렇다면 Node.js가 병렬로 처리하기 위해서 어떤 방법을 사용할까?

이벤트 방식으로 처리한다.

Node.js를 공부하다보면 한 번쯤은 접했을 개념인 이벤트 방식에서는

1) 요청을 비동기로 처리하기 위해 이벤트를 발생시키고
2) 서버 내부에 메세지 형태로 전달한다.

이후 서버 내부에서 이벤트 루프가 메세지를 처리한다.
3) 이렇게 내부에서 메세지를 처리하는 동안 제어권이 다음 요청으로 넘어간다.
4) 처리가 완료되면 Callback을 호출해 처리완료를 호출 측에 알려준다.

이처럼 Node.js의 기본적인 작업 방식이 병렬적으로 처리하는 것이기 때문에 해당 서버에서는 비동기처리를 사용하는 것이 보다 효과적이다.

3. 비동기처리의 발전과정

비동기처리는 ES가 발전함에 따라 같이 발전해왔다. 나는 크게 4가지 형태를 살펴볼 예정이다.

1) Callback

2) Promise & then & catch

3) async & await

1) Callback 함수는 비동기 작업이 끝나고 난 뒤 실행되는 함수이다.

내가 느끼기에 '지옥의 연속성'이라는 단점이 있다. 말 그대로 callback을 사용하게 되면 덧붙여서 계속 사용하는 경우가 많은데 이는 코드의 가독성을 떨어뜨린다는 단점이 있다.

아래는 콜백 함수의 예제이다.

const fs = require('fs');

// 파일 읽기 함수
function readFileContent(filePath, callback) {
    fs.readFile(filePath, 'utf8', (err, data) => {
        if (err) {
            // 에러가 발생한 경우 콜백 함수에 에러를 전달
            callback(err, null);
            return;
        }

        // 정상적으로 파일을 읽은 경우 콜백 함수에 데이터를 전달
        callback(null, data);
    });
}

// 예제 파일 경로
const filePath = 'example.txt';

// 파일 읽기 함수 호출
readFileContent(filePath, (err, data) => {
    if (err) {
        console.error('파일 읽기 에러:', err);
        return;
    }

    console.log('파일 내용:', data);
});

console.log('파일 읽기 요청을 기다립니다...');

2) Promise & then & catch

promise 객체는 콜백함수의 단점을 보완하기 위해 등작했으며 기본 형태는
Promise(resolve, reject)이다.
아래 예제는 파일 읽기를 promise를 이용해 실행한 것이다.

const fs = require('fs/promises');

// Promise를 사용한 파일 읽기 함수
function readFileContent(filePath) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, 'utf8')
            .then((data) => {
                resolve(data); // 파일을 성공적으로 읽었을 때 resolve 호출
            })
            .catch((err) => {
                reject(err); // 에러가 발생했을 때 reject 호출
            });
    });
}

// 예제 파일 경로
const filePath = 'example.txt';

// Promise를 사용한 파일 읽기 함수 호출
readFileContent(filePath)
    .then((data) => {
        console.log('파일 내용:', data);
    })
    .catch((err) => {
        console.error('파일 읽기 에러:', err);
    });

console.log('파일 읽기 요청을 기다립니다...');

이때 resolve는 주어진 일을 성공했을 때, reject는 실패했을 때 호출된다.
또한 promise를 반환하는 함수는 then과 catch를 통해 결과와 에러를 처리할 수 있다.

하지만 promise then을 연속해서 사용할 경우 가독성이 떨어진다는 단점이 있다.

3) async & await

async와 await은 앞선 promise를 보완하기 위해 등장했다. then과 catch를 사용하지 않는 대신 async와 await을 사용해 코드의 가독성을 높히고 코드를 보다 간결하게 만들어준다.

4. async & await

async
async는 function 앞에 덧붙여 비동기로 처리할 것이라고 알려준다.

await
await은 비동기로 사용할 함수의 왼쪽에 명시한다.
그리고 Promise 비동기 처리가 완료될 때 까지 코드 실행을 일시 중지하고 wait 하는 역할을 한다.

아래 예제는 관련 코드이다.

const fs = require('fs/promises');

// async 함수를 사용한 파일 읽기 함수
async function readFileContent(filePath) {
    try {
        const data = await fs.readFile(filePath, 'utf8');
        return data;
    } catch (err) {
        throw err;
    }
}

// 예제 파일 경로
const filePath = 'example.txt';

// async 함수를 사용한 파일 읽기 함수 호출
async function main() {
    try {
        const data = await readFileContent(filePath);
        console.log('파일 내용:', data);
    } catch (err) {
        console.error('파일 읽기 에러:', err);
    }
}

// main 함수 호출
main();

console.log('파일 읽기 요청을 기다립니다...');

해당 코드는 파일 읽기를 async와 await을 사용해 비동기처리하는 과정으로 앞선 예제들과 같은 작업을 수행한다. 그러나 앞선 예제들과 비교했을 때 async와 await을 사용한 비동기처리 코드가 훨씬 간결함을 알 수 있다.

또한 이 코드가 비동기처리로 수행된다는 점을 고려했을 때 main함수에서 출력하는 파일 내용: 'data에 해당하는 부분' 보다 '파일 읽기 요청을 기다립니다...'가 먼저 출력된다는 점을 짐작할 수 있다.

5.비동기처리의 효과

그렇다면 Node.js는 왜 비동기처리 방식을 택했으며 비동기로 처리하기 위해 콜백함수부터 async&await에 이르기까지 다양한 발전을 거듭해왔을까?

그 이유는 첫번째 목차에서 설명한 것처럼 메인스레드의 단점을 보완하기 위해서이다. 메인스레드에서는 요청을 여러 스레드에서 병렬적으로 처리하므로 동시 접속자가 많을 경우 그만큼 스레드의 사용이 많아지며 이에 따라 메모리 자원도 많이 사용된다. 그럼 메인 스레드의 기능이 저하된다.

이와 달리 Node.js는 병렬처리를 thread가 아닌 이벤트 루프에서 하기 때문에 멀티 스레드의 단점을 보완할 수 있다. 즉 요청을 받는 곳과 요청을 처리하는 곳이 달라 효과적으로 동작할 수 있다.

Node.js의 비동기처리는 현재까지 나온 async&await 을 비롯해 발전을 거듭할 것이다. Node.js을 사용하는 개발자라면 이러한 비동기처리 방식의 발전에 지속적인 관심을 가지면 좋을 것 같다.

profile
얼렁뚱땅 요리조리

0개의 댓글