C++에서 예외(exception)는 런타임 오류를 처리하는 핵심 메커니즘이지만, 모든 코드베이스가 예외를 선호하는 것은 아닙니다. 일부 프로젝트는 성능상 이유나 제약 때문에 예외를 비활성화하고, 에러 코드를 반환하거나 std::expected를 통한 명시적 에러 처리 방식을 선호하기도 합니다. 또한 RAII를 통해 예외 안전성을 확보하고, 리소스 누수를 방지하는 패턴도 중요한 스타일 이슈입니다.
이번 글에서는 다양한 스타일 가이드와 프로젝트 사례를 바탕으로, 예외 사용 여부 결정, std::expected나 에러 코드 기반 접근, RAII 기법, noexcept 사용, 그리고 에러 처리 시 주석과 문서화 방법 등을 다뤄봅니다.
다양한 스타일 가이드와 사례
- 구글 C++ 스타일 가이드:
- 과거에는 예외 사용을 금지했으나, 최근엔 일부 프로젝트에서 제한적 사용 허용
- 에러 코드 반환 패턴이나 status 객체를 통한 명시적 처리 권장
- RAII를 통한 리소스 관리 강조
- LLVM 스타일 가이드:
- LLVM 프로젝트는 기본적으로 예외를 사용하지 않음(성능, 호환성 이유)
- 에러를 Error 타입이나 Expected<T>로 처리, 명시적 에러 핸들링
- RAII를 사용해 메모리나 파일 핸들 등 리소스 관리를 안전하게 함
- 모질라 스타일 가이드:
- 예외 사용 가능하지만, 성능 민감 코드나 특정 모듈은 에러 코드 기반
- mozilla::Result 형태로 std::expected 유사 패턴 활용
- RAII idiom 적극 권장
장점 및 단점 분석
예외(throw) 사용
장점:
- 정상 흐름과 오류 흐름 분리 명확
- 예외 처리 코드가 오류 발생 지점과 떨어져 있어도 제어 흐름 단순화 가능
단점:
- 런타임 오버헤드(예외 테이블, 스택 언와인딩) 가능성
- noexcept 함수와의 연계 어려움, 이식성/성능 문제
- 일부 환경(임베디드, 고성능 애플리케이션)에서 예외 비활성화 필요
std::expected 또는 에러 코드 반환
장점:
- 모든 코드 경로에서 오류 처리가 명시적, 호출자가 항상 처리할지 결정 가능
- noexcept 환경이나 제한적 런타임 환경에 유용
- std::expected<T,E>로 성공/실패를 타입 시스템에서 명확히 표현
단점:
- 호출자 코드가 매번 오류 처리 분기를 넣어야 하므로 간결성 낮아짐
- 성공 경로와 실패 경로가 섞여 가독성 저하 가능(적절한 헬퍼 함수나 매크로로 개선 가능)
RAII
장점:
- 예외나 에러 코드와 상관없이 리소스가 자동 정리
- 스마트 포인터, std::unique_ptr, std::lock_guard 등 사용 시 예외 안전성 강화
- 가독성, 유지보수성 향상
단점:
- 초기 배우는 과정에서 개념 파악 필요
- RAII 객체 설계 시 주의 필요(너무 복잡한 RAII 클래스는 오히려 혼란)
어떤 경우 어떤 선택을 할까?
- 고성능/임베디드 환경: 예외 비활성화, 에러 코드나 std::expected 활용. RAII로 리소스 관리 일관성 확보
- 일반 데스크톱/서버 응용: 예외 기반 흐름 제어 가능. 그러나 공용 라이브러리는 std::expected로 명시적 에러 처리 제공할 수도 있음
- 혼합 접근: 공개 API는 std::expected처럼 타입 안전 에러 전달, 내부 구현은 예외 사용. 팀 합의와 빌드 옵션으로 제어
Noexcept를 통한 함수 예측성 보장, 예외와 RAII 결합해 안전한 리소스 처리, std::expected 기반 함수 체인 등을 혼합해 상황에 맞는 에러 처리 패턴을 확립할 수 있습니다.
실제 예제 코드 비교
// 예외 사용
int SafeDivide(int a, int b) {
if (b == 0) throw std::runtime_error("divide by zero");
return a / b;
}
// std::expected 사용 (C++23)
#include <expected>
std::expected<int, std::string> SafeDivide2(int a, int b) {
if (b == 0) return std::unexpected("divide by zero");
return a / b;
}
// RAII 예: 파일 핸들 관리
class FileHandle {
public:
explicit FileHandle(const char* path) : f_(std::fopen(path, "r")) {}
~FileHandle() {
if (f_) std::fclose(f_);
}
FILE* get() const { return f_; }
private:
FILE* f_;
};
위 예제에서 예외 기반 함수, std::expected 기반 함수, 그리고 RAII를 통한 리소스 관리 패턴을 확인할 수 있습니다.
마무리
예외 처리와 에러 관리 스타일은 프로젝트 특성과 팀 합의에 따라 크게 달라집니다. 성능, 이식성, 유지보수성, 협업 환경을 고려하여 예외 사용 여부를 결정하고, 대안으로 std::expected나 에러 코드를 활용할 수 있습니다. RAII는 어떤 에러 처리 방식을 선택하든 좋은 보완재로서 코드 안전성과 가독성을 높여줍니다.
다음 편에서는 현대 문법(auto, 구조적 바인딩, 람다 캡처 등)을 사용할 때 어떤 스타일 가이드라인을 적용할지, 모던 C++ 문법 요소를 효과적으로 사용하는 방식에 대해 살펴보겠습니다.
'개발 이야기 > C++' 카테고리의 다른 글
[C++ 스타일 9편] 입출력과 문자열 처리: std::format, iostream, 로깅, 그리고 문자열 뷰 (0) | 2024.12.15 |
---|---|
[C++ 스타일 8편] 현대 문법의 활용: auto, 구조적 바인딩, 람다 캡처, 그리고 개선된 for 루프 (0) | 2024.12.15 |
[C++ 스타일 6편] 템플릿, Concepts, 그리고 메타프로그래밍 코드의 스타일 (0) | 2024.12.15 |
[C++ 스타일 5편] 주석과 문서화: Doxygen, Javadoc, 단문 주석 스타일 (0) | 2024.12.15 |
[C++ 스타일 4편] 클래스와 함수 인터페이스: 멤버 순서, 접근성(접근제어) 배치, 함수 본문 스타일 (0) | 2024.12.15 |