13. 예외 처리
13.1 오류 처리
13.1.1 예외
예외(exception) 이란 개념은 오류가 탐지된 시점에서 오류가 처리될 수 있는 시점으로 정보를 전달하는 데 도움을 주기 위해 제공된다.
전형적인 예제는 아래와 같다.
기본적으로 예외는 특정 목적을 위해 특별히 정의된 사용자 정의 타입만을 사용하기를 권장한다. -> 명확한 의도전달!
13.1.2 전통적인 예외 처리
프로그램 종료!(꽤나 과격한 방법..) -> 계속적으로 돌아가는 프로그램에서는 사용 불가한 방법
오류 처리 함수를 호출한다.
오류 처리 함수를 통해서 프로그램이 종료되지 않고 로직이 계속될 수 있다.
13.1.3. 버티기
프로그램이 종료를 받아들일 수 없는 상황이라면 모든 예외를 붙잡아 둘 수 있다. -> 결국 예외는 프로그래머가 허용한 경우에만 프로그램을 종료한다.
때때로 사람들은 오류 메시지를 출력하고, 사용자에게 도움을 요청하는 대화상자를 만드는 등의 방식으로 '버티기' 의 부정적인 측면들을 완화시키려고 노력한다.
13.1.4 예외의 다른 관점
13.1.4.1 비동기적 이벤트
예외 처리 매커니즘은 동기적은 예외만을 처리하게 설계되어 있다.
비동기적 이벤트는 예외 처리를 사용할 수 없기 때문에, 근본적으로 다른 매커니즘을 필요로 한다.
많은 시스템은 비동기적 예외를 처리하기 위한 매커니즘 신호와 같은 것들을 지원해서 비동기적 예외를 처리한다.
13.1.4.2 오류가 아닌 예외
예외를 '시스템의 어떤 부분에서 요청된 작업을 처리할 수 없는 것' 이라고 생각해보자.
예외(throw) 는 함수 호출에 비해서 드물에 이루어져야 한다. -> 그렇지 않은 경우 시스템 구조가 모호해진다.
프로그램 동작에 나쁜 영향을 끼치지 않는 예외의 경우, 단지 프로그래머는 그것을 오류라고 간주하고 예외처리 매커니즘을 오류를 처리하는 도구 정도로 생각한다. -> 누군가는 예외 처리 매커니즘을 그저 또 하나의 제어 구조이자 호출자에게 값을 반환하는 다른 방법 정도로 생각할 수 있다.
'예외처리는 오류처리' 라는 관점에 집중해야 한다. -> 그 외 관점의 예외처리는 용도에 맞지 않게 사용하고 있는 것이다.
13.1.6. 계층적인 예외 처리
예외 처리의 목적은 프로그램의 한 부분이 다른 부분에게 요청된 작업이 수행될 수 없다는 사실을 알려주기 위한 수단을 제공하는 것이다.
프로그램의 다양한 부분이 예외 처리되는 방식과 예외가 처리될 장소에 대한 정의가 되어 있어야 한다. -> 예외 처리 매커니즘은 기본적으로 비지역적으로 종합적인 전략을 정의하는 것이 필수적이다. -> 간단하고 명료해야 한다.
규모가 큰 프로젝트에서 예외 처리 전략을 상세하게 정의하고 지키기는 어려운 일이다.. -> 때문에, 범용적으로 사용할 수 있는 예외처리 전략을 세우는 일은 중요한 일이다! -> 범용적이고, 계층적인 예외 처리 전략을 필수적이다.
13.1.7 예외와 효율성
예외 던지기는 함수 호출처럼 비용이 많이 들지 않게끔 구현될 수 있다. -> 쉬운일은 아니다.. -> 하지만, 예외 처리가 아닌 다른 대안 역시 공짜는 아니다!
예외 처리와 관련 없어 보이는 아래 함수를 확인해보자.
g() 나 h() 는 예외를 던질 수 있으므로, f() 는 예외가 일어날 경우 buf 를 제대로 소멸시킬 수 있는 코드를 탑재해야 한다.
신중하고 체계적인 오류 처리가 필요할 때는 예외 처리 매커니즘을 사용하는 것이 최선이다.
noexcept 키워드를 사용하게 되면 컴파일러에게 해당 함수는 예외를 던지지 않는 것을 알려주기 때문에, 요긴하게 사용될 수 있다.
전통적인 C 함수는 예외를 던지지 않으므로 대부분의 C 함수는 noexcept 를 사용할 수 있다.
하지만, C 함수 내부에서 C++ 의 new 연산자를 사용해 bad_alloc 을 던질수도 있다.. -> 늘 그렇듯 측정이 수반되지 않은 효율성 논의는 무의미하다.
13.2 예외 보장
C++ 표준 라이브러리는 모든 라이브러리 연산 내 예외에 대해서 다음 중 하나를 보장한다.
C++ 표준 라이브러리가 여러 수준의 예외 안정성을 제공하는 이유
예외 발생시에도 프로그램이 일관된 상태를 유지
리소스 누수 등 심각한 문제가 발생하지 않도록
기본 보장(basic guarantee)
일관된 상태 유지 : 표준 라이브러리의 대부분의 구성 요소는 예외가 발생하더라도 개체가 일관된 상태를 유지하도록 설계되었다.
리소스 누수 방지 : 표준 라입브러리는 예외 발생 시 리소스가 누수되지 않도록 보장한다.
아래 코드에서 vec 의 상태는 구체적으로 알 수 없다.. -> 예외가 발생하기 전의 상태, 즉 '{1,2,3}' 일수도 있고, -> 예외가 발생하기 직전의 상태인, 즉 '{1,2,3,4}' 일수도 있다. -> 구체적으로 어떤 상태인지 예측할 수 없지만, vec 는 항상 유효한 상태를 유지한다.
강한 보장(strong guarantee)
롤백 : 일부 표준 라이브러리의 구성 요소는 예외가 발생하면 함수 호출 이전 상태로 되돌릴 수 있는 강한 보장을 제공한다.
예외 없음 보장(nothrow guarantee)
예외를 던지지 않음 : 표준 라이브러리의 일부 함수는 예외를 던지지 않음을 보장한다. 주로 소멸자, 메모리 할당 / 해제 함수 등에서 사용된다.
Basic gaurantee, Strong guarantee 는 기본적으로 자원 누출이 없을 것을 요구한다.
13.3 자원 관리
함수에서 파일, 힙 메모리, 뮤텍스 등을 획득할 때는 해당 자원이 반납되는 것이 필수적이다.
다음과 같이 catch 문을 통해서 자원을해제할 수 있다.
하지만 위와 같은 방식은 코드 복잡성과 유지보수 측면에서 더 많은 비용이 들 수 있다. -> 예외 처리의 단점..
때문에, 아래와 같이 생성자와 소멸자를 가진 클래스를 개체를 통해 처리할 수 있다. ( RAII 방식) -> File_ptr 을 생성하는 과정에서 예외가 발생하게 된다면 RAII 에 의해 File_ptr 의 소멸자가 호출된다.
RAII(Resource Acqusition Is Initialization)
RAII 의 기본 개념
C++ 의 중요한 프로그래밍 기법 중 하나로, 자원의 획득과 해제를 개체의 생명 주기와 결부시키는 것을 의미한다.
RAII 의 동작 원리
개체 생성 시 자원 획득 : 개체가 생성되면 생성자를 통해 자원을 얻는다. -> 생성자가 호출되기 이전까지는 개체가 생성되지 않은 것이다!
예외 발생 시 자원 해제 : 개체를 사용하는 함수 내에서 예외가 발생한다면, Stack Unwinding 이 시작되어 개체의 소멸자가 호출된다.
개체 소멸 시 자원 해제 : 개체가 스코프를 벗어나면 소멸자가 호출된다.
13.1.1 Finally
Finally 은 예외 이후의 마무리를 담당하는 임의의 코드를 작성하는 방식으로 RAII 에 비해서 단점이 있어 사용을 권장하지는 않는다.
Finally 에 비해 RAII 의 장점은 다음과 같다. -> 결론적으로 RAII 방식은 자원 관리가 개체의 생명 주기와 결합되어 있어, 코드가 더 간결하고 예외 안정성이 높으며, 유지보수가 용이하다.
코드의 간결성과 가독성
RAII: 자원의 획득과 해제가 객체의 생명 주기에 묶여 있기 때문에, 자원 해제 코드가 분리되지 않고 객체의 소멸자에 포함됩니다. 이는 코드의 가독성을 높이고, 자원 관리를 명확하게 합니다.
finally 블록: 자원을 해제하는 코드가 각기 다른 위치에 흩어질 수 있습니다. 특히 자원을 해제해야 하는 다양한 코드 경로가 있는 경우,
finally
블록을 반복적으로 작성해야 할 수 있습니다.
예외 안전성
RAII: 예외가 발생하더라도 객체의 소멸자가 자동으로 호출되므로, 자원이 안전하게 해제됩니다. 이로 인해 리소스 누수를 방지할 수 있습니다.
finally 블록: 예외가 발생한 경우에도
finally
블록에서 자원을 해제할 수 있지만, 여러 개의 자원을 관리해야 하는 경우 각 자원에 대해 별도의try-finally
블록을 작성해야 할 수 있습니다. 이는 코드가 복잡해질 수 있습니다.
유지보수성
RAII: 자원 관리가 객체의 생명 주기와 결합되어 있어, 자원의 획득과 해제가 명확하고 일관성 있게 이루어집니다. 이를 통해 유지보수가 용이해집니다.
finally 블록: 자원 해제 코드가 여러 곳에 분산되어 있을 수 있으며, 새로운 예외 처리 경로를 추가할 때 모든 관련
finally
블록을 수정해야 할 수 있습니다.
13.4 불변속성의 강제
함수의 선행조건이 충족되지 않을 경우 함수는 제대로 수행될 수 없다. -> 선행조건이란 매개변수를 의미한다.
불변 속성이란 개체가 유효한 상태로 존재하기 위해서 만족해야 하는 상태 혹은 조건이다.
생성자가 클래스의 불변속성을 만족시키지 않는다면 개체는 사용될 수 없다. -> 개체가 예측할 수 없는 상태 변화가 있을 수 있고, 이는 코드의 안정성과 신뢰성을 저해할 수 있기 때문이다.
위 2가지 문제가 있을 경우, 아래와 같은 3가지 접근 법을 섞어서 사용한다.
예외 처리
그냥 아무것도 하지 않는다. -> 선행 조건을 만족시키는 것은 호출자의 몫이고, 그대로 내버려두게 되면 개선된 설계, 테스트를 통해 시스템에서 오류가 사라지게 될 것이다.
프로그램을 종료한다. -> 선행 조건 위반은 심각한 설계 오류로 프로그램이 실행되지 않아야 한다.
희망하는 조건과 불변속성을 체크파기 위해서 범용적으로 assert 기법이 사용된다. assert 의 조건이 true이라면 체크하지 않지만, false 라면 아무런 코드도 생성되지 않는다.
<cassert> 에서 assert 를 제공하면 종류는 다음과 같다.
assert : 컴파일 타임, 런타임 조건을 모두 체크한다.
static_assert : 컴파일 타임 조건을 체크한다.
13.5 예외 던지기와 잡기
13.5.1. 예외 던지기
복사되거나, 이동될 수 있는 모든 타입의 예외를 throw 할 수 있다.
잡힌 개체는, 원칙상으로 던져진 개체의 사본이다. (최적화 매커니즘이 복사를 최소화하긴 하지만..) -> throw x; 는 x 타입의 임시 변수를 x 로 초기화한다. (이러한 임시 변수는 잡히기 전까지 몇 번 더 추가적으로 복사될 수 있다)
아래
throw No_copy();
에서는 기본 생성자가 호출되고, C++ 의 예외처리 시스템은 스택을 넘어 안전하게 전파되어야 한다. 이를 위해 개체를 복사하거나 이동되어야 하는데, 복사 생성자가 delete 로 선언되어 있어서 사용할 수 없다. -> 때문에,throw No_copy();
는 컴파일 에러가 발생한다.
예외를 던진 시점에서 스택을 거슬러 올라가면서 이전에 호출된 함수들에 대한 작업을 취소하고 스택 프레임을 제거하는 stack unwindding 이 발생한다. -> 이 때 함수에서 사용된 개체들의 소멸자가 호출된다.
아래 코드에서, h() 에서 던진 후에 생성된 모든 string 은 생성된 순서 역순으로 소멸된다. "not", "or", "excess", "in" 은 소멸되지만, 제어 스레드가 다다르지 못한 "all all" 은 소멸되지 않으며, 영향을 받지 않았던 "Byron" 도 소멸되지 않는다.
runtime_error 나, out_of_range 등의 표준 라이브러리 예외 클래스는 생성자 인자로 문자열 인자를 받아들이며, 해당 문자를 다시 출력할 함수 what() 을 가지고 있다.
13.5.1.1 noexcept 함수
일부 함수는 예외를 던지지 않으며, 실제로 일부는 예외를 던지지 말아야 한다.
이를 나타내기 위해서 그런 함수를 noexcept 로 선언할 수 있다.
noexcept 로 선언하는 것은 어떤 프로그램에 대해 추론하려는 프로그래머와 어떤 프로그램을 최적화하려는 컴파일러에게 가장 유용한 기능이다. -> 프로그래머는 try 절을 제공하려 신경쓰지 않아도 되고, -> 컴파일러의 최적화 매커니즘은 예외 처리로 발하는 제어 경로에 대해 신경쓰지 않아도 된다.
하지만 noexcept 는 컴파일러와 링커에 의해 완전히 체크되지 않는다. 예를 들어 vector 생성자가 자신의 데이터를 저장하기 위한 메모리를 획득하는데 실패해서 std::bad_alloc 을 던지게 되면 이 경우 프로그램은 std::terminate() 를 호출함으로 무조건적으로 종료된다.
noexcept 가 throw에 대응하게 작성되지 않았다는 점을 명시하는 것 만으로도 의미가 있다.
13.5.1.2 noexcept 연산자
아래 코드의 의미는 noexcept<Is_pod<T>()> 는 함수 Is_pod<T>() 가 true 이면 my_fct 함수는 예외를 던지지 않지만, false 라면 my_fct 함수는 예외를 던진다는 의미이다.
일반적인 noexcept 는 noexcept(true) 를 의미한다.
13.5.2 예외 잡기
원칙적으로 예외는 던져질 때 복사된다. -> 던져진 예외를 catch 까지 안전하게 전달하기 위해서, 개체의 복사 or 이동이 발생하게 된다.
catch 는 다음과 같은 경우 호출된다.
H 가 E 와 같은 타입인 경우
H 가 E 의 명맥한 공개 기반 타입인 경우
포인터 or 참조자인 경우 위 조건이 유효한 경우
13.5.2.1 다시 예외 던지기
예외는 붙잡았지만, 핸들러가 오류를 완벽하게 처리할 수 없다고 판단하는 것은 흔한 일이다. -> 그런 경우 핸들러에서 대개 지역적으로 처리할 수 있는 일을 수행하고 나서, 예외를 다시 던진다.
그에 따라서 오류는 가장 적합한 곳에서 처리될 수 있다.
13.5.2.2 모든 예외 잡기
std::exception 은 표준 라이브러리의 예외를 전부 잡는다. 하지만, 표준 라이브러리 예외는 단 하나의 예외 타입 집합이다. -> 따라서, std::exception& 도 모든 예외를 잡을수는 없다. -> int 나 특정 계층 구조에서 등장하는 예외를 던진다면, std::exception& 에 대한 핸들러에 의해 잡히지 않을 것이다.
모든 예외를 잡고 싶다면,
...
를 사용해서 모든 예외를 붙잡는다 라는 의미로 사용할 수 있다.
13.5.2.3 다중 헤더
try 블록은 여러개의 catch 를 가질 수 있다.
파생 된 예외가 하나 이상의 예외 타입에 대한 핸들러들에 의해 잡힐 수 있으므로, try 문에서 핸들러가 작성 된 순서가 중요하다.
13.6 vector 구현
Last updated