[2026] C++ 예외 안전성 | 예외 발생 시 리소스 누수 Basic·Strong·Nothrow 보장

[2026] C++ 예외 안전성 | 예외 발생 시 리소스 누수 Basic·Strong·Nothrow 보장

이 글의 핵심

C++ 예외 안전성의 C++, 안전성, 예외, 들어가며: 예외 발생 시 메모리가 샜다를 실전 예제와 함께 상세히 설명합니다.

들어가며: 예외 발생 시 메모리가 샜다

“try-catch를 썼는데 왜 메모리 누수가 생기죠?”

파일 처리 코드에 예외 처리를 추가했습니다. 하지만 예외 발생 시 메모리 누수가 생겼습니다. 문제의 코드에서는 new char[1024]로 버퍼를 할당한 뒤, 파일 열기 실패 시 throw로 예외를 던집니다. 예외가 발생하면 delete[] buffer에 도달하지 않고 함수가 스택 언와인딩으로 종료되므로, 그때까지 할당된 buffer는 해제되지 않아 메모리 누수가 됩니다. new/delete를 직접 쓰면 이런 “예외 경로”에서 해제를 빠뜨리기 쉽기 때문에, 버퍼도 std::unique_ptr<char[]> 같은 RAII로 감싸면 예외가 나도 소멸자에서 delete[]가 호출됩니다. 당시에는 “한 번만 throw하는데”라고 생각했지만, 나중에 processFile 안에서 호출하는 다른 함수가 예외를 던지게 되면서 누수가 재현되었습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void processFile(const std::string& path) {
    char* buffer = new char[1024];  // 메모리 할당
    
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Cannot open file");
        // ❌ buffer가 해제 안 됨!
    }
    
    // 파일 처리...
    
    delete[] buffer;  // 정상 경로에서만 실행됨
}

위 코드 설명: new char[1024]로 할당한 buffer는 예외가 나면 delete[] buffer에 도달하지 못합니다. throw 이후 함수가 스택 언와인딩으로 종료되기 때문에 수동 해제 코드가 실행되지 않아 메모리 누수가 발생합니다. 리소스는 반드시 RAII로 관리해야 예외 경로에서도 안전합니다. 원인:

  • 예외가 던져지면 함수가 즉시 종료
  • delete[] buffer에 도달하지 못함
  • 메모리 누수 발생 실무 정리: 예외 안전성을 보장하려면 “리소스는 RAII로만 관리”하는 것이 가장 단순합니다. raw new/delete나 수동 fclose 같은 코드가 있으면 예외 경로에서 해제가 누락되기 쉽고, std::unique_ptr, std::ifstream, std::lock_guard 등은 소멸자에서 정리되므로 예외가 나도 누수가 발생하지 않습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o safe_file safe_file.cpp && ./safe_file
#include <memory>
#include <fstream>
#include <stdexcept>
#include <string>
#include <iostream>
void processFile(const std::string& path) {
    auto buffer = std::make_unique<char[]>(1024);  // RAII
    std::ifstream file(path);  // RAII
    if (!file) {
        throw std::runtime_error("Cannot open file");
        // ✅ buffer와 file 소멸자가 자동 호출됨!
    }
    // 파일 처리...
}
int main() {
    try {
        processFile("nonexistent.txt");
    } catch (const std::exception& e) {
        std::cerr << e.what() << "\n";
    }
    return 0;
}

위 코드 설명: std::make_unique<char[]>(1024)std::ifstream은 생성 시 리소스를 획득하고, 스코프를 벗어날 때(예외 포함) 소멸자에서 자동으로 해제합니다. throw가 나도 buffer와 file이 역순으로 정리되므로 누수가 발생하지 않습니다. 실행 결과: Cannot open file(또는 해당 예외 메시지)가 stderr에 출력됩니다. 이 경험으로 예외 안전성의 중요성을 깨달았습니다.
예외가 나면 함수가 중간에 빠져나가므로, 그 전에 획득한 리소스(메모리, 파일, 락)는 반드시 소멸자나 catch에서 정리되어야 합니다. RAII로 “획득은 생성자, 해제는 소멸자”에 묶어 두면 예외가 나도 스코프를 벗어날 때 자동으로 정리되므로, 이 글에서 다루는 basic/strong/nothrow 보장을 실전에서 적용하는 데 도움이 됩니다. 이 글을 읽으면:

  • 예외 안전성의 세 가지 수준을 이해할 수 있습니다.
  • RAII와 예외를 안전하게 결합하는 방법을 알 수 있습니다.
  • noexcept 지정자의 의미와 사용법을 익힐 수 있습니다.
  • 실전에서 예외 안전한 코드를 작성할 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다. 일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

목차

  1. 실무에서 겪는 문제 시나리오
  2. 예외 안전성이란
  3. 세 가지 보장 수준
  4. RAII와 예외 결합
  5. noexcept 지정자
  6. 실전 패턴
  7. 자주 발생하는 문제와 해결법
  8. 모범 사례와 선택 가이드
  9. 프로덕션 패턴

1. 실무에서 겪는 문제 시나리오

시나리오 1: 멀티스레드에서 락이 해제되지 않음

서버가 멀티스레드로 동작 중입니다. mutex.lock()processRequest()에서 예외가 나면 unlock()에 도달하지 못해 데드락이 발생합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 나쁜 예: 락이 예외 경로에서 해제되지 않음
void handleRequest() {
    mutex.lock();
    processRequest();  // 예외 발생 시 unlock() 호출 안 됨
    mutex.unlock();
}

해결: std::lock_guard 등 RAII로 락을 관리합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 좋은 예: lock_guard는 소멸자에서 자동 unlock
void handleRequest() {
    std::lock_guard<std::mutex> lock(mutex);
    processRequest();  // 예외 발생해도 lock 소멸 시 unlock
}

시나리오 2: 컨테이너 수정 중 예외로 인한 상태 불일치

std::vector에 여러 요소를 추가하는 중 예외가 나면, 일부만 추가된 중간 상태가 남아 재시도나 롤백이 어렵습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Basic Guarantee: vec는 유효하지만 일부만 추가됨
void addItems(std::vector<Item>& vec, const std::vector<Item>& items) {
    for (const auto& item : items) {
        vec.push_back(item);  // 3번째에서 예외 시 vec는 2개만 추가됨
    }
}

해결: Strong Guarantee를 위해 복사 후 한 번에 교체하는 방식입니다.

시나리오 3: 생성자에서 예외 시 부분 생성된 객체

생성자에서 여러 리소스를 획득하다가 중간에 예외가 나면, 이미 획득한 리소스가 해제되지 않을 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 예: 생성자에서 예외 시 socket1만 해제되고 socket2는 누수
class DualConnection {
    Socket* socket1;
    Socket* socket2;
public:
    DualConnection() : socket1(new Socket()), socket2(nullptr) {
        socket1->connect("host1");
        socket2 = new Socket();  // 여기서 예외 시 socket1 누수
        socket2->connect("host2");
    }
};

해결: 멤버를 std::unique_ptr 등 RAII로 두면, 생성자에서 예외가 나도 이미 초기화된 멤버의 소멸자가 호출됩니다.

시나리오 4: 소멸자에서 예외를 던짐

소멸자에서 예외를 던지면 스택 언와인딩 중에 std::terminate()가 호출됩니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 나쁜 예: 소멸자에서 예외
~Resource() {
    if (cleanup())  // 실패 시 예외 던짐
        throw std::runtime_error("Cleanup failed");  // terminate() 호출!
}

해결: 소멸자에서는 예외를 던지지 않고, try-catch로 잡아 로그만 남깁니다.

2. 예외 안전성이란

예외 발생 시 프로그램 상태

예외가 던져지면:

  1. 현재 함수 실행 중단
  2. 스택 언와인딩 (Stack Unwinding) 시작
  3. 지역 객체들의 소멸자가 역순으로 호출됨
  4. catch 블록을 찾을 때까지 호출 스택을 거슬러 올라감 다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void func3() {
    Resource r3;
    throw std::runtime_error("Error!");
    // r3 소멸자 호출됨
}
void func2() {
    Resource r2;
    func3();
    // 예외 전파, r2 소멸자 호출됨
}
void func1() {
    Resource r1;
    try {
        func2();
    } catch (...) {
        // 여기서 잡힘
        // r1은 정상적으로 소멸됨
    }
}

위 코드 설명: func3에서 throw가 나면 r3 소멸자가 먼저 호출되고, 그다음 func2의 r2, func1의 r1 순으로 역순 소멸자가 호출됩니다(스택 언와인딩). try-catch는 func1에만 있지만, 중간에 있는 Resource 객체들은 모두 정상적으로 정리됩니다. 예외 안전성이란:

  • 예외가 발생해도 리소스 누수가 없고
  • 프로그램이 유효한 상태를 유지하는 것

스택 언와인딩 흐름도

다음은 mermaid를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TD
    subgraph Throw[throw 발생]
        A[func3: throw]
    end
    
    subgraph Unwind[스택 언와인딩]
        B[r3 소멸자 호출]
        C[func2로 전파]
        D[r2 소멸자 호출]
        E[func1로 전파]
        F[r1 소멸자 호출]
    end
    
    subgraph Catch[catch 블록]
        G[예외 처리]
    end
    
    A --> B --> C --> D --> E --> F --> G

위 다이어그램 설명: 예외가 던져지면 가장 안쪽 스코프의 지역 객체(r3)부터 역순으로 소멸자가 호출되고, catch 블록을 찾을 때까지 호출 스택을 거슬러 올라갑니다.

3. 세 가지 보장 수준

Level 1: Basic Guarantee (기본 보장)

정의: 예외 발생 시 리소스 누수 없음, 객체는 유효하지만 내용은 변경될 수 있음 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void basicSafe(std::vector<int>& vec, int value) {
    vec.push_back(value);  // 실패 시 vec는 유효하지만 변경됨
}
int main() {
    std::vector<int> vec = {1, 2, 3};
    try {
        basicSafe(vec, 4);  // 메모리 부족 시 예외
    } catch (...) {
        // vec는 유효하지만 4가 추가되었을 수도, 안 되었을 수도
    }
}

위 코드 설명: vec.push_back(value)가 메모리 부족 등으로 예외를 던지면, vec는 그 전까지의 상태는 유효하지만 “4가 들어갔는지 안 들어갔는지”는 보장되지 않을 수 있습니다. 리소스 누수만 없고 객체는 파괴되지 않는다는 것이 Basic Guarantee입니다. 특징:

  • 최소한의 보장
  • 리소스 누수만 없으면 됨
  • 객체 상태는 변경될 수 있음 완전한 Basic Guarantee 예시: 커스텀 컨테이너 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <stdexcept>
#include <memory>
template <typename T>
class SimpleVector {
    std::unique_ptr<T[]> data_;
    size_t size_ = 0;
    size_t capacity_ = 0;
public:
    // Basic Guarantee: push_back 실패 시 리소스 누수 없음, 객체는 유효
    void push_back(const T& value) {
        if (size_ >= capacity_) {
            auto new_cap = (capacity_ == 0) ? 4 : capacity_ * 2;
            auto new_data = std::make_unique<T[]>(new_cap);
            for (size_t i = 0; i < size_; ++i) {
                new_data[i] = data_[i];  // 복사 생성자 예외 가능
            }
            data_ = std::move(new_data);
            capacity_ = new_cap;
        }
        data_[size_++] = value;  // 대입 연산자 예외 가능
    }
};

위 코드 설명: push_back에서 메모리 재할당이나 복사/대입 중 예외가 나면, data_는 유효하고 size_는 변경되지 않았을 수 있습니다. 리소스 누수는 없지만(unique_ptr이 정리), “정확히 어떤 상태인지”는 보장되지 않습니다. 이게 Basic Guarantee입니다.

Level 2: Strong Guarantee (강한 보장)

정의: 예외 발생 시 상태가 변경 전으로 롤백됨 (commit or rollback) 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void strongSafe(std::vector<int>& vec, int value) {
    std::vector<int> temp = vec;  // 복사
    temp.push_back(value);        // 실패 가능
    vec = std::move(temp);        // 성공 시에만 반영 (noexcept)
}
int main() {
    std::vector<int> vec = {1, 2, 3};
    try {
        strongSafe(vec, 4);
    } catch (...) {
        // vec는 {1, 2, 3} 그대로 (변경 전 상태)
    }
}

위 코드 설명: temp에 복사한 뒤 temp에만 push_back하고, 성공하면 vec = std::move(temp)로 한 번에 바꿉니다. push_back이 예외를 던지면 vec는 건드리지 않았으므로 원래 상태 그대로이고, move 대입은 noexcept라서 예외 없이 완료됩니다. 따라서 “전부 성공하거나 전부 롤백”인 Strong Guarantee가 됩니다. 특징:

  • 트랜잭션 의미론
  • 성공하거나 실패하거나 (중간 상태 없음)
  • 복사 비용이 들 수 있음 완전한 Strong Guarantee 예시: 여러 요소 추가 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <algorithm>
// Strong Guarantee: 성공 시 전부 반영, 실패 시 vec는 변경 전 그대로
template <typename T>
void appendAll(std::vector<T>& vec, const std::vector<T>& to_add) {
    std::vector<T> temp = vec;           // 1. 복사 (실패 가능)
    temp.reserve(temp.size() + to_add.size());
    for (const auto& item : to_add) {
        temp.push_back(item);            // 2. temp에만 추가 (실패 시 vec 무관)
    }
    vec = std::move(temp);               // 3. 성공 시에만 교체 (noexcept)
}
int main() {
    std::vector<std::string> vec = {"a", "b"};
    std::vector<std::string> add = {"c", "d"};
    try {
        appendAll(vec, add);             // vec == {"a","b","c","d"}
    } catch (const std::bad_alloc&) {
        // vec는 여전히 {"a","b"} - 변경 전 상태 유지
    }
}

위 코드 설명: temp에만 작업하고, 모든 작업이 성공한 뒤 vec = std::move(temp)로 한 번에 교체합니다. std::vector의 move 대입은 noexcept이므로 이 시점 이후에는 예외가 나지 않습니다. 중간에 예외가 나면 vec는 전혀 건드리지 않았으므로 Strong Guarantee가 만족됩니다.

Level 3: Nothrow Guarantee (예외 없음 보장)

정의: 절대 예외를 던지지 않음 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void nothrowOp(int& value) noexcept {
    value++;  // 예외 없음
}
int main() {
    int x = 5;
    nothrowOp(x);  // 예외 걱정 없음
}

위 코드 설명: noexcept로 선언된 함수는 예외를 던지지 않음을 약속합니다. 내부에서 예외가 발생하면 catch되지 않고 std::terminate()가 호출되어 프로그램이 종료됩니다. 단순 연산만 하므로 예외가 나지 않는 경우에 사용하는 Nothrow Guarantee 예시입니다. 특징:

  • noexcept 키워드로 명시
  • 예외를 던지면 std::terminate() 호출 (프로그램 종료)
  • 최고 수준의 보장 완전한 Nothrow Guarantee 예시: swap, 포인터 조작 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <utility>
#include <cstdint>
class Handle {
    void* resource_ = nullptr;
public:
    void swap(Handle& other) noexcept {
        std::swap(resource_, other.resource_);  // 포인터 swap만, 예외 없음
    }
    void* get() const noexcept {
        return resource_;
    }
    void reset() noexcept {
        resource_ = nullptr;  // 단순 대입, 예외 없음
    }
};
// Nothrow Guarantee: 절대 예외를 던지지 않음
void safeIncrement(int* ptr) noexcept {
    if (ptr) {
        ++(*ptr);
    }
}

위 코드 설명: swap, get, reset, safeIncrement는 메모리 할당이나 복사 생성 없이 포인터/정수만 다루므로 예외를 던지지 않습니다. noexcept로 선언하면 컴파일러가 최적화에 활용하고, std::vector 재할당 시 move 대신 복사를 쓰지 않게 됩니다.

세 가지 보장 수준 비교표

보장 수준리소스 누수객체 상태사용 예
Basic없음유효하나 변경될 수 있음vector::push_back, 일반 연산
Strong없음변경 전으로 롤백Copy-and-Swap, 트랜잭션
Nothrow없음변경 없음 (예외 자체 없음)swap, move, 소멸자

보장 수준 선택 흐름도

아래 코드는 mermaid를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TD
    A[연산 수행] --> B{예외 발생?}
    B -->|아니오| C[정상 완료]
    B -->|예| D{리소스 누수?}
    D -->|있음| E[❌ 보장 없음]
    D -->|없음| F{객체 상태}
    F -->|원래대로| G[✅ Strong Guarantee]
    F -->|유효하나 변경됨| H[✅ Basic Guarantee]
    A --> I{예외 가능?}
    I -->|아니오| J[✅ Nothrow Guarantee]

4. RAII(Resource Acquisition Is Initialization, 리소스 획득은 초기화)와 예외 결합

자동 리소스 정리

나쁜 예: 수동 정리 다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void processLine(FILE*);  // 예외를 던질 수 있음
void processData() {
    FILE* file = fopen("data.txt", "r");
    if (!file) {
        throw std::runtime_error("Cannot open");
    }
    
    char* buffer = new char[1024];
    
    // processLine(file) 등이 예외를 던지면
    // ❌ delete[], fclose에 도달하지 못함
    processLine(file);
    
    // 정상 종료
    delete[] buffer;
    fclose(file);
}

위 코드 설명: fopen과 new로 획득한 리소스는 예외가 나기 전에 매 경로에서 수동으로 delete[]와 fclose를 호출해야 합니다. someError 분기와 정상 종료 경로에 중복 정리 코드가 들어가고, 새 예외 경로가 생기면 해제를 빼먹기 쉽습니다. 좋은 예: RAII 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void processLine(std::ifstream&);  // 예외를 던질 수 있음
void processData() {
    std::ifstream file("data.txt");  // RAII
    if (!file) {
        throw std::runtime_error("Cannot open");
    }
    
    auto buffer = std::make_unique<char[]>(1024);  // RAII
    
    // processLine(file) 예외 발생해도
    // ✅ buffer와 file 소멸자가 자동 호출됨
    processLine(file);
}

위 코드 설명: std::ifstream과 std::make_unique는 소멸자에서 파일 닫기와 메모리 해제를 수행합니다. someError에서 throw를 해도 스코프를 벗어날 때 buffer, file이 역순으로 정리되므로 정리 코드를 여러 곳에 쓸 필요가 없습니다.

여러 리소스 관리

다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void multipleResources(const std::string& path1, const std::string& path2) {
    auto file1 = std::make_unique<std::ifstream>(path1);
    if (!*file1) {
        throw std::runtime_error("Cannot open file1");
    }
    
    auto file2 = std::make_unique<std::ifstream>(path2);
    if (!*file2) {
        throw std::runtime_error("Cannot open file2");
        // ✅ file1은 자동으로 닫힘
    }
    
    auto buffer = std::make_unique<char[]>(1024);
    
    // 작업 중 예외 발생 시
    // buffer, file2, file1 순서로 자동 정리
}

위 코드 설명: file2 열기 실패로 throw가 나면 file1을 가진 unique_ptr이 먼저 소멸하면서 파일이 닫힙니다. 여러 리소스를 RAII 객체로 두면 획득의 역순으로 자동 정리되므로, 예외 경로에서도 누수가 나지 않습니다.

커스텀 RAII 래퍼

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class FileHandle {
    FILE* file;
public:
    FileHandle(const char* path, const char* mode) 
        : file(fopen(path, mode)) {
        if (!file) {
            throw std::runtime_error("Cannot open file");
        }
    }
    
    ~FileHandle() {
        if (file) fclose(file);
    }
    
    // 복사 금지
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    FILE* get() const { return file; }
};
void useFile() {
    FileHandle file("data.txt", "r");  // RAII
    // 예외 발생해도 자동으로 닫힘
    processFile(file.get());
}

위 코드 설명: FileHandle은 생성자에서 fopen, 소멸자에서 fclose를 호출합니다. 복사는 막고, useFile에서 예외가 나든 정상 종료든 스코프를 벗어날 때 소멸자가 호출되어 파일이 닫힙니다. C 스타일 FILE*를 RAII로 감싸는 전형적인 패턴입니다.

5. noexcept 지정자

noexcept의 의미

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void safeFunction() noexcept {
    // 이 함수는 예외를 던지지 않음을 보장
}
void riskyFunction() {
    // noexcept 없음: 예외를 던질 수 있음
}

위 코드 설명: noexcept를 붙이면 “이 함수는 예외를 던지지 않는다”는 계약을 컴파일러와 호출자에게 알립니다. noexcept 함수 안에서 예외가 나면 스택 언와인딩 없이 std::terminate()가 호출되므로, 예외를 절대 던지지 않는 연산에만 사용해야 합니다. noexcept 위반 시: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void func() noexcept {
    throw std::runtime_error("Error!");  // noexcept 위반
}
int main() {
    func();  // std::terminate() 호출, 프로그램 종료
}

위 코드 설명: noexcept로 선언된 함수에서 throw가 실행되면 C++ 런타임이 std::terminate()를 호출해 프로그램을 종료합니다. catch로 잡을 수 없으므로, noexcept는 “예외가 나지 않음을 보장할 수 있는” 함수에만 지정해야 합니다.

noexcept가 필수인 경우

1. 소멸자 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Resource {
public:
    ~Resource() noexcept {  // 소멸자는 기본적으로 noexcept
        // 예외를 던지면 안 됨
        try {
            cleanup();
        } catch (...) {
            // 에러를 삼킴
        }
    }
};

위 코드 설명: 소멸자는 예외가 나도 스택 언와인딩 중에 호출되므로, 소멸자에서 예외를 던지면 위험합니다. 그래서 소멸자는 기본적으로 noexcept처럼 동작하며, 정리 중 에러는 try-catch로 잡아 로그만 남기고 삼키는 방식이 안전합니다. 2. move 생성자/대입 연산자 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        // move는 noexcept여야 std::vector 등에서 최적화됨
        data = other.data;
        other.data = nullptr;
    }
    
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
    
private:
    int* data = nullptr;
};

위 코드 설명: std::vector 등이 재할당할 때 요소를 옮길 때 move 생성자/대입을 쓰는데, 이들이 noexcept일 때만 move를 사용하고 아니면 복사를 씁니다. move가 실패하면 Strong Guarantee를 지키기 어렵기 때문에, move 연산은 noexcept로 선언하는 것이 표준 관례입니다. 이유: std::vector가 재할당 시 move가 noexcept면 move 사용, 아니면 복사 사용 3. swap 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

class MyClass {
public:
    void swap(MyClass& other) noexcept {
        std::swap(data, other.data);
    }
    
private:
    int* data = nullptr;
};

위 코드 설명: swap은 포인터나 핸들만 맞바꾸므로 예외를 던지지 않습니다. Copy-and-Swap 패턴에서 swap이 noexcept여야 대입 연산자가 Strong Guarantee를 제공할 수 있으므로, swap에도 noexcept를 붙이는 것이 좋습니다.

noexcept 조건부 지정

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
void process(T value) noexcept(noexcept(T())) {
    // T의 생성자가 noexcept면 이 함수도 noexcept
    T copy = value;
}

위 코드 설명: noexcept(noexcept(T()))는 “T의 기본 생성자가 noexcept이면 이 함수도 noexcept”라는 조건부 지정입니다. 템플릿에서 타입에 따라 예외를 던질 수 있는지가 달라질 때, 호출자에게 정확한 예외 명세를 전달할 수 있습니다.

6. 실전 패턴

패턴 1: Copy-and-Swap (Strong Guarantee)

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Buffer {
    char* data;
    size_t size;
    
public:
    Buffer(size_t sz) : data(new char[sz]), size(sz) {}
    
    ~Buffer() {
        delete[] data;
    }
    
    // Copy-and-Swap으로 Strong Guarantee
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            Buffer temp(other);        // 복사 (실패 가능)
            swap(temp);                // swap (noexcept)
            // temp 소멸 시 기존 data 해제
        }
        return *this;
    }
    
    void swap(Buffer& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
};

위 코드 설명: 대입 시 temp에 복사한 뒤(temp 생성이 예외를 던질 수 있음), 성공하면 swap으로 멤버를 맞바꿉니다. swap은 noexcept이므로 그 시점 이후에는 예외가 나지 않고, temp 소멸 시 기존 data가 해제됩니다. 복사 실패 시 원래 객체는 그대로이므로 Strong Guarantee가 만족됩니다.

패턴 2: 트랜잭션 스타일

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Database {
public:
    void updateRecord(int id, const std::string& value) {
        // 1. 백업
        auto backup = getRecord(id);
        
        try {
            // 2. 변경 시도
            setRecord(id, value);
            commit();
        } catch (...) {
            // 3. 실패 시 롤백
            setRecord(id, backup);
            throw;  // 예외 재전파
        }
    }
};

위 코드 설명: 먼저 현재 값을 backup에 두고, setRecord·commit을 시도합니다. 예외가 나면 catch에서 backup으로 롤백한 뒤 throw로 예외를 다시 던져 호출자에게 실패를 알립니다. “시도 → 실패 시 원상 복구” 트랜잭션 스타일로 Strong Guarantee를 구현한 예입니다.

패턴 3: 예외 변환

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 하위 레벨 예외를 상위 레벨 예외로 변환
void loadUserData(int userId) {
    try {
        // 데이터베이스 예외 (구체적)
        database.query("SELECT * FROM users WHERE id = " + std::to_string(userId));
    } catch (const DatabaseException& e) {
        // 애플리케이션 예외 (추상적)로 변환
        throw UserNotFoundException("User not found: " + std::to_string(userId));
    }
}

위 코드 설명: 하위 레이어(DatabaseException)의 예외를 상위 레이어용(UserNotFoundException)으로 감싸서 다시 던집니다. 호출자가 DB 상세보다 “사용자를 찾을 수 없음”이라는 도메인 의미로 처리할 수 있게 하며, 예외 타입을 계층에 맞게 변환하는 패턴입니다.

패턴 4: 리소스 획득 후 예외 안전 보장

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Connection {
    Socket* socket;
    bool connected;
    
public:
    Connection(const std::string& host, int port) 
        : socket(new Socket()), connected(false) {
        try {
            socket->connect(host, port);
            connected = true;
        } catch (...) {
            delete socket;  // 생성자 실패 시 정리
            throw;
        }
    }
    
    ~Connection() {
        if (connected) {
            socket->disconnect();
        }
        delete socket;
    }
};
// 더 나은 방법: unique_ptr 사용
class Connection {
    std::unique_ptr<Socket> socket;
    bool connected;
    
public:
    Connection(const std::string& host, int port) 
        : socket(std::make_unique<Socket>()), connected(false) {
        socket->connect(host, port);  // 예외 발생해도 socket 자동 해제
        connected = true;
    }
    
    ~Connection() {
        if (connected) {
            socket->disconnect();
        }
        // socket 자동 해제
    }
};

위 코드 설명: 첫 번째 Connection은 생성자에서 connect 실패 시 수동으로 delete socket 후 재throw합니다. 두 번째는 unique_ptr을 써서 socket을 RAII로 관리하므로, connect에서 예외가 나도 소멸자에서 자동 해제되어 코드가 단순하고 누수 위험이 없습니다.

패턴 5: 예외 중립 함수

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
void processContainer(std::vector<T>& vec) {
    // 이 함수는 T의 연산이 던지는 예외를 그대로 전파
    for (auto& item : vec) {
        item.process();  // T::process()가 예외 던질 수 있음
    }
    // 예외 발생 시 vec는 부분적으로 처리된 상태 (Basic Guarantee)
}

위 코드 설명: 이 함수 자체는 예외를 던지지 않지만, item.process()가 던지는 예외를 그대로 위로 전파합니다. 중간에 예외가 나면 그 시점까지 처리된 항목은 변경된 상태로 남으므로 Basic Guarantee만 제공하며, 호출자가 예외를 처리하거나 더 위로 전파할 수 있습니다.

7. 자주 발생하는 문제와 해결법

문제 1: “예외가 나는데 왜 메모리가 계속 늘어나요?”

원인: new/delete를 직접 사용하고, 예외 경로에서 delete를 호출하지 않음. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 코드
void process() {
    char* buf = new char[1024];
    doWork();  // 예외 시 delete[] 호출 안 됨
    delete[] buf;
}
// ✅ 올바른 코드
void process() {
    auto buf = std::make_unique<char[]>(1024);
    doWork();  // 예외 시 unique_ptr 소멸자에서 자동 해제
}

문제 2: “mutex가 풀리지 않아 데드락이 발생해요”

원인: lock() 후 예외가 나면 unlock()에 도달하지 못함. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 코드
void criticalSection() {
    mutex.lock();
    riskyOperation();  // 예외 시 unlock 안 됨
    mutex.unlock();
}
// ✅ 올바른 코드
void criticalSection() {
    std::lock_guard<std::mutex> lock(mutex);
    riskyOperation();
}

문제 3: “소멸자에서 예외를 던지면 프로그램이 종료돼요”

원인: 소멸자는 기본적으로 noexcept처럼 동작. 예외를 던지면 std::terminate() 호출. 해결법: 다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 코드
~Resource() {
    if (!cleanup()) {
        throw std::runtime_error("Cleanup failed");  // terminate()!
    }
}
// ✅ 올바른 코드
~Resource() noexcept {
    try {
        cleanup();
    } catch (const std::exception& e) {
        std::cerr << "Cleanup error: " << e.what() << "\n";
        // 로그만 남기고 삼킴
    }
}

문제 4: “vector::push_back 중 예외가 나면 일부만 추가돼요”

원인: push_back은 Basic Guarantee. 메모리 부족 등으로 예외 시 중간 상태 가능. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Strong Guarantee가 필요하면: 복사 후 한 번에 교체
void addSafely(std::vector<Item>& vec, const Item& item) {
    auto temp = vec;
    temp.push_back(item);
    vec = std::move(temp);  // 성공 시에만 반영
}

문제 5: “생성자에서 예외가 나면 이미 할당한 멤버가 누수돼요”

원인: 생성자에서 예외 시 해당 객체의 소멸자는 호출되지 않음. 하지만 이미 완전히 초기화된 멤버의 소멸자는 호출됨. 해결법: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 코드: raw 포인터
class Bad {
    int* a;
    int* b;
public:
    Bad() : a(new int(1)), b(nullptr) {
        b = new int(2);  // 예외 시 a 누수
    }
};
// ✅ 올바른 코드: RAII 멤버
class Good {
    std::unique_ptr<int> a;
    std::unique_ptr<int> b;
public:
    Good() : a(std::make_unique<int>(1)), b(std::make_unique<int>(2)) {
        // b 생성에서 예외 시 a의 소멸자 자동 호출
    }
};

8. 모범 사례와 선택 가이드

보장 수준 선택 가이드

상황권장 보장이유
리소스 관리 (RAII)Nothrow소멸자에서 예외를 던지면 안 됨
대입 연산자StrongCopy-and-Swap 패턴
swap, moveNothrow표준 라이브러리와의 호환, 최적화
컨테이너 수정Basic 또는 Strong성능 vs 안전성 트레이드오프
단순 읽기/연산Nothrow예외 없음이 보장되는 경우

체크리스트: 예외 안전한 코드 작성

  • new/delete 대신 std::unique_ptr, std::shared_ptr 사용
  • fopen/fclose 대신 std::ifstream, std::ofstream 사용
  • lock/unlock 대신 std::lock_guard, std::scoped_lock 사용
  • 소멸자에서 예외를 던지지 않음 (try-catch로 감싸기)
  • move 생성자/대입 연산자에 noexcept 지정
  • swap 구현 시 noexcept 지정
  • Strong Guarantee가 필요하면 Copy-and-Swap 또는 트랜잭션 스타일

예외 안전성과 성능

  • Basic Guarantee: 복사 없이 직접 수정 가능
  • Strong Guarantee: 복사/임시 객체 필요 → 비용 증가
  • Nothrow Guarantee: 예외 경로 없음 → 컴파일러 최적화 유리

9. 프로덕션 패턴

패턴 A: Scope Guard (RAII 확장)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <functional>
#include <utility>
class ScopeGuard {
    std::function<void()> on_exit_;
    bool active_ = true;
public:
    explicit ScopeGuard(std::function<void()> f) : on_exit_(std::move(f)) {}
    ~ScopeGuard() {
        if (active_) {
            try {
                on_exit_();
            } catch (...) {
                // 예외 삼킴 (소멸자에서 던지면 안 됨)
            }
        }
    }
    void release() { active_ = false; }
    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
};
// 사용 예: 작업 실패 시 롤백
void updateConfig(const Config& cfg) {
    auto backup = loadConfig();
    ScopeGuard guard([&] { saveConfig(backup); });  // 예외 시 롤백
    saveConfig(cfg);
    guard.release();  // 성공 시 롤백 방지
}

패턴 B: 예외 안전한 팩토리

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
template <typename T, typename....Args>
std::unique_ptr<T> makeUniqueSafe(Args&&....args) {
    auto ptr = std::make_unique<T>(std::forward<Args>(args)...);
    return ptr;  // 생성 성공 시에만 반환, 실패 시 예외
}
// 여러 리소스 생성 시: 하나라도 실패하면 이전 것들 자동 정리
std::vector<std::unique_ptr<Connection>> createConnections(
    const std::vector<std::string>& hosts) {
    std::vector<std::unique_ptr<Connection>> result;
    result.reserve(hosts.size());
    for (const auto& host : hosts) {
        result.push_back(std::make_unique<Connection>(host));
        // 예외 시 result의 기존 요소들 소멸자로 자동 정리
    }
    return result;
}

패턴 C: 스레드 안전 + 예외 안전

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <mutex>
#include <memory>
#include <optional>
template <typename T>
class ThreadSafeQueue {
    mutable std::mutex mtx_;
    std::vector<T> data_;
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mtx_);
        data_.push_back(std::move(value));  // Basic Guarantee
    }
    std::optional<T> tryPop() {
        std::lock_guard<std::mutex> lock(mtx_);
        if (data_.empty()) return std::nullopt;
        T value = std::move(data_.back());
        data_.pop_back();  // noexcept
        return value;
    }
};

위 코드 설명: push에서 예외가 나든 정상 종료든 lock_guard 소멸자가 unlock을 호출하므로 데드락이 발생하지 않습니다.

패턴 D: 로깅 + 예외 전파

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <iostream>
#include <stdexcept>
void processWithLogging(const std::string& input) {
    std::cerr << "[INFO] Processing: " << input << "\n";
    try {
        doProcess(input);
        std::cerr << "[INFO] Success\n";
    } catch (const std::exception& e) {
        std::cerr << "[ERROR] " << e.what() << "\n";
        throw;  // 예외 재전파
    }
}

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++ 예외 안전성, strong guarantee, RAII 예외, noexcept, 예외 스펙 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목내용
Basic Guarantee리소스 누수 없음, 상태는 유효
Strong Guarantee상태가 변경 전으로 롤백
Nothrow Guarantee예외를 절대 던지지 않음
RAII + 예외소멸자가 자동으로 리소스 정리
noexcept소멸자, move, swap에 필수
패턴Copy-and-Swap, 트랜잭션, 변환

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ 예외 안전성(exception safety) 완벽 가이드. Basic·Strong·Nothrow 세 가지 보장 수준, RAII와 예외 결합 패턴, noexcept 지정자 사용법, 예외 발생 시 리소스 누수 방… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 한 줄 요약: RAII로 리소스를 관리하면 예외가 나도 누수가 나지 않습니다. 다음으로 커스텀 예외(#8-3)를 읽어보면 좋습니다. 다음 글: C++ 실전 가이드 #8-3: 커스텀 예외와 성능 - 커스텀 예외 클래스와 예외 성능을 다룹니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3