[Effective Objective-C] #32 안전한 예외 처리 코드를 작성하려면 메모리 관리를 주의 깊게 다루라
출처 : Effective Objective-C
-
예외는 많은 최신 언어에서 제공되는 기능이다.
C 에는 예외라는 개념이 없지만 C++ 과 오브젝티브-C 에는 있다.
사실 최신 런타임에서는 C++ 과 오브젝티브-C 예외가 서로 호환된다.
이는 한 언어에서 발생한 예외를 다른 언어에서 catch 해서 처리할 수 있다는 것을 말한다.
-
오브젝티브-C 는 심각한 에러 처리에만 예외를 사용하도록 권장하긴 하지만 그래도 예외를 잡아 처리하는 코드가 필요할 수 있다.
예를 들어 예외가 발생하는 것에 대해 아무런 통제를 할 수 없는 오브젝티브-C++ 코드나 서드 파티 라이브러리를 사용하는 코드에서 예외 처리를 할 필요가 있다.
또 몇몇 시스템 라이브러리는 여전히 예외를 활용한다.
예를 들어 키-값 관찰자는 등록되지 않은 관찰자를 등록 해제하려고 하면 예외를 던진다.
-
매모리 관리 측면에서 예외는 흥미로운 문제를 일으킨다.
try 블록 내에서 객체가 리테인되고 그 객체가 릴리즈되기 전에 예외가 발생한다면 catch 블록에서 그 객체를 처리해주지 않을 경우 메모리 누수가 생길 것이다.
C++ 소멸자는 오브젝티브-C 예외 처리 루틴에서 실행된다.
C++ 에서 이 사실은 중요한데, 예외가 발생하여 일반적인 객체의 생명 주기를 다 채우지 못한 객체들은 제거(destroy) 되어야 하기 때문이다.
그렇지 않으면 그 객체가 사용한 메모리는 누수가 생길 수 있다.
여기서 언급하지 않은 파일 핸들러 같은 모든 시스템 리소스는 적절하게 반납되지 않을 것이다.
-
예외 처리 루틴에서 자동으로 객체를 제거하는 방법을 MRC 에서는 트릭으로 처리한다.
@finally 블록을 사용하는 것이다.
이 블록은 예외가 발생하는 것과 상관없이 무조건 딱 한 번 실행되는 것을 보장한다.
@finally 사용을 위해서는 객체의 선언 부분이 @try 블록 밖으로 나와야 한다.
@finally 블록에서 참조할 수 있어야 하기 때문이다.
릴리즈가 필요한 모든 객체를 이러한 방식으로 처리하는 것은 매우 힘들 수 있다.
로직이 이것보다 훨씬 복잡하고 @try 블록 내에 코드가 만다면 실수를 쉽게 저지를 수 있고 그러면 잠재적으로 객체 누수가 발생할 것이다.
파일 디스크립터나 데이터베이스 연결 같이 수가 제한되어 있는 리소스( 또는 그것을 관리하는 ) 객체의 누수라면
그 누수는 잠재적으로 큰 문제가 될 수 있다.
앱이 쓸데없이 모든 시스템 리소스를 점유할 것이기 때문이다.
-
ARC 를 사용하면 이 상황은 좀 더 심각해진다.
@finally 블록에 릴리스를 추가하는 트릭을 사용할 수 없기 때문이다.
release 를 호출하는 것이 허용되지 않기 때문이다. ( 즉 ARC 를 사용하면 사용자가 release 를 코드에 직접 사용할 수 없다. )
-
아마도 ARC 가 이 상황을 확실하게 처리하리라고 생각할 것이다
하지만 그렇지 않다.
그렇게 하기 위해선 예외가 발생했을 때 정리가 필요한 모든 객체를 추적하기 위한 많은 양의 보일러플레이트 코드가 필요하다.
또한 이 코드는 예외가 발생하지 않더라도 심각하게 런타임 성능을 저하시킨다.
또 부수적인 코드를 추가함으로써 앱 코드 크기를 상당히 증가시킨다.
이 부작용은 전혀 이상적이지 않다.
-
이 기능은 기본적으로 꺼져 있지만 ARC 는 예외를 안전하게 처리하는 추가 코드를 만들어 내는 기능이 있다.
그 코드는 컴파일러 표식(compiler flag)인 -fobjc-arc-exceptions 를 이용해 켤 수 있다.
기본적으로 이 기능이 꺼져 있는 이유는 오브젝티브-C 프로그래밍은 예외가 발생했을 때 앱이 종료되는 경우에만 예외를 사용하도록 하기 때문이다.
그렇기 때문에 어쨌든 앱이 종료된다면 잠재적인 메모리 누수는 상관없다.
앱이 종료된다면 안전하게 예외를 처리하는 추가적인 코드는 전혀 필요가 없다.
-
컴파일러가 오브젝티브-C++ 모드일 때 -fobj-arc-exceptions 표식은 기본적으로 켜져 있다.
C++ 은 ARC 가 구현할 코드와 비슷한 코드가 필요하다.
그래서 ARC 가 예외를 안전하게 처리하기 위해 집어넣는 코드보다 성능 저하가 크지 않다.
또 C++ 은 예외를 많이 활용한다.
그래서 오브젝티브-C++ 을 사용하는 개발자는 예외를 사용하길 원할 가능성이 크다.
-
MRC 를 사용하고 예외를 잡아 처리해야 한다면 앞서와 같은 방법으로 정확히 정리하는 코드를 작성해야 하는 것을 기억하라.
ARC 를 사용하고 반드시 예외를 잡아서 처리해야 한다면 -fobj-arc-exceptions 표식을 켜야 할 것이다.
그러나 예외를 처리하기 위해 너무 많은 코드를 작성해야 한다면 그 대신 NSError 스타일의 에러 전달(error passing)을 활용하도록 리펙터링을 하라
생각해야 할 것들
예외가 잡혔을 때 try 블록 내에서 생성한 객체는 반드시 필요한 정리 작업이 수행되도록 보장하라.
기본적으로 ARC 는 예외가 발생했을 때 정리를 하는 코드를 만들어 내진 않는다.
이는 컴파일러 표식으로 동작하게 설정할 수 있다.
그러나 많은 양의 코드를 만들어 낼 것이고 런타임 성능에 좋지 않은 영향을 미칠 것이다.
댓글