프로그래밍C++ 백엔드 개발자

C++에서 예외 처리 시 스택 언와인딩(stack unwinding)과 객체 파괴(destruction) 간의 차이점을 설명하시오. 예외 발생 시 자원의 올바른 해제를 보장하려면 어떻게 해야 하며?

Hintsage AI 어시스턴트로 면접 통과

답변.

C++에서 예외 처리는 스택 언와인딩 메커니즘을 동반하며, 이는 예외 발생 이전에 호출 스택에 생성된 지역 객체들의 자동 파괴(소멸자 호출)를 포함합니다. 이 현상은 자원의 올바른 해제를 위해 중요합니다.

문제의 배경

C++ 설계 초기 단계에서 내장 쓰레기 수집기가 부족하여 자원의 해제(메모리, 파일, 소켓 등)는 프로그래머에게 맡겨졌습니다. 예외 처리는 오류로 인해 코드가 롤백될 때 올바른 해제를 보장해야 했습니다.

문제점

예외 발생 시 객체 자동 파괴 메커니즘이 없다면 자원 누수가 발생합니다. 각 catch 블록에서 수동으로 자원을 해제하지 않으면 코드가 복잡하고 신뢰할 수 없게 됩니다.

해결책

C++는 예외를 던질 때 호출 스택에서 throw 이전의 영역에 있는 모든 객체가 생성된 반대 순서로 파괴되는 스택 언와인딩을 구현합니다. 이들의 소멸자는 자동으로 호출됩니다.

자원을 지속적으로 해제하는 고전적인 방법은 RAII(자원 획득은 초기화다) 패턴을 사용하는 것입니다: 모든 자원은 객체에 감싸져 있으며, 이 객체가 파괴될 때 소유한 자원을 해제합니다.

코드 예시:

#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("파일을 열 수 없습니다"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // throw가 발생하면 롤백됩니다. // ... 파일 작업 throw std::runtime_error("일부 오류"); // fclose는 자동으로 호출됩니다. }

주요 특징들:

  • 스택 언와인딩은 스택의 모든 객체의 소멸자를 자동으로 호출합니다.
  • RAII는 예외 발생 여부와 관계없이 자원 해제를 보장합니다.
  • catch에서 자원을 수동으로 해제할 필요가 드물며 소멸자가 이를 더 잘 처리합니다.

함정이 있는 질문들.

단순히 try-catch를 사용하고 catch 블록에서 delete나 fclose를 수동으로 호출해서 누수를 방지할 수 없나요?

답변:

이것은 불편하고 신뢰할 수 없습니다: 여러 출구 지점이나 중첩 자원이 있을 때 자원을 닫는 것을 잊기 쉽습니다. 소멸자는 함수에서 예외가 "통과"되더라도 호출됩니다. catch는 필요하지 않습니다.

스택 언와인딩 중에 힙(heap)에 있는 객체가 파괴될까요? 이 객체들이 스택의 객체에 "감싸지지" 않는다면요?

답변:

아니요. 스택 언와인딩은 스택에 위치한 객체에 대해서만 소멸자를 호출합니다. 힙 객체가 올바르게 파괴되려면 스택의 객체가 이를 소유해야 합니다(예: 스마트 포인터를 통해).

스택 언와인딩 중 소멸자가 또 다른 예외를 던지면 어떻게 되나요?

답변:

스택 언와인딩 중 어떤 객체가 파괴되는 과정에서 두 번째 예외가 발생하면 프로그램은 std::terminate()를 호출하며 종료됩니다. 소멸자에서 예외를 던지지 마십시오!

일반적인 실수와 반패턴

  • 소멸자에서 자원을 해제하지 않고 catch에서 수동 관리를 신뢰하는 것.
  • 소멸자에서 예외 던지기.
  • 힙 자원 관리를 위해 스마트 포인터를 사용하지 않기.
  • 중첩 자원을 잊기(예: 구조체 내의 파일).

실제 사례

부정적인 경우

개발자가 RAII를 사용하지 않고 catch 블록을 통해 파일을 수동으로 닫고 메모리를 해제합니다.

장점:

  • 자원을 명시적으로 관리합니다.

단점:

  • catch를 지나쳐 throw가 발생하면 누수를 피할 수 없습니다.
  • 코드를 유지보수하고 수정하기 어렵고 해제를 잊기 쉽습니다.

긍정적인 경우

파일과 자원이 RAII 래퍼 클래스로 감싸져 있습니다. 결과: 해제는 항상 소멸자에서 발생하며, throw/catch와 관계없이 이루어집니다.

장점:

  • 자원 해제의 신뢰성.
  • 코드 양이 적고 유지보수가 쉽습니다.

단점:

  • RAII 래퍼를 작성할 수 있어야 합니다.
  • 새로운 유형의 자원 추가 시 새로운 클래스가 필요합니다.