서버/Node.js

Node.js 비동기 프로그래밍 가이드 (1)

JohnnyDeveloper 2024. 7. 28. 18:35

Node.js에서 비동기 프로그래밍의 기본 원리와 최신 기술을 설명합니다.
비동기 함수, 프로미스, async/await 등의 개념을 다룹니다.

 

1. 비동기 프로그래밍이란?

비동기 프로그래밍은 시간이 오래 걸리는 작업이 다른 작업을 방해하지 않도록 하는 프로그래밍 방식입니다. 예를 들어, 친구에게 문자를 보내놓고 답장을 기다리기보다는, 답장이 올 때까지 다른 일을 하는 것과 같습니다.

1.1 콜백 함수

콜백 함수는 어떤 작업이 끝났을 때 호출되는 함수입니다. 예를 들어, 친구에게 문자를 보내고, 답장이 오면 답장을 읽는 것처럼, 특정 작업이 끝나면 그 결과를 처리하는 함수입니다.

function sendMessage(message, callback) {
  console.log("Sending message: " + message);
  setTimeout(() => {
    console.log("Message sent!");
    callback();
  }, 1000); // 1초 후에 콜백 함수 호출
}

sendMessage("Hello!", () => {
  console.log("Message received!");
});

 

위 예제에서 sendMessage 함수는 메시지를 보내고, 1초 후에 콜백 함수를 호출합니다.

1.2 콜백 지옥

하지만 콜백 함수가 많아지면 코드가 복잡해져서 일명 '콜백 지옥'에 빠질 수 있습니. 이렇게 되면 코드가 읽기 어렵고 관리하기 힘들어집니다.

doTask(function(result) {
  doNextTask(result, function(newResult) {
    doAnotherTask(newResult, function(finalResult) {
      console.log(finalResult);
    });
  });
});

 

이런 구조를 피하기 위해 프로미스와 async/await 같은 개념이 등장했습니다.

1.3 프로미스(Promise)

프로미스는 비동기 작업의 결과를 나타내는 객체입니다. 프로미스는 세 가지 상태를 가질 수 있습니다: pending(대기), fulfilled(이행됨), rejected(거부됨). 이는 마치 친구에게 문자를 보냈을 때, 친구가 답장을 보내거나 보내지 않거나, 답장 내용을 기다리는 것과 같습니다.

function sendMessagePromise(message) {
  return new Promise((resolve, reject) => {
    console.log("Sending message: " + message);
    setTimeout(() => {
      if (message === "Hello!") {
        console.log("Message sent!");
        resolve("Message received!");
      } else {
        reject("Message not sent.");
      }
    }, 1000);
  });
}

sendMessagePromise("Hello!")
  .then(response => {
    console.log(response);
  })
  .catch(error => {
    console.error(error);
  });

 

여기서 sendMessagePromise 함수는 메시지를 보내고, 메시지가 "Hello!"일 때만 성공적으로 메시지를 받았다고 처리합니다. 다른 메시지인 경우에는 오류가 발생합니다.

1.4 async/await

async/await는 프로미스를 더 쉽게 다루기 위한 최신 문법입니다. async 함수는 항상 프로미스를 반환하며, await 키워드는 프로미스가 해결될 때까지 기다립니다. 이를 통해 비동기 코드를 동기 코드처럼 읽기 쉽게 만들 수 있습니다.

async function sendMessageAsync(message) {
  try {
    console.log("Sending message: " + message);
    let response = await sendMessagePromise(message);
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

sendMessageAsync("Hello!");

 

위 코드에서 sendMessageAsync 함수는 sendMessagePromise를 호출하고, 프로미스가 해결될 때까지 기다립니다. 이로 인해 코드가 더욱 직관적이고 읽기 쉽게 변합니다.


2. Node.js에서 비동기 프로그래밍의 장점

 

Node.js에서 비동기 프로그래밍을 사용하는 주요 이유는 성능 최적화자원 효율성입니다. 특히 서버 환경에서 많은 동시 요청을 처리할 때 비동기 프로그래밍의 장점이 두드러집니다.

2.1 높은 동시성 처리

Node.js의 비동기 모델은 싱글 스레드 이벤트 루프를 사용하여 많은 클라이언트 요청을 동시에 처리할 수 있습니다. 이는 서버가 한 번에 많은 요청을 처리할 수 있도록 하여 응답 시간을 줄이고 사용자 경험을 개선합니다.

2.2 서버 자원 절약

비동기 프로그래밍을 통해 I/O 작업이 완료될 때까지 대기하지 않고 다른 작업을 계속 수행할 수 있습니다. 이는 CPU 자원의 효율적인 사용을 가능하게 하며, 서버의 부하를 줄이는 데 기여합니다.

2.3 코드 가독성 향상

async/await 문법을 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 크게 향상됩니다. 이는 복잡한 비동기 흐름을 이해하기 쉽게 만들고, 코드 유지보수를 용이하게 합니다.

2.4 예제: 비동기 데이터베이스 쿼리

아래는 async/await를 사용하여 데이터베이스에서 비동기적으로 데이터를 조회하는 예제입니다. 이 예제는 데이터베이스에 대한 비동기 요청과 응답을 처리하는 방법을 보여줍니다.

const { Client } = require('pg');

async function fetchData() {
  const client = new Client({
    user: 'your-username',
    host: 'your-database-host',
    database: 'your-database-name',
    password: 'your-password',
    port: 5432,
  });

  try {
    await client.connect();
    const res = await client.query('SELECT * FROM your_table');
    console.log(res.rows);
  } catch (err) {
    console.error('Error executing query', err.stack);
  } finally {
    await client.end();
  }
}

fetchData();

이 예제에서는 PostgreSQL 클라이언트를 사용하여 데이터베이스에 연결하고, 데이터를 조회한 후 결과를 출력합니다. await 키워드를 사용하여 각 비동기 작업이 완료될 때까지 기다리며, 에러가 발생할 경우 이를 catch 블록에서 처리합니다.


3. 비동기 오류 처리

 

콜백 기반의 비동기 코드에서는 함수의 매개변수로 오류와 결과를 전달하는 방식이 일반적입니다. 이 방식은 오류 발생 시 콜백 내에서 오류를 처리할 수 있게 합니다.

3.1.1 콜백 패턴의 기본 예제

다음은 콜백 패턴에서의 오류 처리 기본 예제입니다:

function doSomethingAsync(callback) {
  setTimeout(() => {
    const error = false; // 오류가 발생하면 true로 설정
    if (error) {
      callback('Error occurred');
    } else {
      callback(null, 'Success');
    }
  }, 1000);
}

doSomethingAsync((err, result) => {
  if (err) {
    console.error('Error:', err);
  } else {
    console.log('Result:', result);
  }
});

 

이 예제에서 doSomethingAsync 함수는 비동기 작업을 수행하고, 작업이 완료되면 콜백을 호출합니다. 오류가 발생하면 콜백의 첫 번째 매개변수로 오류 메시지를 전달하고, 그렇지 않으면 결과를 전달합니다.

 

3.1.2 콜백 지옥과 오류 처리의 어려움

콜백 기반의 오류 처리 방식은 코드의 가독성과 유지보수를 어렵게 만들 수 있습니다. 특히, 콜백이 중첩되면 '콜백 지옥'이라고 불리는 문제에 직면할 수 있습니다. 이를 피하기 위해 프로미스나 async/await 같은 방법을 사용하여 코드를 구조화하는 것이 좋습니다.

 

3.1.3 프로미스에서의 오류 처리

프로미스는 비동기 작업의 결과를 표현하는 객체로, 콜백의 중첩을 피하고 오류 처리를 더 체계적으로 할 수 있습니다. 프로미스의 오류는 .catch() 메서드를 사용하여 처리할 수 있습니다.

프로미스 오류 처리 예제

function doSomethingWithPromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const error = false; // 오류 발생 여부
      if (error) {
        reject('Error occurred in promise');
      } else {
        resolve('Promise resolved successfully');
      }
    }, 1000);
  });
}

doSomethingWithPromise()
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    console.error('Caught error:', error);
  });

 

이 예제에서 doSomethingWithPromise 함수는 프로미스를 반환하며, 오류가 발생하면 reject를 호출하고, 성공 시 resolve를 호출합니다. .catch() 메서드는 프로미스 체인에서 발생한 오류를 처리합니다.

 

3.1.4 오류 전파와 일관된 오류 처리

프로미스 체인에서 오류가 발생하면, 해당 오류는 .catch() 메서드가 호출될 때까지 전파됩니다. 이를 통해 여러 프로미스에서 발생한 오류를 일관되게 처리할 수 있습니다.

function step1() {
  return new Promise((resolve, reject) => {
    // 비동기 작업
    resolve('Step 1 complete');
  });
}

function step2() {
  return new Promise((resolve, reject) => {
    // 비동기 작업 중 오류 발생
    reject('Error in Step 2');
  });
}

step1()
  .then(result => {
    console.log(result);
    return step2();
  })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Error caught:', error);
  });

 

이 코드에서 step2 함수에서 발생한 오류는 .catch() 메서드에서 잡힙니다. 이렇게 하면 비동기 작업의 모든 단계에서 발생하는 오류를 한 곳에서 처리할 수 있습니다.

3.2.1 async/await에서의 오류 처리

async/await는 프로미스를 사용하는 비동기 코드를 더욱 읽기 쉽게 만들어줍니다. 이와 함께 try/catch 블록을 사용하여 오류를 처리할 수 있습니다.

async function asyncFunction() {
  try {
    let result = await doSomethingWithPromise();
    console.log('Result:', result);
  } catch (error) {
    console.error('Caught error:', error);
  }
}

asyncFunction();

 

이 예제에서 asyncFunction은 async 키워드를 사용하여 정의된 비동기 함수입니다. await 키워드는 프로미스가 해결될 때까지 대기하며, try/catch 블록은 발생하는 오류를 잡아 처리합니다.

3.2.2 다중 비동기 작업에서의 오류 처리

여러 개의 비동기 작업을 처리할 때, 각 작업의 오류를 개별적으로 처리하거나, 모든 작업이 완료된 후에 오류를 처리할 수 있습니다.

 

개별 오류 처리 예제

async function multipleAsyncTasks() {
  try {
    let result1 = await task1();
    console.log('Task 1 result:', result1);
  } catch (error) {
    console.error('Task 1 error:', error);
  }

  try {
    let result2 = await task2();
    console.log('Task 2 result:', result2);
  } catch (error) {
    console.error('Task 2 error:', error);
  }
}

 

모든 작업 후의 오류 처리 예제:

async function multipleAsyncTasks() {
  try {
    let [result1, result2] = await Promise.all([task1(), task2()]);
    console.log('Task 1 result:', result1);
    console.log('Task 2 result:', result2);
  } catch (error) {
    console.error('Error in tasks:', error);
  }
}

 

첫 번째 방법은 각 작업의 오류를 개별적으로 처리하고, 두 번째 방법은 모든 작업이 완료된 후에 발생한 오류를 처리합니다.

 

다음 글: Node.js 비동기 프로그래밍 가이드(2)