본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #32 안전한 예외 처리 코드를 작성하려면 메모리 관리를 주의 깊게 다루라

by 돼지왕 왕돼지 2017. 9. 25.
반응형

 [Effective Objective-C] #32 안전한 예외 처리 코드를 작성하려면 메모리 관리를 주의 깊게 다루라


출처 : Effective Objective-C

@finally, @try, arc, C++, c++ 예외, catch, compiler flag, default value, error passing, Finally, finally block release, fobjc-arc-exceptions, manual reference counting, MRC, nserror, Objective-C, objective-c++ 모드, TRY, [Effective Objective-C] #32 안전한 예외 처리 코드를 작성하려면 메모리 관리를 주의 깊게 다루라, 객체 선언부, 데이터베이스 연결, 등록 해제, 등록되지 않은 관찰자, 런타임, 런타임 성능 저하, 리펙터링, 리펙토링, 메모리 관리, 메모리 누수, 소멸자, 시스템 라이브러리, 시스템 리소스, 안전한 예외 처리, 에러 전달, 예외, 컴파일러 표식, 코드 생성, 키-값 관찰자, 파일 디스크립터, 파일 핸들러


-

예외는 많은 최신 언어에서 제공되는 기능이다.

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 는 예외가 발생했을 때 정리를 하는 코드를 만들어 내진 않는다.

이는 컴파일러 표식으로 동작하게 설정할 수 있다.

그러나 많은 양의 코드를 만들어 낼 것이고 런타임 성능에 좋지 않은 영향을 미칠 것이다.




반응형

댓글