Robust exception handling (번역)

https://eli.thegreenplace.net/2008/08/21/robust-exception-handling/ 번역

Exceptions vs. error status codes

예외는 에러 상태 코드를 반환하는 것보다 좋다. 몇몇 언어는 (파이썬 포함해서) 언어의 코어펑션과 표준 라이브러리가 throw 하는 예외를 처리해야 한다. 그렇지 않은 언어라도 오류 코드보다 예외를 선호하는게 낫다.

파이썬에서는 종종 ‘EAFP 과 LBYL 논쟁’ 이 있다.

  • EAFP : 허락보다는 용서를 구하는 것이 쉽다. Easier to Ask for Forgiveness than Permission
  • LBYL : 도약하기 전에 살펴보라. Look Before You Leap

Alex Martelli 의 말을 인용해보자.

도약하기 전에 살펴보기(LBYL) 라고도 하는 다른 언어의 일반적인 관용구는 작업을 실제로 하기 전에 미리 살펴보는 것이다. 모든 상황에 대해 미리 확인 하는 것인데 여러가지 이유로 적합하지 않을 수 있다.

  • 검사 : 미리 체크 하는 코드 / 작업 : 실제 수행하는 코드
  1. 모든것이 괜찮은 일반적인 주 케이스들에 대해 검사 코드는 가독성과 명확성을 감소 시킨다.
  2. 검사하는 코드는 작업 그 자체, 수행할 작업의 중복이다.
  3. 프로그래머는 아마도 쉽게 검사 코드를 누락하는 오류를 범할 것이다.
  4. 검사가 수행되는 순간과 작업이 시도되는 순간 사이의 상황이 변경될 수 있다.

LBYL 의 예

def do_something(filename):
  if not can_open_file(filename):
    return err(...)
  f = open(filename)
  ...

can_open_fileopen 이 성공할 것을 가정한다고 해보자. 이를 Alex 의 요점이 어떻게 적용되는지 확인해 보자.

  1. 가독성 감소는 덜 분명하지만, 더 많은 오류 검사가 추가된다면 가독성은 떨어진다. (C에 대한 경험이 많은 사람이라면, 누구나 일부 기능의 맨 위에 있는 일반적인 오류검사의 양에 익숙하다.)
  2. can_oen_fileopen 작업을 복제한다. open 이 built-in code 라서 DRY (don’t repeat yourself) 원칙을 위해하는 것을 덜 느끼게 된다.
  3. can_open_file 이 모든 체크를 다 했다고 어떻게 확신할 수 있나? 그렇지 않다면 어떻게 되는가?
  4. 다중처리 환경에서 작업한다고 가정해 보자. (서버에서 실행되는 웹 응용 프로그램이라고 생각해보자) can_open_fileopen 사이에 다른 프로세스가 그 파일을 열었거나 삭제 하거나 수정했다면?. 이는 디버깅 하기 어려운 경합이 발생한다.

EAFP 의 예

def do_something(filename):
  try:
    f = open(filename)
  except IOError, e:
    raise MyApplicationsExceptionType(e.msg)
    # could even pass whole traceback etc
    # etc...

LBYL 코드에서 발생한 문제점들이 발생하지 않으며 더 많은 유연성을 제공한다. 예를 들어 더 높은 수준에서 Exception 을 핸들링 하는게 적절하다고 판단되며, 원하면 예외 처리를 아예 안 할 수도 있다. 이는 에러 코드로는 하기 어렵다.

Never use exceptions for flow-control

예외는 예외적인 상황에서 존재한다. (정상적인 실행이 아닌 이벤트) 프로그래머가 str.find('substring') 을 호출한다면 그는 str 에서 substring 이 없다고 해서 예외가 발생할 것으로 생각하지 않는다. 이것은 find / 찾기 를 호출한 목적 자체가 substring 존재 여부를 확인하고자 했기 때문이다. 이 경우 예외 발생보다는 None 이나 -1 을 반환하는 것이 낫다. 그러나 그가 str[idx] 를 호출했다면 str 의 길이가 idx+1 보다 짧다면 예외가 발생하는 것이 적절하다. 왜냐하면 사용자는 idx 가 유효하다고 생각했지만 그렇지 않았다. 이는 “기대하지 못한 이벤트 (예기치 못한 상황)” 이다.

흐름제어에 사용되는 예외는 goto 와 같다.

Handle exceptions at the level that knows how to handle them

본질적으로, 예외는 여러 상위 계층으로 전파될 수 있고 여러 계층에서 처리(catch)될 수 있다.
질문이 생길 수 있다. – 어디서 예외를 catch 하는 것이 적절한 것인가.
가장 좋은 위치는 그 예외를 핸들링 할 수 있는 코드 조각이다. 프로그래밍 오류 (IndexError, TypeError, NameError 등) 같은 일부 예외는 프로그래머에게 직접 맡겨서 해결해 (handling) 면 예외는 사라진다.

당신이 완전한 어플리케이션을 가지고 있다면, 예외와 함께 무언가 실패하는 것은 적절하지 않다. 정중한 에러메세지를 보여주고 에러를 잘 로깅하는게 낫다. 나중에 분석할 때 도움이 될 것이다. 에러 다이얼로그에서 에러를 리포트 하겠냐고 물어보는 프로그램도 있다.

따라서 try/except 코드를 작성하기 전에 스스로에게 물어보라. “이 에러를 처리하기에 이곳이 적절한 위치인가? 여기에 에러를 처리하기에 충분한 정보가 있나?”

이것이 except: 구문은 모든 에러를 캐치하기 때문에 조심해야 한다. 너가 의도한 에러 외에도 모든 것을 캐치한다.

Do not expose implementation details with exceptions

위에서 언급한것 처럼 예외는 구현 계층 구조를 따라서 급행 열차를 타듯 전파된다. 예를 들어 그들은 캡슐화를 깨지기 쉽게 만들고 구현의 상세를 노출 시킨다.

예를 들어 파일을 사용해서 내부 캐시를 구현한 모듈을 만든다고 가정해 보자. 어떤 이유로 모듈이 캐시 파일을 열 수 없는 오류가 발생할 수 있다. open 함수는 IOError 를 발생시킬 것이다. 어디서 잡아야 할까?

당신이 만드는 객체를 사용하는 사용자에게 맡기지 말아라. 이는 캡슐화를 깨트린다. 사용자는 당신이 파일을 사용하고 있다는 사실을 인식하지 못한다. 어쩌면 다음 버전은 당신은 DB나 API 를 사용하고 있을 수도 있다.

오히려 모듈이 예외를 catch 하고 그것을 custom Exception (CachingFailedError) 로 감싸고, 가능한 많은 정보를 보존하고 전파해야 한다. 그러면 모듈 사용자는 CachingFailedError 만 catch 하면 되고 캐시 모듈이 바뀌더라도 그의 코드를 수정할 필요가 없어진다. 또한 사용자는 테스트 중에 오류의 원인을 조사하는 경우에 필요한 모든 정보를 얻을 수 있을 것이다.

예외를 올바르게 다시 발생시키는 것은 어렵다 만능 레시피도 없다. 더 많은 정보를 줄 수 있는가? 원래의 예외가 필요한가? 역추적이 필요한가?
이전 예외를 유지하면서 더 적절한 형식으로 예외를 다시 발생시키키만 하면 되는 경우는 다음을 수행한다.

class StuffCachingError(Exception): pass

def do_stuff():
    try:
        cache = open(filename)
        # do stuff with cache
    except IOError, e:
        raise StuffCachingError('Caching error: %s' % e)

이점

  1. 내부 구현을 외부 코드에 노출하지 않았다. 사용자는 do_stuff() 를 호출하는데 있어 StuffCahingError 만 처리하면 된다. 파일을 사용하는지는 알 바 아니다.
  2. 사용자가 error 에 대해서 깊이 분석하기 원할 때, 당신은 이미 원래의 에러를 메세지에 더해 제공했다.

원래의 역 추적도 유지해야 한다고 생각되면 (대부분 필요하지 않다.) 다음과 같이 작성하자.

class StuffCachingError(Exception): pass

def do_stuff():
    try:
        cache = open('z.pyg')
        # do stuff with cache
    except IOError:
        exc_class, exc, traceback = sys.exc_info()
        my_exc = StuffCachingError('Caching error: %s' % exc)
        raise my_exc.__class__, my_exc, traceback

이제 이전과 같이 StuffCachingError 예외가 발생하면, open 에러에 대한 traceback 이 발생한다.

Document the exceptions thrown by your code

문서는 그 프로그램의 외부 세계와의 계약이다.

  1. argument 를 전달해서 호출될 때의 기대하는 상태
  2. 반환되는 결과
  3. 사이드 이펙트
  4. throw 되는 예외

처음 3가지 정도를 문서화 하는 것이 일반적이지만 마지막 예외 는 문서화하지 않는다. 이것도 하면 좋겠다

Leave a Reply

Your email address will not be published. Required fields are marked *