Exception handling과 readability

Tips 2006. 12. 27. 02:09
프로그래밍을 하다 보면 참으로 어처구니없이도 많은 Exception을 만나게 됩니다.
언어와 플랫폼에 무관하게 언제든지 exception은 프로그래머를 잡아먹을 기세로 눈을 번뜩이고 있습니다. 그래서 사용하는 exception처리 기법이 try-catch 구조 입니다.

의외로 경력 깨나 된다는 분들도 try-catch 구문을 무시하는 경향이 있습니다.
readability(가독성)을 몹시 떨어뜨리는 데다가 로직의 흐름을 한눈에 보기 힘들게 만들기도 하고, 코드가 길어지게 합니다. 때로는 디버그를 위해서 어떤 에러가 나는지를 직접 에러를 일으켜 보면서 따라가 봐야 할 때도 있습니다. 그래서 경험많은 프로그래머들도 깜빡 빼먹기 쉽습니다.

이쯤 읽으셨으면 "나는 이거 안읽어도 되겠다", 혹은 "한번 읽어봐야겠다"는 판단이 드실겁니다. 맞게 판단하셨으니 그렇게 하시면 됩니다^^ 어렵게 익힌 테크닉이나 심오한 로직따위는 나오지 않습니다.;;;

각설하고..
try-catch의 아이디어는 간단합니다.

어떤 코드를 우선 try(시도)합니다. 시도 결과 exception이 발생하면, 미리 정의된 exception 대응 행동(catch)을 수행합니다. 요즘의 대부분의 CPU는 exception이 생기는 몇가지 상황을 정의해 두었고 이 때에 특정 핀에 신호가 뜸으로서 exception을 알려줍니다. 이를 직접 사용하기도 하고, 컴파일러, 특정 API 또는 인터프리터레벨에서 한단계 더 포장하여 사용하는 경우도 많이 있습니다.
뭐가 됐건 간에 시도후에 실패하면 처리할 코드를 미리 정의해 두는게 기본 아이디어입니다.

C++, C# 등에서는,
[CODE type=cpp]
try {
  // some statements...
} catch (Exception e) {
  // handling statements...
}
[/CODE]
이와 같은 구문을 쓰죠.. 발생한 exception은 보통 객체(객체의 참조)로 전달받을수 있는데, 이는 사용하는 컴파일러, API, SDK, Framwork등 오만가지 이름으로 불리워지는 개발 플랫폼-_-에 의해 그 Type 명이 달라지기도 하므로 사용하는 개발환경의 문서를 참고하여 만들면 됩니다.

Python에서는
[CODE type=python]
try:
  # some statements
except:
  # handling statements
[/CODE]
이렇게 쓰면 됩니다.

미리 정의된 구체적인 Exception을 핸들링 할 때에는, 예를들어,
[CODE type=python]
try:
  # some statements
except KeyboardInterrupt:
  # Ctrl-C handling statements
except:
  # handling statements
else:
  # No exception
[/CODE]
이런 구문으로 Ctrl-C키를 눌러 중단시그널을 보냈을 경우와 기타 exception이 일어났을 경우, exception이 하나도 안생겼을 경우 수행할 코드까지 모두 정의 가능합니다.
[CODE type=python]
try:
  # some statements
finally:
  # whether an exception is asserted or not
[/CODE]
이렇게 하면 try후에 exception이 있던 없던 간에 finally 코드가 수행됩니다.
하지만 except: 구문과 finally: 구문은 함께 있을수는 없습니다. Python document에 의하면, 어떤 것을 먼저 수행해야 하는지가 모호해 지기 때문에 막는다고 되어있군요..

어찌됐건 예기치 못한 에러 상황에서 애써 작성한 프로그램이 죽어버리는 일이 생기지 않도록 하려면 exception handler를 만들어 주어서 에러를 견뎌 내도록 만들어 주어야 합니다.
이게 말이 쉽지 막상 그렇게 코드를 만들려고 하면 보통 짜증나는 일이 아닙니다.ㅡ_ㅡ

다음 예를 보죠..
[code type=python]
def get_host_and_port(s_addr='localhost:13579'):
  '''
  s_addr로 hostname:port notation의 서버주소를 받아서 호스트명과 포트번호를 쪼갠다.
  '''
  s_fields = s_addr.split(':')
  from string import atoi
  return s_fields[0], atoi(s_fields[1])
[/code]
이 간단한 함수는 고맙게도 왠만해서는 매우 훌륭히 작동해 줍니다^^
Python의 interactive-mode prompt에서 수행하면 이렇게 됩니다.
[code type=python]
>>> get_host_and_port('asdf.com:1234')
('asdf.com', 1234)
[/code]

그런데 문제가 있죠..
[code type=python]
>>> get_host_and_port('asdf.com')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 4, in get_host_and_port
IndexError: list index out of range
[/code]
그렇습니다. 포트번호를 안주면 에러가 납니다.
그래서 코드를 고칩니다.
[code type=python]
def get_host_and_port(s_addr='localhost:13579'):
  '''
  s_addr로 hostname:port notation의 서버주소를 받아서 호스트명과 포트번호를 쪼갠다.
  '''
  s_fields = s_addr.split(':')
  from string import atoi
  try:
    hostname = s_fields[0]
   portnum = atoi(s_fields[1])
  except:
    portnum = 13579 # default port로 가정
  return hostname, portnum
[/code]
이번에는 잘 될까요?

[code type=python]
>>> get_host_and_port('asdf.com')
('asdf.com', 13579)
>>> get_host_and_port(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 5, in get_host_and_port
AttributeError: 'int' object has no attribute 'split'
[/code]
이전에 exception을 내던 문제는 해결됐는데 다른 문제가 또 있군요..
이번 문제는 parameter의 type문제 입니다. 결정을 해야 합니다. 숫자 파라메터를 받을수 없다고 exception을 내버리던지, 에러를 리턴하던지, 무시하고 미리 정의된 기본값으로 리턴할지..

이렇게 수정해 봅니다.
[code type=python]
def get_host_and_port(s_addr='localhost:13579'):
  '''
  s_addr로 hostname:port notation의 서버주소를 받아서 호스트명과 포트번호를 쪼갠다.
  '''
  try:
    s_fields = s_addr.split(':')
  except:
   return ('localhost', 13579)

  from string import atoi
  try:
    hostname = s_fields[0]
   portnum = atoi(s_fields[1])
  except:
   portnum = 13579 # default port로 가정
  return hostname, portnum
[/code]
처음의 단 두줄짜리 함수가 이렇게 지저분하게 indenting되며 늘어집니다.
자칫 코드에 로직이 선명히 보이지 않게 될 수 있습니다.
지금 예로 든 함수도 적절히 디버그되지 못하였습니다. 다만 try-catch 구문의 거의 유일한 단점인 가독성 저해를 강조해 보려고 일부러 저렇게 만든 것입니다. 가져다 쓰지 마세요. 위험한 프로그램을 만들어 줄겁니다;;;
(참고로 위 함수는 리턴이 두번 나옵니다. 매우 바람직하지 못하므로 도저히 피해갈수 없는 상황을 제외하고서는 절대 여러개의 리턴을 가지는 함수를 만들지 마세요. 석달후에 후회합니다. 게다가 아직도 핸들링되지 않은 exception발생가능성이 10,12,16 라인 등에 도사리고 있습니다. 로직 자체에도 결함이 있습니다. 귀차니즘의 압박으로...쿨럭;;;)

이렇게 코드가 더러워진듯 보여도 실제 저 코드를 사용하는 프로그램이 죽어버릴 가능성은 많이 줄어들었습니다^^

안정적으로 잘 돌아가는 프로그램일수록 소스코드는 try-catch로 얼룩져 있습니다.
개발직후 갖은 혹독한 테스트 환경에서 수도없이 많은 어처구니없는 exception을 리포팅 받아 모두 적절히 견뎌낼 수 있도록 만들어지기 때문에 안정적인 프로그램이라고 칭송받는 겁니다. 많은 이들이 욕하는 M$의 제품군들 역시 수많은 try-catch로 얼룩져 있으며, 윈도우98시절에 비해 현재는 M$의 소프트웨어가 대단한 안정성 향상을 이룩했다는 것을 누구나 인정합니다.
(절대 밝혀져서는 안될 어둠의 루트로 M$ 코드의 일부를 본적이 있습니다^^;;;)

try-catch의 거의 유일한 단점이 코드를 지저분하게 만드는 것이라고 했는데, 사실 습관 들이기 나름으로 가독성을 저해하지 않기도 합니다. 이건 제가 뭐라 말할 수 있는 성격이 아니군요.. 프로그래머 각자가 exception handling 코드를 포함해서도 읽기 좋은 코드로 만드는 습관을 들이는 것이 스스로에게도 도움되는 일일 것입니다. try-catch를 가급적 nesting안한다던지.. 등등등

유일한 단점이 아니라 "거의" 유일한 단점이라고 했는데, 다른 단점들도 많이 있겠지만 또 한가지 들수 있는 단점은, 완벽한 try-catch는 에러를 내지 않는다는 겁니다. 이건 또 무슨 말일까요?ㅡ_ㅡ

try-catch로 잘 막아서 절대 에러가 나지 않는 어떤 함수를 만들었다고 해 봅시다.
이런 함수를 다른팀의 누군가가 사용하려고 합니다. 이사람은 잘못된 parameter를 입력으로 넘겨주는 엄청난 버그를 만들어 놓고도 절대 발견하지 못합니다. 우리가 제공해준 함수는 어떤 잘못된 입력이 와도 어쨌거나 에러 없이 코드를 수행하고 적당히 리턴값을 만들어 주니까요.. 버그를 만든 사람은 역시 다른팀 코드를 가져다 쓰니 로직에 문제가 생긴다고 생각해 버리겠죠..

그래서 assert나 raise (어떤 언어에서는 throw)구문을 활용해서 적절히 일부러 exception을 일으켜 주거나, 전통적인 C함수의 방법처럼 리턴코드를 항상 리턴값에 포함시켜서, 현재 리턴값은 exception handling의 결과임을 알리는 방법을 사용해야 합니다. exception으로 인해 소프트웨어가 멈추지는 않더라도 call-stack의 상위 코드에 exception발생사실을 알려줄 필요가 있다는 거죠..

도무지 엔지니어링의 세계는 얼마나 많은 trade-off를 더 해야 할지 모르는 세계입니다.
저는 아직도 어디까지를 raise하고 어디까지를 감춘채 handling할지를 결정보지 못했습니다. 아마 적어도 제가 죽기전에는 그런 공식은 나오지 않을겁니다.

마지막으로 은근히 유용한 try-catch 구문의 장점을 말씀드리겠습니다.
"실력있어 보인다"는 겁니다.ㅡ_ㅡ;;; 서두에도 말했던 것처럼 생각보다 많은 개발자 분들이 십수년의 경력에도 불구하고 exception에는 관대합니다. 이 글을 읽으신 여러분이 exception에 엄격해 진다면 실력있어 보이는 것을 넘어 실력있는 개발자로 인정 받으실 겁니다.

자신감을 가지세요.. 여러분이 생각하는것보다 훨씬 많은 사람이 당신의 코드를 보고 감탄합니다. 진짜로 여러분이 만든 코드는 뛰어납니다. 모든 사람의 머리가 다르기 때문에 항상 새로운 코드가 만들어지기 때문입니다. 자신있게 작성하고 테스트하고 진화(evolution, innovation)해야 더 훌륭한 코드를 만들수 있을 겁니다.

'Tips' 카테고리의 다른 글

my.cnf에 wait_timeout설정하는 문제  (0) 2008.01.31
: