프로그래밍/NODEJS 강좌 BY GEMINI

콜백 함수 (Callbacks)와 콜백 지옥 (Callback Hell) 😫

lazy_web_devloper 2025. 6. 5. 14:20
728x90
반응형

콜백 함수 (Callbacks)와 콜백 지옥 (Callback Hell) 😫

1. 콜백 함수 (Callbacks)란?

비동기 프로그래밍에서 콜백 함수는 특정 작업이 완료된 후에 실행되도록 다른 함수의 인자로 전달되는 함수를 말합니다. "나중에 호출해줘(call me back)"라는 의미로 생각하시면 쉽습니다.

Node.js의 많은 비동기 함수들은 마지막 인자로 콜백 함수를 받습니다. 이 콜백 함수는 비동기 작업이 성공적으로 완료되었을 때 그 결과를 가지고 호출되거나, 작업 중 오류가 발생했을 때 에러 정보를 가지고 호출됩니다.

Node.js 콜백 함수의 일반적인 형태 (Error-first callback pattern):

Node.js에서는 콜백 함수의 첫 번째 인자로 에러 객체(error 또는 err)를 전달하고, 두 번째 인자부터 실제 결과 데이터를 전달하는 컨벤션(관례)을 널리 사용합니다. 이를 Error-first callback pattern 이라고 부릅니다.

JavaScript
 
function 비동기함수(파라미터1, 파라미터2, (err, data) => {
  // 1. 에러 처리 우선
  if (err) {
    // 에러가 발생했을 경우 처리 로직
    console.error('에러 발생:', err);
    return; // 에러 발생 시 함수 실행 중단
  }

  // 2. 성공 시 데이터 처리
  // 에러가 없다면 (err === null 또는 undefined) 성공적으로 데이터를 받아 처리
  console.log('데이터:', data);
  // ... 데이터를 사용하는 로직 ...
});

예시: fs.readFile 사용

앞서 fs 모듈에서 보았던 fs.readFile 함수가 대표적인 콜백 함수 사용 예입니다.

JavaScript
 
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.error('파일을 찾을 수 없습니다.');
    } else {
      console.error('파일 읽기 중 오류 발생:', err);
    }
    return;
  }
  console.log('파일 내용:');
  console.log(data);

  // 여기서 파일 내용을 가지고 다음 작업을 수행할 수 있습니다.
  // 예를 들어, 읽은 내용을 다른 파일에 쓰는 작업 등
});

console.log('파일 읽기를 요청했습니다. (이 메시지가 먼저 출력될 수 있습니다)');

위 코드에서 fs.readFile은 파일 읽기를 시작하고 즉시 다음 라인(console.log('파일 읽기를 요청했습니다...'))으로 넘어갑니다. 파일 읽기가 완료되면 (성공하든 실패하든) 나중에 전달된 콜백 함수 (err, data) => { ... }가 실행됩니다.

Java 개발자를 위한 비유:

Java에서는 인터페이스를 정의하고 익명 클래스나 람다식을 사용하여 콜백과 유사한 패턴을 구현할 수 있습니다. 예를 들어, 스윙(Swing)에서 이벤트 리스너를 등록하는 경우가 비슷합니다.

Java
 
// Java 예시 (간단한 비유)
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // 버튼 클릭 시 실행될 로직 (콜백)
        System.out.println("Button clicked!");
    }
});

// 람다 사용
button.addActionListener(e -> System.out.println("Button clicked!"));

Node.js의 콜백은 이러한 이벤트 처리나 비동기 작업 완료 후 로직 실행을 위한 일반적인 패턴입니다.

2. 콜백 지옥 (Callback Hell) / 파멸의 피라미드 (Pyramid of Doom)

콜백 함수는 비동기 로직을 처리하는 간단하고 직접적인 방법이지만, 여러 개의 비동기 작업이 순차적으로 또는 의존적으로 실행되어야 할 때 문제가 발생할 수 있습니다. 즉, 한 비동기 작업의 결과가 다음 비동기 작업의 입력으로 사용되는 경우, 콜백 함수 안에 또 다른 콜백 함수를 중첩해서 호출하게 됩니다. 이러한 코드가 깊어지면 다음과 같은 형태가 됩니다.

JavaScript
 
비동기작업1((err1, data1) => {
  if (err1) {
    console.error('에러1:', err1);
    return;
  }
  // data1을 사용하여 비동기작업2 호출
  비동기작업2(data1, (err2, data2) => {
    if (err2) {
      console.error('에러2:', err2);
      return;
    }
    // data2를 사용하여 비동기작업3 호출
    비동기작업3(data2, (err3, data3) => {
      if (err3) {
        console.error('에러3:', err3);
        return;
      }
      // data3을 사용하여 비동기작업4 호출
      비동기작업4(data3, (err4, data4) => {
        if (err4) {
          console.error('에러4:', err4);
          return;
        }
        // 모든 작업 성공!
        console.log('최종 결과:', data4);
      });
    });
  });
});

이처럼 코드가 오른쪽으로 계속 들여쓰기 되면서 피라미드 모양을 이루는 것을 콜백 지옥 (Callback Hell) 또는 파멸의 피라미드 (Pyramid of Doom) 라고 부릅니다.

콜백 지옥의 문제점:

  • 가독성 저하: 코드를 읽고 이해하기 매우 어렵습니다. 로직의 흐름을 파악하기 힘듭니다.
  • 디버깅 어려움: 에러가 발생했을 때 어느 부분에서 문제가 생겼는지 추적하기 어렵습니다. 각 콜백마다 에러 처리를 반복해야 합니다.
  • 유지보수성 악화: 코드 수정이나 기능 추가가 매우 복잡하고 오류를 유발하기 쉽습니다.
  • 제어 흐름의 복잡성: 조건 분기나 반복문 등을 콜백과 함께 사용하면 코드가 더욱 얽히게 됩니다.

콜백 지옥 해결을 위한 노력 (간략 소개):

콜백 지옥은 Node.js 초기부터 개발자들을 괴롭혔던 문제이며, 이를 해결하기 위해 다양한 방법들이 제시되었습니다.

  • 함수 분리: 중첩된 콜백 함수들을 각각 이름 있는 함수로 분리하여 코드의 깊이를 줄이는 방법. 가독성은 다소 향상되지만, 근본적인 비동기 제어 흐름의 복잡성을 해결하지는 못할 수 있습니다.
  • 제어 흐름 라이브러리 사용: async (NPM 패키지, async.js)와 같은 라이브러리는 series, parallel, waterfall 등의 유틸리티 함수를 제공하여 콜백 기반의 비동기 작업들을 더 체계적으로 관리할 수 있게 도와줍니다.
  • 프로미스 (Promises) 사용: ES6(ECMAScript 2015)에 표준으로 도입된 Promise 객체는 비동기 작업의 최종 성공 또는 실패를 나타내는 객체입니다. .then()과 .catch()를 사용하여 콜백 중첩을 줄이고 비동기 코드를 더 선형적으로 작성할 수 있게 해줍니다. (다음 섹션에서 자세히 다룹니다.)
  • Async/Await 사용: ES2017(ES8)에 도입된 async 함수와 await 키워드는 프로미스를 기반으로 동작하며, 비동기 코드를 마치 동기 코드처럼 보이게 작성할 수 있게 해줍니다. 가독성과 유지보수성을 크게 향상시키는 현대적인 비동기 처리 방식입니다. (프로미스 이후 섹션에서 자세히 다룹니다.)

콜백 함수는 Node.js 비동기 프로그래밍의 기본이지만, 그 한계점(콜백 지옥) 때문에 프로미스나 async/await와 같은 더 발전된 패턴들이 등장하게 되었습니다.

728x90
반응형